Skip to content

Instantly share code, notes, and snippets.

@Leon2xiaowu
Last active November 19, 2024 14:32
Show Gist options
  • Save Leon2xiaowu/2c05e77ce66f1c7e9a77c5f87c92f2fa to your computer and use it in GitHub Desktop.
Save Leon2xiaowu/2c05e77ce66f1c7e9a77c5f87c92f2fa to your computer and use it in GitHub Desktop.
fava_dashboards Sankey diagram configuration optimization, handle the display of losing investments
- name: IncomeStatementSankey
panels:
- title: IncomeStatementSankey 💸
height: 800px
link: /mainbook/income_statement/
queries:
- bql: |
SELECT account, CONVERT(SUM(position), '{{ledger.ccy}}') AS value
WHERE account ~ '^(Income|Expenses):'
GROUP BY account
link: /mainbook/account/{account}/
type: d3_sankey
script: |
const currencyFormatter = utils.currencyFormatter(ledger.ccy);
// const days = (new Date(ledger.dateLast) - new Date(ledger.dateFirst)) / (1000 * 60 * 60 * 24) + 1;
// const divisor = days / (365 / 12); // monthly
const divisor = 1; // Adjust this if you want to calculate the mean
const valueThreshold = 10; // skip nodes below this value
let totalIncome = 0;
const nodes = [{ name: "Income" }];
const links = [];
function addNode(root) {
for (let node of root.children) {
let label = node.name.split(":").pop();
// skip over pass-through accounts
while (node.children.length === 1) {
node = node.children[0];
label += ":" + node.name.split(":").pop();
}
// skip nodes below the threshold
if (Math.abs(node.value / divisor) < valueThreshold) continue;
console.log(node.name, root.name)
// append the percentage of expenditure shown
label += `(${((Math.abs(node.value) / Math.abs(totalIncome)) * 100).toFixed(2)}%)`;
nodes.push({ name: node.name, label });
// Losses are considered expenses
if (node.name.startsWith("Income:") && node.value < 0) {
links.push({ source: node.name, target: root.name, value: -node.value / divisor });
} else {
links.push({
source: root.name == "Expenses" ? "Income" : root.name,
target: node.name,
value: node.value / divisor,
});
}
addNode(node);
}
}
const accountTree = utils.buildAccountTree(panel.queries[0].result, (row) => row.value[ledger.ccy] ?? 0);
if (accountTree.children.length !== 2) {
throw Error("No Income/Expense accounts found.");
}
const [first, second] = accountTree.children;
let [incomeTree, expensesTree] = first.name === "Income" ? [first, second] : [second, first];
// Filter out investment losses from income
const {updatedData, positiveItems, positiveItemsLoss} = utils.filterLeafNodesAndUpdateParentValue(incomeTree)
incomeTree = updatedData;
totalIncome = incomeTree.value
const lossTree = {"name":"Expenses","children": positiveItems, "value": positiveItemsLoss}
// Revenue node
addNode(incomeTree);
// Expenditure node
addNode(expensesTree);
// Losses node
addNode(lossTree);
const savings = -incomeTree.value - expensesTree.value - positiveItemsLoss;
if (savings > 0) {
const savingValue = savings / divisor;
const savingLabel = `(${((Math.abs(savingValue) / Math.abs(totalIncome)) * 100).toFixed(2)}%)`;
nodes.push({ name: "Savings", label: `Saving ${savingLabel}` });
links.push({ source: "Income", target: "Savings", value: savingValue });
}
return {
align: "left",
valueFormatter: currencyFormatter,
data: {
nodes,
links,
},
onClick: (event, node) => {
if (node.name === "Savings") return;
const link = panel.queries[0].link.replace("{account}", node.name);
window.open(helpers.urlFor(link));
},
};
utils:
inline: |
function filterLeafNodesAndUpdateParentValue(data) {
// Stores all deleted leaf nodes
let positiveItems = [];
let positiveItemsLoss = 0;
function recursiveFilter(node) {
if (!node.children || node.children.length === 0) {
if (node.value > 0) {
positiveItems.push(node);
positiveItemsLoss += node.value;
return null;
} else {
return node;
}
}
let filteredChildren = [];
let totalValue = 0;
for (let child of node.children) {
const filteredChild = recursiveFilter(child);
if (filteredChild) {
filteredChildren.push(filteredChild);
totalValue += filteredChild.value;
}
}
node.children = filteredChildren;
node.value = totalValue;
return node;
}
const updatedData = recursiveFilter(data);
return {
positiveItemsLoss,
positiveItems,
updatedData
};
}
return {
// other functions
filterLeafNodesAndUpdateParentValue
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment