Skip to content

Commit

Permalink
coin2html: add balances chart
Browse files Browse the repository at this point in the history
  • Loading branch information
mkobetic committed Jan 2, 2025
1 parent a49534b commit 5fa5029
Show file tree
Hide file tree
Showing 5 changed files with 50,573 additions and 32,999 deletions.
1 change: 1 addition & 0 deletions cmd/coin2html/js/body.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ <h1>
<script src="src/utils.ts"></script>
<script src="src/views.ts"></script>
<script src="src/viewsBalances.ts"></script>
<script src="src/viewsBalancesChart.ts"></script>
<script src="src/viewsRegister.ts"></script>
<script src="src/viewsAggregatedRegisterChart.ts"></script>
<script src="src/ui.ts"></script>
20 changes: 14 additions & 6 deletions cmd/coin2html/js/src/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import {
renderPostingsWithSubAccounts,
viewRegister,
} from "./viewsRegister";
import { viewChartTotals } from "./viewsAggregatedRegisterChart";
import { viewAggregatedRegisterChart } from "./viewsAggregatedRegisterChart";
import { Account } from "./account";
import { PostingGroup, shortenAccountName, topN } from "./utils";
import { viewBalances } from "./viewsBalances";
import { viewBalancesChart } from "./viewsBalancesChart";

export const Aggregation = {
None: null,
Expand Down Expand Up @@ -45,18 +46,22 @@ export const State = {
},
};

// View types by account category.
// Available view types by account category.
// These define what is offered in the view drop-down.
// All types have Register.
export const Views = {
Assets: {
Balances: viewBalances,
Register: viewRegister,
Chart: viewChartTotals,
ChartBalances: viewBalancesChart,
ChartAggregatedRegister: viewAggregatedRegisterChart,
},
Liabilities: {
Balances: viewBalances,
Register: () => viewRegister({ negated: true }),
Chart: () => viewChartTotals({ negated: true }),
ChartBalances: () => viewBalancesChart({ negated: true }),
ChartAggregatedRegister: () =>
viewAggregatedRegisterChart({ negated: true }),
},
Income: {
Balances: viewBalances,
Expand All @@ -65,15 +70,18 @@ export const Views = {
negated: true,
aggregatedTotal: true,
}),
Chart: () => viewChartTotals({ negated: true }),
ChartBalances: () => viewBalancesChart({ negated: true }),
ChartAggregatedRegister: () =>
viewAggregatedRegisterChart({ negated: true }),
},
Expenses: {
Balances: viewBalances,
Register: () =>
viewRegister({
aggregatedTotal: true,
}),
Chart: viewChartTotals,
ChartBalances: viewBalancesChart,
ChartAggregatedRegister: viewAggregatedRegisterChart,
},
Equity: {
Balances: viewBalances,
Expand Down
2 changes: 1 addition & 1 deletion cmd/coin2html/js/src/viewsAggregatedRegisterChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { scaleLinear, scaleOrdinal, scaleTime } from "d3-scale";
import { schemeCategory10 } from "d3-scale-chromatic";
import { select } from "d3-selection";

export function viewChartTotals(options?: {
export function viewAggregatedRegisterChart(options?: {
negated?: boolean; // is this negatively denominated account (e.g. Income/Liability)
}) {
const containerSelector = MainView;
Expand Down
104 changes: 104 additions & 0 deletions cmd/coin2html/js/src/viewsBalancesChart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { stratify, treemap } from "d3-hierarchy";
import { select } from "d3-selection";
import { group } from "d3-array";
import { addBalanceDepthInput, emptyElement, MainView, State } from "./views";
import { scaleOrdinal } from "d3-scale";
import { schemePastel1 } from "d3-scale-chromatic";
import { AccountBalanceAndTotal } from "./utils";

// based on https://observablehq.com/@d3/nested-treemap
export function viewBalancesChart(options?: {
negated?: boolean; // is this negatively denominated account (e.g. Income/Liability)
}) {
const containerSelector = MainView;
const account = State.SelectedAccount;
const opts = { negated: false }; // defaults
Object.assign(opts, options);
emptyElement(containerSelector);
addBalanceDepthInput(containerSelector);
let root = stratify<AccountBalanceAndTotal>()
.id(({ account }) => account.fullName)
.parentId(({ account }) =>
account.parent && account.parent != State.SelectedAccount.parent
? account.parent.fullName
: undefined
)(account.withAllChildBalances(State.EndDate));
root = root.sum((a) => a.balance.toNumber());

const [width, height] = [1200, 800];
const tm = treemap<AccountBalanceAndTotal>()
.size([width, height])
.padding(4)
.paddingTop(20);
const nodes = tm(root);

let uidCounter = 0;

const svg = select(containerSelector)
.append("svg")
.attr("id", "chart")
.attr("width", "100%")
.attr("height", height);
// .attr("viewBox", [0, 0, width, height]);
// .attr(
// "style",
// "max-width: 100%; height: auto; overflow: visible; font: 10px sans-serif;"
// );

const color = scaleOrdinal([0, 10], schemePastel1);

const node = svg
.selectAll("g")
.data(group(nodes, (d) => d.height))
.join("g")
.selectAll("g")
.data((d) => d[1])
.join("g")
.attr("transform", (d) => `translate(${d.x0},${d.y0})`);

node
.append("title")
.text(({ data }) =>
data.account.children.length > 0
? `${data.account.fullName} ${data.total} [ ${data.balance} ]`
: `${data.account.fullName} ${data.balance}`
);

node
.append("rect")
.attr("id", (d: any) => (d.nodeUid = `node-${uidCounter++}`))
.attr("fill", (d) => color(d.height))
.attr("width", (d) => d.x1 - d.x0)
.attr("height", (d) => d.y1 - d.y0);

node
.append("clipPath")
.attr("id", (d: any) => (d.clipUid = `clip-${uidCounter++}`))
.append("use")
.attr("xlink:href", (d: any) => `#${d.nodeUid}`);

node
.append("text")
.attr("clip-path", (d: any) => `url(#${d.clipUid})`)
.selectAll("tspan")
.data(({ data }) => {
const bits = [data.account.name, data.total.toString()];
if (data.account.children.length > 0) bits.push(data.balance.toString());
return bits;
})
.join("tspan")
.text((d) => d);

const narrowBoxLimit = 150;
node
.filter((d: any) => d.x1 - d.x0 > narrowBoxLimit)
.selectAll("tspan")
.attr("dx", 10)
.attr("y", 15);

node
.filter((d) => d.x1 - d.x0 <= narrowBoxLimit)
.selectAll("tspan")
.attr("x", 10)
.attr("dy", 15);
}
Loading

0 comments on commit 5fa5029

Please sign in to comment.