add gain overview chart

This commit is contained in:
Anantha Kumaran 2022-07-03 13:08:22 +05:30
parent 1c6b0a4186
commit 40e9edd78a
4 changed files with 210 additions and 33 deletions

View File

@ -8,7 +8,10 @@ import {
formatCurrencyCrude,
formatFloat,
Gain,
Overview
Overview,
tooltip,
skipTicks,
restName
} from "./utils";
export default async function () {
@ -18,41 +21,13 @@ export default async function () {
);
renderLegend();
const start = _.min(
_.flatMap(gains, (g) => _.map(g.overview_timeline, (o) => o.timestamp))
),
end = dayjs();
const divs = d3
.select("#d3-gain-timeline-breakdown")
.selectAll("div")
.data(_.sortBy(gains, (g) => g.account));
divs.exit().remove();
const columns = divs.enter().append("div").attr("class", "columns");
const leftColumn = columns.append("div").attr("class", "column is-2");
leftColumn
.append("table")
.attr("class", "table is-narrow is-fullwidth is-size-7")
.append("tbody")
.each(renderTable);
const rightColumn = columns.append("div").attr("class", "column is-10");
rightColumn
.append("svg")
.attr("width", "100%")
.attr("height", "150")
.each(function (gain) {
renderOverviewSmall(gain.overview_timeline, this, [start, end]);
});
renderOverview(gains);
renderPerAccountOverview(gains);
}
const areaKeys = ["gain", "loss"];
const colors = ["#b2df8a", "#fb9a99"];
const areaScale = d3.scaleOrdinal().domain(areaKeys).range(colors);
const areaScale = d3.scaleOrdinal<string>().domain(areaKeys).range(colors);
const lineKeys = ["balance", "investment", "withdrawal"];
const lineScale = d3
.scaleOrdinal<string>()
@ -94,6 +69,187 @@ function renderTable(gain: Gain) {
});
}
function renderOverview(gains: Gain[]) {
gains = _.sortBy(gains, (g) => g.account);
const BAR_HEIGHT = 16;
const id = "#d3-gain-overview";
const svg = d3.select(id),
margin = { top: 40, right: 30, bottom: 80, left: 150 },
width =
document.getElementById(id.substring(1)).parentElement.clientWidth -
margin.left -
margin.right,
height = gains.length * BAR_HEIGHT,
g = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.attr("height", height + margin.top + margin.bottom);
const y = d3.scaleBand().range([0, height]).paddingInner(0.1).paddingOuter(0);
y.domain(gains.map((g) => restName(g.account)));
const getInvestmentAmount = (g: Gain) =>
_.last(g.overview_timeline).investment_amount;
const getGainAmount = (g: Gain) => _.last(g.overview_timeline).gain_amount;
const maxInvestment = _.chain(gains).map(getInvestmentAmount).max().value();
const maxGain = _.chain(gains).map(getGainAmount).max().value();
const maxLoss = _.min([_.chain(gains).map(getGainAmount).min().value(), 0]);
const maxX = maxInvestment + maxGain + Math.abs(maxLoss);
const x = d3.scaleLinear().range([0, width]);
x.domain([0, maxX]);
const x1 = d3
.scaleLinear()
.range([0, x(maxInvestment)])
.domain([0, maxInvestment]);
const x2 = d3
.scaleLinear()
.range([x(maxInvestment), width])
.domain([maxLoss, maxGain]);
g.append("g")
.attr("class", "axis y")
.attr("transform", "translate(0," + height + ")")
.call(
d3
.axisBottom(x1)
.tickSize(-height)
.tickFormat(
skipTicks(
50,
x(maxInvestment),
x1.ticks().length,
formatCurrencyCrude
)
)
);
g.append("g")
.attr("class", "axis y")
.attr("transform", "translate(0," + height + ")")
.call(
d3
.axisBottom(x2)
.tickSize(-height)
.tickFormat(
skipTicks(
50,
width - x(maxInvestment),
x2.ticks().length,
formatCurrencyCrude
)
)
);
g.append("g").attr("class", "axis y dark").call(d3.axisLeft(y));
g.append("g")
.selectAll("rect")
.data(gains)
.enter()
.append("rect")
.attr("fill", lineScale("investment"))
.attr("x", x(0))
.attr("y", (g) => y(restName(g.account)))
.attr("height", y.bandwidth())
.attr("width", (g) => x(getInvestmentAmount(g)));
g.append("g")
.selectAll("rect")
.data(gains)
.enter()
.append("rect")
.attr("fill", (g) =>
getGainAmount(g) < 0 ? areaScale("loss") : areaScale("gain")
)
.attr("x", (g) => (getGainAmount(g) < 0 ? x2(getGainAmount(g)) : x2(0)))
.attr("y", (g) => y(restName(g.account)))
.attr("height", y.bandwidth())
.attr("width", (g) => x(Math.abs(getGainAmount(g))));
g.append("g")
.selectAll("rect")
.data(gains)
.enter()
.append("rect")
.attr("fill", "transparent")
.attr("data-tippy-content", (g: Gain) => {
const current = _.last(g.overview_timeline);
return tooltip([
["Account", [g.account, "has-text-weight-bold has-text-right"]],
[
"Investment",
[
formatCurrency(current.investment_amount),
"has-text-weight-bold has-text-right"
]
],
[
"Withdrawal",
[
formatCurrency(current.withdrawal_amount),
"has-text-weight-bold has-text-right"
]
],
[
"Gain",
[
formatCurrency(current.gain_amount),
"has-text-weight-bold has-text-right"
]
],
[
"Balance",
[
formatCurrency(
current.investment_amount +
current.gain_amount -
current.withdrawal_amount
),
"has-text-weight-bold has-text-right"
]
],
["XIRR", [formatFloat(g.xirr), "has-text-weight-bold has-text-right"]]
]);
})
.attr("x", x(0))
.attr("y", (g) => y(restName(g.account)))
.attr("height", y.bandwidth())
.attr("width", x(maxX));
}
function renderPerAccountOverview(gains: Gain[]) {
const start = _.min(
_.flatMap(gains, (g) => _.map(g.overview_timeline, (o) => o.timestamp))
),
end = dayjs();
const divs = d3
.select("#d3-gain-timeline-breakdown")
.selectAll("div")
.data(_.sortBy(gains, (g) => g.account));
divs.exit().remove();
const columns = divs.enter().append("div").attr("class", "columns");
const leftColumn = columns.append("div").attr("class", "column is-2");
leftColumn
.append("table")
.attr("class", "table is-narrow is-fullwidth is-size-7")
.append("tbody")
.each(renderTable);
const rightColumn = columns.append("div").attr("class", "column is-10");
rightColumn
.append("svg")
.attr("width", "100%")
.attr("height", "150")
.each(function (gain) {
renderOverviewSmall(gain.overview_timeline, this, [start, end]);
});
}
function renderOverviewSmall(
points: Overview[],
element: Element,

View File

@ -1,5 +1,4 @@
import * as d3 from "d3";
import { group } from "d3";
import legend from "d3-svg-legend";
import dayjs from "dayjs";
import _ from "lodash";

View File

@ -122,6 +122,22 @@
</div>
</div>
</div>
<div class="container is-fluid">
<div class="columns">
<div class="column is-12">
<svg id="d3-gain-overview" width="100%"></svg>
</div>
</div>
<div class="columns">
<div class="column is-12 has-text-centered">
<div>
<p class="heading">Gain Overview</p>
</div>
</div>
</div>
</div>
</section>
<section class="section tab-gain">
<div class="container is-fluid d3-gain-timeline-breakdown">
<div class="columns">
<div id="d3-gain-timeline-breakdown" class="column is-12">

View File

@ -10,6 +10,12 @@ body {
font-size: 10px;
fill: #aaa;
}
.axis.dark text {
font-size: 12px;
fill: #666;
}
.axis path.domain, .axis line {
fill: none;
stroke: #ccc;