class CashFlow {
constructor(data, {label = "", domain = [], truncate = false , fromCum = false } = {}) {
this.label = label;
const getDate = d => d.date;
const getValue = d => d.value;
if(!domain.length && Array.isArray(data)) domain = [
d3.min(data, getDate),
d3.utcMonth.offset(d3.max(data, getDate), 1)
]
this.domain = domain.map(d3.utcMonth.floor);
this.index = d3.utcMonth.range(...this.domain);
if (typeof data === "function") {
this.diff = this.index
.map((date, i) => ({date, value: data(date, i)}));
} else if (Array.isArray(data)) {
const rollup = d3.rollup(
data,
arr => d3.sum(arr, getValue),
({date}) => d3.utcMonth.floor(date)
);
this.diff = this.index
.map(date => ({date, value: rollup.get(date) || 0}));
} else if (typeof data === "number") {
this.diff = this.index.map(date => ({date, value: data}));
} else {
throw new Error(`Invalid flow data. Please provide one of the following:
- a number (constant monthly flow)
- an array of {date, value} objects
- a function that takes a month and returns a {date, value} object`);
}
// constructor expects series of differences;
// this rolls them up into cumulative sum
this.cum = Array.from(d3.cumsum(this.diff, getValue),
(value, i) => ({date: this.index[i], value}));
}
static sum(flows) {
const data = Array.from(
d3.rollup(
flows.flatMap(d => [...d]),
values => ({
date: values[0].date,
value: d3.sum(values, d => d.value)
}),
d => +d.date
).values());
return new CashFlow(data, {
label: flows.map(flow => flow.label).join(" + ")
});
}
static plotFlow(data, label, options = {}) {
return Plot.plot({
height: 200,
x: {tickFormat: dateFormat, label: null},
y: {tickFormat: d3.format("$"), label, transform: d => d / 1000},
color: {
type: "diverging",
scheme: "RdBu",
domain: [-1e-6, 1e-6]
},
marks: [
Plot.barY(data, {x: "date", y: "value", fill: "value"}),
Plot.ruleY([0])
],
...options
})
}
plot(options) {
return this.plotDiff(options)
}
plotCum(options) {
return CashFlow.plotFlow(this.cum, `Cumulative ${this.label || "flow"} (k)`, options)
}
plotDiff(options) {
return CashFlow.plotFlow(this.diff, `${this.label || "Flow"} / mo. (k)`, options)
}
npv(rate) {
return xnpv(rate, this.diff.map(d => d.value), this.diff.map(d => d.date))
}
[Symbol.iterator]() {
return this.diff[Symbol.iterator]()
};
}