add allocation target
This commit is contained in:
parent
41b0e1f66a
commit
30200d5ff4
|
@ -44,6 +44,15 @@ func generateConfigFile(cwd string) {
|
|||
config := `
|
||||
journal_path: "%s"
|
||||
db_path: "%s"
|
||||
allocation_targets:
|
||||
- name: Debt
|
||||
target: 40
|
||||
accounts:
|
||||
- Asset:Debt:*
|
||||
- name: Equity
|
||||
target: 60
|
||||
accounts:
|
||||
- Asset:Equity:*
|
||||
commodities:
|
||||
- name: NIFTY
|
||||
type: mutualfund
|
||||
|
|
|
@ -10,3 +10,4 @@
|
|||
# Reference Guide
|
||||
|
||||
- [Accounts](accounts.md)
|
||||
- [Allocation Targets](allocation-targets.md)
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# Allocation Targets
|
||||
|
||||
Paisa allows you to set a allocation target for a group of
|
||||
accounts. The allocation page shows how far your current allocation is
|
||||
from the allocation target. For example, to keep a 40:60 split between
|
||||
debt and equity, add the following configuration to the `paisa.yaml`
|
||||
file. The account name can have `*` which matches any characters
|
||||
|
||||
```yaml
|
||||
allocation_targets:
|
||||
- name: Debt
|
||||
target: 40
|
||||
accounts:
|
||||
- Asset:Debt:*
|
||||
- name: Equity
|
||||
target: 60
|
||||
accounts:
|
||||
- Asset:Equity:*
|
||||
```
|
2
go.mod
2
go.mod
|
@ -8,7 +8,7 @@ require (
|
|||
github.com/google/btree v1.0.1
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/samber/lo v1.11.0
|
||||
github.com/samber/lo v1.25.0
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spf13/cobra v1.4.0
|
||||
github.com/spf13/viper v1.10.1
|
||||
|
|
4
go.sum
4
go.sum
|
@ -76,8 +76,8 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ
|
|||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/samber/lo v1.11.0 h1:JfeYozXL1xfkhRUFOfH13ociyeiLSC/GRJjGKI668xM=
|
||||
github.com/samber/lo v1.11.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A=
|
||||
github.com/samber/lo v1.25.0 h1:H8F6cB0RotRdgcRCivTByAQePaYhGMdOTJIj2QFS2I0=
|
||||
github.com/samber/lo v1.25.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -10,6 +11,7 @@ import (
|
|||
"github.com/ananthakumaran/paisa/internal/model/posting"
|
||||
"github.com/ananthakumaran/paisa/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/viper"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
|
@ -20,6 +22,19 @@ type Aggregate struct {
|
|||
MarketAmount float64 `json:"market_amount"`
|
||||
}
|
||||
|
||||
type AllocationTargetConfig struct {
|
||||
Name string
|
||||
Target float64
|
||||
Accounts []string
|
||||
}
|
||||
|
||||
type AllocationTarget struct {
|
||||
Name string `json:"name"`
|
||||
Target float64 `json:"target"`
|
||||
Current float64 `json:"current"`
|
||||
Aggregates map[string]Aggregate `json:"aggregates"`
|
||||
}
|
||||
|
||||
func GetAllocation(db *gorm.DB) gin.H {
|
||||
var postings []posting.Posting
|
||||
result := db.Where("account like ?", "Asset:%").Order("date ASC").Find(&postings)
|
||||
|
@ -34,7 +49,8 @@ func GetAllocation(db *gorm.DB) gin.H {
|
|||
})
|
||||
aggregates := computeAggregate(postings, now)
|
||||
aggregates_timeline := computeAggregateTimeline(postings)
|
||||
return gin.H{"aggregates": aggregates, "aggregates_timeline": aggregates_timeline}
|
||||
allocation_targets := computeAllocationTargets(postings)
|
||||
return gin.H{"aggregates": aggregates, "aggregates_timeline": aggregates_timeline, "allocation_targets": allocation_targets}
|
||||
}
|
||||
|
||||
func computeAggregateTimeline(postings []posting.Posting) []map[string]Aggregate {
|
||||
|
@ -55,6 +71,37 @@ func computeAggregateTimeline(postings []posting.Posting) []map[string]Aggregate
|
|||
return timeline
|
||||
}
|
||||
|
||||
func computeAllocationTargets(postings []posting.Posting) []AllocationTarget {
|
||||
var targetAllocations []AllocationTarget
|
||||
var configs []AllocationTargetConfig
|
||||
viper.UnmarshalKey("allocation_targets", &configs)
|
||||
|
||||
totalMarketAmount := lo.Reduce(postings, func(acc float64, p posting.Posting, _ int) float64 { return acc + p.MarketAmount }, 0.0)
|
||||
|
||||
for _, config := range configs {
|
||||
targetAllocations = append(targetAllocations, computeAllocationTarget(postings, config, totalMarketAmount))
|
||||
}
|
||||
|
||||
return targetAllocations
|
||||
}
|
||||
|
||||
func computeAllocationTarget(postings []posting.Posting, config AllocationTargetConfig, total float64) AllocationTarget {
|
||||
date := time.Now()
|
||||
postings = lo.Filter(postings, func(p posting.Posting, _ int) bool {
|
||||
return lo.SomeBy(config.Accounts, func(accountGlob string) bool {
|
||||
match, err := filepath.Match(accountGlob, p.Account)
|
||||
if err != nil {
|
||||
log.Fatal("Invalid account value used in target_allocations", accountGlob, err)
|
||||
}
|
||||
return match
|
||||
})
|
||||
})
|
||||
|
||||
aggregates := computeAggregate(postings, date)
|
||||
currentTotal := lo.Reduce(postings, func(acc float64, p posting.Posting, _ int) float64 { return acc + p.MarketAmount }, 0.0)
|
||||
return AllocationTarget{Name: config.Name, Target: config.Target, Current: (currentTotal / total) * 100, Aggregates: aggregates}
|
||||
}
|
||||
|
||||
func computeAggregate(postings []posting.Posting, date time.Time) map[string]Aggregate {
|
||||
byAccount := lo.GroupBy(postings, func(p posting.Posting) string { return p.Account })
|
||||
result := make(map[string]Aggregate)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import $ from "jquery";
|
||||
import * as d3 from "d3";
|
||||
import legend from "d3-svg-legend";
|
||||
import dayjs from "dayjs";
|
||||
|
@ -5,6 +6,7 @@ import _ from "lodash";
|
|||
import {
|
||||
Aggregate,
|
||||
ajax,
|
||||
AllocationTarget,
|
||||
formatCurrency,
|
||||
formatFloat,
|
||||
lastName,
|
||||
|
@ -12,88 +14,282 @@ import {
|
|||
rainbowScale,
|
||||
secondName,
|
||||
textColor,
|
||||
tooltip
|
||||
tooltip,
|
||||
skipTicks
|
||||
} from "./utils";
|
||||
|
||||
export default async function () {
|
||||
const { aggregates: aggregates, aggregates_timeline: aggregatesTimeline } =
|
||||
await ajax("/api/allocation");
|
||||
const {
|
||||
aggregates: aggregates,
|
||||
aggregates_timeline: aggregatesTimeline,
|
||||
allocation_targets: allocationTargets
|
||||
} = await ajax("/api/allocation");
|
||||
_.each(aggregates, (a) => (a.timestamp = dayjs(a.date)));
|
||||
_.each(aggregatesTimeline, (aggregates) =>
|
||||
_.each(aggregates, (a) => (a.timestamp = dayjs(a.date)))
|
||||
);
|
||||
renderAllocationTarget(allocationTargets);
|
||||
renderAllocation(aggregates);
|
||||
renderAllocationTimeline(aggregatesTimeline);
|
||||
}
|
||||
|
||||
function renderAllocationTarget(allocationTargets: AllocationTarget[]) {
|
||||
const id = "#d3-allocation-target";
|
||||
|
||||
if (_.isEmpty(allocationTargets)) {
|
||||
$(id).closest(".container").hide();
|
||||
return;
|
||||
}
|
||||
allocationTargets = _.sortBy(allocationTargets, (t) => t.name);
|
||||
const BAR_HEIGHT = 25;
|
||||
const svg = d3.select(id),
|
||||
margin = { top: 20, right: 0, bottom: 10, left: 150 },
|
||||
fullWidth = document.getElementById(id.substring(1)).parentElement
|
||||
.clientWidth,
|
||||
width = fullWidth - margin.left - margin.right,
|
||||
height = allocationTargets.length * BAR_HEIGHT * 2,
|
||||
g = svg
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
svg.attr("height", height + margin.top + margin.bottom);
|
||||
|
||||
const keys = ["target", "current"];
|
||||
const colorKeys = ["target", "current", "diff"];
|
||||
const colors = ["#1f77b4", "#17becf", "#4a4a4a"];
|
||||
|
||||
const y = d3.scaleBand().range([0, height]).paddingInner(0).paddingOuter(0);
|
||||
y.domain(allocationTargets.map((t) => t.name));
|
||||
|
||||
const y1 = d3
|
||||
.scaleBand()
|
||||
.range([0, y.bandwidth()])
|
||||
.domain(keys)
|
||||
.paddingInner(0)
|
||||
.paddingOuter(0.1);
|
||||
|
||||
const z = d3.scaleOrdinal<string>(colors).domain(colorKeys);
|
||||
|
||||
const maxX = _.chain(allocationTargets)
|
||||
.flatMap((t) => [t.current, t.target])
|
||||
.max()
|
||||
.value();
|
||||
const targetWidth = 400;
|
||||
const targetMargin = 20;
|
||||
const textGroupWidth = 150;
|
||||
const textGroupMargin = 20;
|
||||
const textGroupZero = targetWidth + targetMargin;
|
||||
|
||||
const x = d3
|
||||
.scaleLinear()
|
||||
.range([textGroupZero + textGroupWidth + textGroupMargin, width]);
|
||||
x.domain([0, maxX]);
|
||||
const x1 = d3.scaleLinear().range([0, targetWidth]).domain([0, maxX]);
|
||||
|
||||
g.append("line")
|
||||
.attr("stroke", "#ddd")
|
||||
.attr("x1", 0)
|
||||
.attr("y1", height)
|
||||
.attr("x2", width)
|
||||
.attr("y2", height);
|
||||
|
||||
g.append("text")
|
||||
.attr("fill", "#4a4a4a")
|
||||
.text("Target")
|
||||
.attr("text-anchor", "end")
|
||||
|
||||
.attr("x", textGroupZero + (textGroupWidth * 1) / 3)
|
||||
.attr("y", -5);
|
||||
|
||||
g.append("text")
|
||||
.attr("fill", "#4a4a4a")
|
||||
.text("Current")
|
||||
.attr("text-anchor", "end")
|
||||
.attr("x", textGroupZero + (textGroupWidth * 2) / 3)
|
||||
.attr("y", -5);
|
||||
|
||||
g.append("text")
|
||||
.attr("fill", "#4a4a4a")
|
||||
.text("Diff")
|
||||
.attr("text-anchor", "end")
|
||||
.attr("x", textGroupZero + textGroupWidth)
|
||||
.attr("y", -5);
|
||||
|
||||
g.append("g")
|
||||
.attr("class", "axis y")
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
.call(
|
||||
d3
|
||||
.axisBottom(x1)
|
||||
.tickSize(-height)
|
||||
.tickFormat(skipTicks(40, x, (n) => formatFloat(n, 0)))
|
||||
);
|
||||
|
||||
g.append("g").attr("class", "axis y dark").call(d3.axisLeft(y));
|
||||
|
||||
const textGroup = g
|
||||
.append("g")
|
||||
.selectAll("g")
|
||||
.data(allocationTargets)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", "inline-text");
|
||||
|
||||
textGroup
|
||||
.append("line")
|
||||
.attr("stroke", "#ddd")
|
||||
.attr("x1", 0)
|
||||
.attr("y1", (t) => y(t.name))
|
||||
.attr("x2", width)
|
||||
.attr("y2", (t) => y(t.name));
|
||||
|
||||
textGroup
|
||||
.append("text")
|
||||
.text((t) => formatFloat(t.target))
|
||||
.attr("text-anchor", "end")
|
||||
.attr("alignment-baseline", "middle")
|
||||
.style("fill", z("target"))
|
||||
.attr("x", textGroupZero + (textGroupWidth * 1) / 3)
|
||||
.attr("y", (t) => y(t.name) + y.bandwidth() / 2);
|
||||
|
||||
textGroup
|
||||
.append("text")
|
||||
.text((t) => formatFloat(t.current))
|
||||
.attr("text-anchor", "end")
|
||||
.attr("alignment-baseline", "middle")
|
||||
.style("fill", z("current"))
|
||||
.attr("x", textGroupZero + (textGroupWidth * 2) / 3)
|
||||
.attr("y", (t) => y(t.name) + y.bandwidth() / 2);
|
||||
|
||||
textGroup
|
||||
.append("text")
|
||||
.text((t) => formatFloat(t.current - t.target))
|
||||
.attr("text-anchor", "end")
|
||||
.attr("alignment-baseline", "middle")
|
||||
.style("fill", z("diff"))
|
||||
.attr("x", textGroupZero + (textGroupWidth * 3) / 3)
|
||||
.attr("y", (t) => y(t.name) + y.bandwidth() / 2);
|
||||
|
||||
const groups = g
|
||||
.append("g")
|
||||
.selectAll("g.group")
|
||||
.data(allocationTargets)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", "group")
|
||||
.attr("transform", (t) => "translate(0," + y(t.name) + ")");
|
||||
|
||||
groups
|
||||
.selectAll("g")
|
||||
.data((t) => [
|
||||
{ key: "target", value: t.target },
|
||||
{ key: "current", value: t.current }
|
||||
])
|
||||
.enter()
|
||||
.append("rect")
|
||||
.attr("fill", (d) => {
|
||||
return z(d.key);
|
||||
})
|
||||
.attr("x", x1(0))
|
||||
.attr("y", (d) => y1(d.key))
|
||||
.attr("height", y1.bandwidth())
|
||||
.attr("width", (d) => x1(d.value));
|
||||
|
||||
const paddingTop = (y1.range()[1] - y1.bandwidth() * 2) / 2;
|
||||
d3.select("#d3-allocation-target-treemap")
|
||||
.append("div")
|
||||
.style("height", height + margin.top + margin.bottom + "px")
|
||||
.style("position", "absolute")
|
||||
.style("width", "100%")
|
||||
.selectAll("div")
|
||||
.data(allocationTargets)
|
||||
.enter()
|
||||
.append("div")
|
||||
.style("position", "absolute")
|
||||
.style("left", margin.left + x(0) + "px")
|
||||
.style("top", (t) => margin.top + y(t.name) + paddingTop + "px")
|
||||
.style("height", y1.bandwidth() * 2 + "px")
|
||||
.style("width", x.range()[1] - x.range()[0] + "px")
|
||||
.append("div")
|
||||
.style("position", "relative")
|
||||
.attr("height", y1.bandwidth() * 2)
|
||||
.each(function (t) {
|
||||
renderPartition(this, t.aggregates, d3.treemap());
|
||||
});
|
||||
}
|
||||
|
||||
function renderAllocation(aggregates: { [key: string]: Aggregate }) {
|
||||
const allocation = function (id, hierarchy) {
|
||||
const div = d3.select("#" + id),
|
||||
margin = { top: 0, right: 0, bottom: 0, left: 20 },
|
||||
width =
|
||||
document.getElementById(id).parentElement.clientWidth -
|
||||
margin.left -
|
||||
margin.right,
|
||||
height = +div.attr("height") - margin.top - margin.bottom;
|
||||
renderPartition(
|
||||
document.getElementById("d3-allocation-category"),
|
||||
aggregates,
|
||||
d3.partition()
|
||||
);
|
||||
renderPartition(
|
||||
document.getElementById("d3-allocation-value"),
|
||||
aggregates,
|
||||
d3.treemap()
|
||||
);
|
||||
}
|
||||
|
||||
const percent = (d) => {
|
||||
return formatFloat((d.value / root.value) * 100) + "%";
|
||||
};
|
||||
function renderPartition(element: HTMLElement, aggregates, hierarchy) {
|
||||
const div = d3.select(element),
|
||||
margin = { top: 0, right: 0, bottom: 0, left: 20 },
|
||||
width = element.parentElement.clientWidth - margin.left - margin.right,
|
||||
height = +div.attr("height") - margin.top - margin.bottom;
|
||||
|
||||
const color = rainbowScale(_.keys(aggregates));
|
||||
|
||||
const stratify = d3
|
||||
.stratify<Aggregate>()
|
||||
.id((d) => d.account)
|
||||
.parentId((d) => parentName(d.account));
|
||||
|
||||
const partition = hierarchy.size([width, height]).round(true);
|
||||
|
||||
const root = stratify(_.sortBy(aggregates, (a) => a.account))
|
||||
.sum((a) => a.market_amount)
|
||||
.sort(function (a, b) {
|
||||
return b.height - a.height || b.value - a.value;
|
||||
});
|
||||
|
||||
partition(root);
|
||||
|
||||
const cell = div
|
||||
.selectAll(".node")
|
||||
.data(root.descendants())
|
||||
.enter()
|
||||
.append("div")
|
||||
.attr("class", "node")
|
||||
.attr("data-tippy-content", (d) => {
|
||||
return tooltip([
|
||||
["Account", [d.id, "has-text-right"]],
|
||||
[
|
||||
"MarketAmount",
|
||||
[formatCurrency(d.value), "has-text-weight-bold has-text-right"]
|
||||
],
|
||||
["Percentage", [percent(d), "has-text-weight-bold has-text-right"]]
|
||||
]);
|
||||
})
|
||||
.style("top", (d: any) => d.y0 + "px")
|
||||
.style("left", (d: any) => d.x0 + "px")
|
||||
.style("width", (d: any) => d.x1 - d.x0 + "px")
|
||||
.style("height", (d: any) => d.y1 - d.y0 + "px")
|
||||
.style("background", (d) => color(d.id))
|
||||
.style("color", (d) => textColor(color(d.id)));
|
||||
|
||||
cell
|
||||
.append("p")
|
||||
.attr("class", "heading has-text-weight-bold")
|
||||
.text((d) => lastName(d.id));
|
||||
|
||||
cell
|
||||
.append("p")
|
||||
.attr("class", "heading has-text-weight-bold")
|
||||
.style("font-size", ".5 rem")
|
||||
.text(percent);
|
||||
const percent = (d) => {
|
||||
return formatFloat((d.value / root.value) * 100) + "%";
|
||||
};
|
||||
|
||||
allocation("d3-allocation-category", d3.partition());
|
||||
allocation("d3-allocation-value", d3.treemap());
|
||||
const color = rainbowScale(_.keys(aggregates));
|
||||
|
||||
const stratify = d3
|
||||
.stratify<Aggregate>()
|
||||
.id((d) => d.account)
|
||||
.parentId((d) => parentName(d.account));
|
||||
|
||||
const partition = hierarchy.size([width, height]).round(true);
|
||||
|
||||
const root = stratify(_.sortBy(aggregates, (a) => a.account))
|
||||
.sum((a) => a.market_amount)
|
||||
.sort(function (a, b) {
|
||||
return b.height - a.height || b.value - a.value;
|
||||
});
|
||||
|
||||
partition(root);
|
||||
|
||||
const cell = div
|
||||
.selectAll(".node")
|
||||
.data(root.descendants())
|
||||
.enter()
|
||||
.append("div")
|
||||
.attr("class", "node")
|
||||
.attr("data-tippy-content", (d) => {
|
||||
return tooltip([
|
||||
["Account", [d.id, "has-text-right"]],
|
||||
[
|
||||
"MarketAmount",
|
||||
[formatCurrency(d.value), "has-text-weight-bold has-text-right"]
|
||||
],
|
||||
["Percentage", [percent(d), "has-text-weight-bold has-text-right"]]
|
||||
]);
|
||||
})
|
||||
.style("top", (d: any) => d.y0 + "px")
|
||||
.style("left", (d: any) => d.x0 + "px")
|
||||
.style("width", (d: any) => d.x1 - d.x0 + "px")
|
||||
.style("height", (d: any) => d.y1 - d.y0 + "px")
|
||||
.style("background", (d) => color(d.id))
|
||||
.style("color", (d) => textColor(color(d.id)));
|
||||
|
||||
cell
|
||||
.append("p")
|
||||
.attr("class", "heading has-text-weight-bold")
|
||||
.text((d) => lastName(d.id));
|
||||
|
||||
cell
|
||||
.append("p")
|
||||
.attr("class", "heading has-text-weight-bold")
|
||||
.style("font-size", ".5 rem")
|
||||
.text(percent);
|
||||
}
|
||||
|
||||
function renderAllocationTimeline(
|
||||
|
|
|
@ -71,10 +71,10 @@ function renderTable(gain: Gain) {
|
|||
|
||||
function renderOverview(gains: Gain[]) {
|
||||
gains = _.sortBy(gains, (g) => g.account);
|
||||
const BAR_HEIGHT = 13;
|
||||
const BAR_HEIGHT = 15;
|
||||
const id = "#d3-gain-overview";
|
||||
const svg = d3.select(id),
|
||||
margin = { top: 40, right: 40, bottom: 80, left: 150 },
|
||||
margin = { top: 5, right: 20, bottom: 30, left: 150 },
|
||||
width =
|
||||
document.getElementById(id.substring(1)).parentElement.clientWidth -
|
||||
margin.left -
|
||||
|
@ -92,7 +92,7 @@ function renderOverview(gains: Gain[]) {
|
|||
.range([0, y.bandwidth()])
|
||||
.domain(["0", "1"])
|
||||
.paddingInner(0)
|
||||
.paddingOuter(0);
|
||||
.paddingOuter(0.1);
|
||||
|
||||
const keys = ["balance", "investment", "withdrawal", "gain", "loss"];
|
||||
const colors = ["#1f77b4", "#17becf", "#ff7f0e", "#b2df8a", "#fb9a99"];
|
||||
|
@ -153,7 +153,7 @@ function renderOverview(gains: Gain[]) {
|
|||
.text("XIRR")
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("x", xirrWidth / 2)
|
||||
.attr("y", height + 40);
|
||||
.attr("y", height + 30);
|
||||
|
||||
g.append("g")
|
||||
.attr("class", "axis y")
|
||||
|
@ -304,6 +304,7 @@ function renderOverview(gains: Gain[]) {
|
|||
.attr("height", y1.bandwidth())
|
||||
.attr("width", (d) => x(d[0][1]) - x(d[0][0]));
|
||||
|
||||
const paddingTop = (y1.range()[1] - y1.bandwidth() * 2) / 2;
|
||||
g.append("g")
|
||||
.selectAll("rect")
|
||||
.data(gains)
|
||||
|
@ -311,8 +312,8 @@ function renderOverview(gains: Gain[]) {
|
|||
.append("rect")
|
||||
.attr("fill", (g) => (g.xirr < 0 ? z("loss") : z("gain")))
|
||||
.attr("x", (g) => (g.xirr < 0 ? x1(g.xirr) : x1(0)))
|
||||
.attr("y", (g) => y(restName(g.account)))
|
||||
.attr("height", y.bandwidth())
|
||||
.attr("y", (g) => y(restName(g.account)) + paddingTop)
|
||||
.attr("height", y.bandwidth() - paddingTop * 2)
|
||||
.attr("width", (g) => Math.abs(x1(0) - x1(g.xirr)));
|
||||
|
||||
g.append("g")
|
||||
|
|
|
@ -48,6 +48,13 @@ export interface Aggregate {
|
|||
timestamp: dayjs.Dayjs;
|
||||
}
|
||||
|
||||
export interface AllocationTarget {
|
||||
name: string;
|
||||
target: number;
|
||||
current: number;
|
||||
aggregates: { [key: string]: Aggregate };
|
||||
}
|
||||
|
||||
export interface Income {
|
||||
date: string;
|
||||
postings: Posting[];
|
||||
|
@ -77,6 +84,7 @@ export function ajax(route: "/api/gain"): Promise<{
|
|||
export function ajax(route: "/api/allocation"): Promise<{
|
||||
aggregates: { [key: string]: Aggregate };
|
||||
aggregates_timeline: { [key: string]: Aggregate }[];
|
||||
allocation_targets: AllocationTarget[];
|
||||
}>;
|
||||
export function ajax(route: "/api/income"): Promise<{
|
||||
income_timeline: Income[];
|
||||
|
|
|
@ -179,6 +179,23 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section tab-allocation">
|
||||
<div class="container is-fluid">
|
||||
<div class="columns">
|
||||
<div class="column is-12 has-text-centered">
|
||||
<div id="d3-allocation-target-treemap" style="width: 100%; position: relative"></div>
|
||||
<svg id="d3-allocation-target" width="100%"></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column is-12 has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Allocation Targets</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section tab-allocation">
|
||||
<div class="container is-fluid">
|
||||
<div class="columns">
|
||||
|
|
|
@ -12,7 +12,7 @@ body {
|
|||
}
|
||||
|
||||
.inline-text text {
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue