Last active
November 19, 2024 14:32
-
-
Save Leon2xiaowu/2c05e77ce66f1c7e9a77c5f87c92f2fa to your computer and use it in GitHub Desktop.
fava_dashboards Sankey diagram configuration optimization, handle the display of losing investments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
- 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