add gain overview chart
This commit is contained in:
parent
1c6b0a4186
commit
40e9edd78a
220
web/src/gain.ts
220
web/src/gain.ts
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue