---
title: "Savings"
order: 1
author:
- name: "Declan Naughton ๐งฎ๐จโ๐ป๐ง"
url: "https://calcwithdec.dev/about.html"
description: "Savings"
format:
html:
resources:
- '../../models/savings/*.js'
- '../../models/savings/*.js.map'
- '../../models/savings/*.json'
- '../../models/savings/savings-nomemo_esm/dist/*.js*'
---
See also: https://calculang-talk-belfastjs.pages.dev/apps/savings
# Savings
```{ojs}
viewof duration_in = Inputs.range([0,15],{step:1,value:5,label:'duration_in'})
viewof interest_rate_in = Inputs.range([0,0.1],{step:0.005,value:0.02,label:'interest_rate_in'})
viewof one_off_payment_in = Inputs.range([0,10000],{step:100,value:0,label:'one_off_payment_in'})
viewof annual_payment_in = Inputs.range([0,10000],{step:100,value:1000,label:'annual_payment_in'})
```
```{ojs}
input_cursors = [{duration_in,interest_rate_in,one_off_payment_in,annual_payment_in}];
viewof grid = embed(
calcuvizspec({
models: [main],
input_cursors,
mark: 'text',
encodings: {
x: {name: 'year_in', type: 'nominal', domain: _.range(-1,duration_in+0+.1)},
text: {name: 'value', type: 'quantitative', format:',.2f'},
y: {name: 'formula', type:'nominal', domain: formulae_not_inputs},
color: {name: 'formula', type:'nominal', domain: formulae_not_inputs, legend: false},
},
width: 500, height:100
}))
```
<!-- code viewer -->
<details><summary>*calculang formulae* ๐ช</summary>
<small>*highlight or click a number in visual to see it's calculang formula below:*</small>
```{ojs}
//| echo: false
// todo here: pull introspection-no-memo, use entrypoint.cul.js, publish as a helper? -> test in calcuvizspec
// ALSO will formula UI sit inside/outside? Inside is better, how to maintain interactivity?
code_viewer = async (entrypoint, formula) => {
const cul_fetch = await fetch(`../../${entrypoint_no_cul_js}${1 ? '-nomemo_esm' : '_esm'}/cul_scope_${0}.${code_opt_fv.indexOf('source') != -1 ? 'cul.js' : 'mjs'}`)
const cul = await cul_fetch.text()
return md`
~~~js
${
formulae_objs.filter(f => f.name == formula_select).map(f => cul.split('\n').filter((d,i) => i >= f.loc.start.line-1 && i < f.loc.end.line).join('\n').slice(13)).join('\n\n')
}
~~~
`
}
code_viewer('',formula_select)
```
<details><summary>*and the values used:*</summary>
```{ojs}
//| echo: false
Inputs.table(WORKINGS_NEW3.map(({formula, year_in, ...d}) => ({ callsite: invocs2.find(e => e.invocationId == (invocs2.find(f => f.invocationId == d.invocationId).parents[0].invocationId)).nodeId, formula, year_in, value:d.returnValue.value})), {sort: 'callsite'})
```
<br/>
<br/>
<br/>
<br/>
<br/>
---
```{ojs}
//| echo: false
// code viewer
formulae_objs = Object.values(introspection_nomemo.cul_functions).filter(d => d.reason == 'definition' && inputs.indexOf(d.name+'_in') == -1)
viewof code_opt_fv = Inputs.select(["๐ calculang source ๐ฌ", "โจ calculang output โจ for ๐ฅ๏ธ"], {label: "๐ง via ๐ฒs", value: "๐ calculang source ๐ฌ"})
viewof formula_select = Inputs.select(/*Object.entries(introspection_nomemo.cul_functions).filter(([a,b]) => b.reason == 'definition' && b.cul_source_scope_id==0).map(([a,b]) => b.name)*/formulae_not_inputs, {label: "formula"})
// force nomemo here
cul_fetch = await fetch(`../../${entrypoint_no_cul_js}${1 ? '-nomemo_esm' : '_esm'}/cul_scope_${0}.${code_opt_fv.indexOf('source') != -1 ? 'cul.js' : 'mjs'}`)
cul = cul_fetch.text()
//cs = (code_opt_fv.indexOf("source") != -1 ? calcuin_fv : calcuout).split('\n')
```
```{ojs}
//| echo: false
grid.addEventListener('mousemove', (event, item) => {
viewof formula_select.value = item.datum.formula;
viewof year_in.value = item.datum.year_in;
viewof year_in.dispatchEvent(new CustomEvent("input"))
viewof formula_select.dispatchEvent(new CustomEvent("input"))
})
```
</details>
</details>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
---
tofix: log gets appended on dupe mousemoves, use signal listeners instead?
navigation?
```{ojs}
viewof year_in = Inputs.range([-1,7],{step:1,value:5,label:'year_in'})
nodes = main_fondued.__tracer.nodes().filter(d => d.type == 'function' && d.name != 'get')
nodes2 = main_fondued.__tracer.nodes().filter(d => (d.type == 'function' || d.type == 'callsite') && d.name != 'get') // for mapping to callsites
invocs = { return main_fondued.__tracer.logDelta(main_fondued.__tracer.trackLogs({ ids: nodes.map(d => d.id)}), 200000 ) ; year_in }
invocs2 = { return main_fondued.__tracer.logDelta(main_fondued.__tracer.trackLogs({ ids: nodes2.map(d => d.id)}), 200000 ) ; year_in }
// where fonuded model is called
// TODO why doesn't this update with param changes?
main_fondued[formula_select]({/*...input_cursors[0]*/duration_in,interest_rate_in,one_off_payment_in,annual_payment_in, year_in}) // hardcodes! replace with
// https://observablehq.com/d/8a3610a164139cef
// lots of hardcodes, replaced with year_in
relationships = [...invocs.filter(d => d.parents).map(d => ({d, parent:invocs.filter(e => e.invocationId==d.parents[0].invocationId)[0]})),
...invocs.filter(d => !d.parents).map(d => ({d, parent:'no parent'}))]
relationships2 = relationships.map(d => ({...d.d, year_in: d.d.arguments[0].value.ownProperties.year_in ? d.d.arguments[0].value.ownProperties.year_in.value : undefined, parent_formula: d.parent.formula, parent_year_in:d.parent.arguments ? d.parent.arguments[0].value.ownProperties.year_in.value : undefined, parent_value: d.parent != 'no parent' ? d.parent.returnValue.value : undefined, parent:d.parent}))
relationships3 = _.uniqBy(relationships2, d => d.parent_formula+d.parent_year_in+d.formula+d.arguments)
WORKINGS_NEW3 = relationships3.filter(d => d.parent != 'no parent').filter(d => d.parent.type='callsite').filter(d => d.parent.formula == formula_select && d.parent.arguments[0].value.ownProperties.year_in.value == year_in)
w3 = WORKINGS_NEW3.map(d => d.returnValue.value)
```
<!-- end code viewer -->
---
<br/>
<br/>
<br/>
<br/>
# Appendix
```{ojs}
import { calcuvizspec } from "@declann/little-calcu-helpers"
embed = require('vega-embed');
viewof entrypoint = Inputs.select(['models/savings/savings.cul.js'], {label:'entrypoint'})
entrypoint_no_cul_js = entrypoint.slice(0,-7)
main = require(`../../${entrypoint_no_cul_js}.js`);
main_fondued = require(`../../${entrypoint_no_cul_js}-nomemo_esm/dist/cul_scope_0-babeled-fondued.bundle.js`);
introspection_fetch = await fetch(`../../${entrypoint_no_cul_js}.introspection.json`)
introspection = introspection_fetch.json({typed:true})
introspection_nomemo_fetch = await fetch(`../../${entrypoint_no_cul_js}-nomemo.introspection.json`)
introspection_nomemo = introspection_nomemo_fetch.json({typed:true})
inputs = Object.values(introspection.cul_functions).filter(d => d.reason == 'input definition').map(d => d.name).sort()
formulae = Object.values(introspection.cul_functions).filter(d => d.reason == 'definition').map(d => d.name)
// formulae excluding pure inputs
formulae_not_inputs = Object.values(introspection.cul_functions).filter(d => d.reason == 'definition' && inputs.indexOf(d.name+'_in') == -1).map(d => d.name)
```