โcalcuvizspecโ by example
โ ๏ธ Disclaimers
The assumptions, methodology and limitations of a model should be carefully considered for any purpose you apply it to. I havenโt even listed these here, nor otherwise documented the model fully/clearly. Proceed with caution ๐
Models are under construction ๐ง
Privacy policy: This page does not currently use analytics or cookies (any). It does use external resources from Content Delivery Networks (mainly jsDelivr, Cloudflare, Unpkg) and ObservableHQ; these services may obtain IP addresses associated with requests.
Notwithstanding the above, it is not recommended to enter real personal or sensitive details. Contact me with real-world calculation or model requirements instead. ๐ฃโ
SaaS cashflows
Tip
In this presentation you can find all calculang model source code by opening Developer Tools (Ctrl+Shift+I) and navigating to the .cul.js
files (Ctrl+P and search .cul.js
).
Super-simplified; modelling four cashflows:
- Monthly Recurring Revenue (MRR) ~ mrr_in, mrr_growth_in
- Payroll ~ salary_per_employee_in, employees_0_in, new_employees_per_month_in
- Rent (fixed) ~ rent_in
- Venture capital (fixed amounts in month 3 and 11) ~ vc_1_in, vc_2_in
inputs โ๏ธ
Code
= Inputs.range([0, 500000], {label: "monthly revenue (mrr)", step: 5000, value:155000});
viewof mrr_in = Inputs.range([-0.3, .8], {label: "mrr growth factor (monthly!)", step: 0.01, value:0.15});
viewof mrr_growth_in = Inputs.range([0, 1e7], {label: "venture capital R1 (vc)", step: 100000, value: 1000000 });
viewof vc_1_in = Inputs.range([0, 1e7], {label: "venture capital R2 (vc)", step: 100000, value: 2000000 });
viewof vc_2_in = Inputs.range([0, 30000], {label: "salary/employee", step: 1000});
viewof salary_per_employee_in = Inputs.range([0, 50], {label: "employees @ start", step: 1, value:26});
viewof employees_0_in = Inputs.range([0, 10], {label: "new employees per mth", step: 1, value:2});
viewof new_employees_per_month_in = Inputs.range([0, 300000], {label: "rent", step: 10000, value:200000});
viewof rent_in = Inputs.range([0, 36], {label: "last month (0-index)", step: 1, value:17});
viewof last_month_in = Inputs.range([-0.1, .2], {label: "npv interest rate (monthly!)", step: 0.001, value:0.0}); viewof npv_i_in
Code
Code
embed(
calcuvizspec({
models: [cfs],
input_cursors: [{mrr_in,mrr_growth_in,vc_1_in,vc_2_in,salary_per_employee_in,employees_0_in,new_employees_per_month_in,rent_in,last_month_in}],
mark: 'bar',
encodings: {
x: {name: 'month_in', type: 'nominal', domain: _.range(-1,last_month_in+0.1, 1)},
y: {name: 'value', type: 'quantitative'},
color: {name: 'formula', type:'nominal', domain: ['mrr_cf','rent_cf','payroll_cf','vc_cf']},
,
}width: 500, height:270
, {renderer: 'svg'}) })
Code
embed(
calcuvizspec({
models: [cfs],
input_cursors: [{npv_i_in,mrr_in,mrr_growth_in,vc_1_in,vc_2_in,salary_per_employee_in,employees_0_in,new_employees_per_month_in,rent_in,last_month_in}],
mark: 'text',
encodings: {
x: {name: 'formula', type:'nominal', domain: ['employees','mrr_cf','rent_cf','npv','payroll_cf','total_cf','vc_cf']},
y: {name: 'month_in', type: 'nominal', domain: _.range(-1,last_month_in+0.1, 1)},
text: {name: 'value', type: 'quantitative', format:',.2f'},
color: {name: 'formula', type:'nominal', domain: ['employees','mrr_cf','rent_cf','npv','payroll_cf','total_cf','vc_cf'], legend:false},
,
}width: 500, height:270
, {renderer: 'svg'}) })
Code
embed(
calcuvizspec({
models: [cfs],
input_cursors: [{npv_i_in,mrr_in,mrr_growth_in,vc_1_in,vc_2_in,salary_per_employee_in,employees_0_in,new_employees_per_month_in,rent_in,last_month_in}],
mark: 'bar',
encodings: {
x: {name: 'month_in', type: 'nominal', domain: _.range(-1,last_month_in+0.1, 1)},
y: {name: 'value', type: 'quantitative', independent:true},
row: {name: 'formula', type:'nominal', domain: ['employees','mrr_cf','rent_cf','payroll_cf','total_cf','vc_cf']},
color: {name: 'formula', type:'nominal', domain: ['employees','mrr_cf','rent_cf','payroll_cf','total_cf','vc_cf'],legend:false},
,
}width: 500, height:30
, {renderer: 'svg'}) })
Code
embed(
calcuvizspec({
models: [cfs],
input_cursors: [{npv_i_in,mrr_in,mrr_growth_in,vc_1_in,vc_2_in,salary_per_employee_in,employees_0_in,new_employees_per_month_in,rent_in,last_month_in}],
mark: {type: 'line', point: true},
encodings: {
x: {name: 'month_in', type: 'nominal', domain: _.range(-1,last_month_in+0.1, 1)},
y: {name: 'value', type: 'quantitative', independent:true},
row: {name: 'formula', type:'nominal', domain: ['employees','mrr_cf','rent_cf','payroll_cf','total_cf','vc_cf']},
color: {name: 'formula', type:'nominal', domain: ['employees','mrr_cf','rent_cf','payroll_cf','total_cf','vc_cf'],legend:false},
,
}width: 500, height:30
, {renderer: 'svg'}) })
Code
Pension calculator
๐ง ๐ง๐ด
Calcs very Ireland specific ๐ฎ๐ช
inputs โ๏ธ (MODIFY to see a history!)
Code
= {
viewof form let form = Inputs.form({
age_0_in: Inputs.range([18,65], {label:'starting age', value:30, step:1}),
fund_value_0_in: Inputs.range([0,1000000], {label:'starting fund value', value:0, step:1000}),
unit_growth_rate_in: Inputs.range([-0.02,0.10], {label:'investment growth rate', value:0.05, step:0.01}),
retirement_age_in: Inputs.range([50,75], {label:'retirement age', value:65, step:1}),
salary_0_in: Inputs.range([5000,300000], {label:'starting salary', value:50000, step:1000}),
salary_inflation_rate_in: Inputs.range([-0.02,0.10], {label:'salary growth rate', value:0.02, step:0.01}),
empee_contribution_rate_in: Inputs.range([0.00,0.30], {label:'empee contribution as % of salary', value:0.1, step:0.01}),
emper_contribution_rate_in: Inputs.range([0.00,0.30], {label:'emper contribution as % of salary', value:0.1, step:0.01}),
contribution_charge_in: Inputs.range([-0.02,0.10], {label:'contribution charge', value:0.04, step:0.01})
;
})
let state = false;
.oninput = () => {console.log('oninput');if (state == false) {mutable inputs_history = [...mutable inputs_history, form.value]; state = true;} mutable inputs_history = [...mutable inputs_history.slice(0,-1), form.value]}
form.onchange = () => { console.log('onchange');state = false; mutable inputs_history[mutable inputs_history.length-1] = form.value };
form
return form;
}
= [JSON.parse(`{"age_0_in":30,"fund_value_0_in":0,"unit_growth_rate_in":0.05,"retirement_age_in":65,"salary_0_in":50000,"salary_inflation_rate_in":0.02,"empee_contribution_rate_in":0.1,"emper_contribution_rate_in":0.1,"contribution_charge_in":0.04}`
mutable inputs_history )]
Code
embed(
calcuvizspec({
models: [pension_calculator],
input_cursors: inputs_history.map(d => ({...d, age_in:d.retirement_age_in /* TODO hacky? */})),
mark: {type: 'text', size: 20, fontWeight:'bold'},
encodings: {
y: {name: 'formula', type:'nominal', domain: ['projected_fund_value', 'accumulated_empee_contributions', 'accumulated_empee_contribution_tax_relief', 'salaries_per_projected_fund']},
x: {name: 'input_cursor_id', type: 'nominal', sort: 'descending'},
color: {name: 'input_cursor_id', type: 'nominal', domain: _.range(0,5,1) /* TODO ignored? */, legend:false},
text: {name: 'value', type: 'quantitative', format:',.2f'},
,
}width: 400, height:100
, {renderer: 'svg'}) })
Code
Code
Code
embed(
calcuvizspec({
models: [pension_calculator],
input_cursors: [form],
mark: 'text',
encodings: {
x: {name: 'formula', type:'nominal', domain: pension_calculator_formulae},
y: {name: 'age_in', type: 'nominal', domain: _.range(form.age_0_in-1,form.retirement_age_in+0.1, 1)},
text: {name: 'value', type: 'quantitative', format:',.2f'},
color: {name: 'formula', type:'nominal', domain: pension_calculator_formulae, legend:false},
,
}width: 700, height:270*2
, {renderer: 'svg'}) })
Code
embed(
calcuvizspec({
models: [pension_calculator],
input_cursors: [form],
mark: 'bar',
encodings: {
x: {name: 'age_in', type: 'nominal', domain: _.range(form.age_0_in-1,form.retirement_age_in+0.1, 1)},
y: {name: 'value', type: 'quantitative', independent:true},
row: {name: 'formula', type:'nominal', domain: pension_calculator_formulae},
color: {name: 'formula', type:'nominal', domain: pension_calculator_formulae,legend:false},
,
}width: 500, height:30
, {renderer: 'svg'}) })
Code
embed(
calcuvizspec({
models: [pension_calculator],
input_cursors: [form],
mark: {type: 'line', point: true},
encodings: {
x: {name: 'age_in', type: 'nominal', domain: _.range(form.age_0_in-1,form.retirement_age_in+0.1, 1)},
y: {name: 'value', type: 'quantitative', independent:true},
row: {name: 'formula', type:'nominal', domain: pension_calculator_formulae},
color: {name: 'formula', type:'nominal', domain: pension_calculator_formulae,legend:false},
,
}width: 500, height:30
, {renderer: 'svg'}) })
projected_fund_value:
Code
embed(
calcuvizspec({
models: [pension_calculator],
input_cursors: [form],
mark: 'rect', // rect for heatmap
encodings: {
y: {name: 'empee_contribution_rate_in', domain: _.range(0.05,0.20,small ? 0.015 : 0.005), type:'ordinal', format:'.1%', sort:'descending'},
x: {name: 'age_0_in', type: 'ordinal', domain: _.range(20,50,small ? 3 : 1)},
color: {name: 'projected_fund_value', type: 'quantitative', scale: {scheme:'turbo', reverse:true}},
,
}width: 600, height: 370/2
, {renderer: 'svg'}) })
salaries_per_projected_fund:
Code
embed(
calcuvizspec({
models: [pension_calculator],
input_cursors: [form], // note: using 0 fund value and emper contributions and starting salary (important error!)
mark: 'text',
encodings: {
y: {name: 'empee_contribution_rate_in', domain: _.range(0.05,0.20,small ? 0.015 : 0.005), type:'ordinal', format:'.1%', sort:'descending'},
x: {name: 'age_0_in', type: 'ordinal', domain: _.range(20,50,small ? 3 : 1)},
text: {name: 'salaries_per_projected_fund', type: 'quantitative', scale: {scheme:'turbo', reverse:true}, format:'.1f'},
color: {name: 'salaries_per_projected_fund', type: 'quantitative', scale: {scheme:'turbo', reverse:true}, format:'.1f', legend:false},
,
}width: 600, height: 370/2
, {renderer: 'svg'}) })
Code
Caution
Known salary growth error heatmap results
Code
For calculang formulae overlay see ObservableHQ (poss. ood)
Projectile
inputs โ๏ธ
Code
import {interval} from '@mootari/range-slider'
//| panel: input
= {
viewof form_projectile let form = Inputs.form({
angle_in: Inputs.range([0,3], {value: 1, step:0.1, label: "Angle" }),
power_in: Inputs.range([0,100], {value: 30, step:0.01, label: "Power" }),
g_in: Inputs.range([0,3], {value: 1, step:0.01, label: "Gravity factor" }),
drag_coefficient_in: Inputs.range([-0.1,0.1], {value: 0.01, step:0.001, label: "Drag coeff." }),
t_interval: interval([0,100], {step:1, label: 'time โ๏ธ', color: 'skyblue', value:[0,60]}),
;
})
let state = false;
.oninput = () => {console.log('oninput');if (state == false) {mutable inputs_history_projectile = [...mutable inputs_history_projectile, form.value]; state = true;} mutable inputs_history_projectile = [...mutable inputs_history_projectile.slice(0,-1), form.value]}
form.onchange = () => { console.log('onchange');state = false; mutable inputs_history_projectile[mutable inputs_history_projectile.length-1] = form.value };
form
return form;
}
= [JSON.parse(`{"angle_in":1,"power_in":30,"g_in":1,"drag_coefficient_in":0.01,"t_interval":[0,60]}`
mutable inputs_history_projectile )]
Code
Code
embed(
calcuvizspec({
models: [projectile],
input_cursors: inputs_history_projectile,
mark: 'point',
encodings: {
detail: {name: 't_in', type: 'quantitative', domain: _.range(form_projectile.t_interval[0],form_projectile.t_interval[1]+0.1,1)},
y: {name: 'y', type:'quantitative'},
x: {name: 'x', type: 'quantitative'},
color: {name: 'input_cursor_id', type: 'nominal'},
,
}width: 400, height: 220
, {renderer: 'svg'}) })
Code
= ['x','y','dx','dy','drag_x','drag_y']
projectile_formulae
embed(
calcuvizspec({
models: [projectile],
input_cursors: [form_projectile],
mark: 'text',
encodings: {
x: {name: 'formula', type:'nominal', domain: projectile_formulae},
y: {name: 't_in', type: 'nominal', domain: _.range(form_projectile.t_interval[0],form_projectile.t_interval[1]+0.1, 5)},
text: {name: 'value', type: 'quantitative', format:',.2f'},
color: {name: 'formula', type:'nominal', domain: projectile_formulae, legend:false},
,
}width: 500, height: 220
, {renderer: 'svg'}) })
Code
embed(
calcuvizspec({
models: [projectile],
input_cursors: [form_projectile],
mark: 'bar',
encodings: {
x: {name: 't_in', type: 'quantitative', domain: _.range(form_projectile.t_interval[0],form_projectile.t_interval[1]+0.1,1)},
y: {name: 'value', type: 'quantitative', independent:true},
row: {name: 'formula', type:'nominal', domain: projectile_formulae},
color: {name: 'formula', type:'nominal', domain: projectile_formulae,legend:false},
,
}width: 500, height:40
, {renderer: 'svg'}) })
Code
embed(
calcuvizspec({
models: [projectile],
input_cursors: inputs_history_projectile,
mark: {type: 'line', point: true, strokeWidth: 0.5},
encodings: {
x: {name: 't_in', type: 'quantitative', domain: _.range(form_projectile.t_interval[0],form_projectile.t_interval[1]+0.1,2)},
y: {name: 'value', type: 'quantitative', independent:true},
row: {name: 'formula', type:'nominal', domain: projectile_formulae},
color: {name: 'input_cursor_id', type: 'nominal'},
,
}width: 500, height:40
, {renderer: 'svg'}) })
Code
Loan
Code
Code
Correct?
Code
inputs โ๏ธ
Code
= Inputs.range([0, 500000], {label: "Principal ๐ฐ โฌ", step:10000, value:+(q.get('principal_in') ?? 100000)});
viewof principal_in
= Inputs.range([-5/100, 20/100], {label: "Interest rate", step: 0.1/100, value: +(q.get('i_in')??4/100)}); // neg interest is probably contrary and not tested well
viewof i_in
= Inputs.select([25, 10, 5, 30, 35, 40, 1, 480], {label: "Term (years)", value: +(q.get('term_in')??10)}); viewof term_in
Code
Code
= ['repayment_amount','v_pow_term_left','interest','capital_repayment','repayment_due','interest_repayment','repayment','balance']
simple_loan_formulae
embed(
calcuvizspec({
models: [simple_loan],
input_cursors: [params],
mark: 'text',
encodings: {
x: {name: 'formula', type:'nominal', domain: simple_loan_formulae},
y: {name: 'year_in', type: 'nominal', domain: _.range(-1,term_in+1.01,1)},
text: {name: 'value', type: 'quantitative', format:',.2f'},
color: {name: 'formula', type:'nominal', domain: simple_loan_formulae, legend:false},
,
}width: 500, height: 220
, {renderer: 'svg'}) })
Code
embed(
calcuvizspec({
models: [simple_loan],
input_cursors: [{principal_in,i_in,term_in}],
mark: 'bar',
encodings: {
x: {name: 'year_in', type: 'nominal', domain: _.range(-1,term_in+1.01,1)},
y: {name: 'value', type: 'quantitative', independent:true},
row: {name: 'formula', type:'nominal', domain: simple_loan_formulae},
color: {name: 'formula', type:'nominal', domain: simple_loan_formulae,legend:false},
,
}width: 500, height:40
, {renderer: 'svg'}) })
Code
In the wild ๐๐ฆ๐ณ
Examples published using calcuvizspec:
- Hearty maths โค๏ธ, my first blog post on CalcWithDec.dev
- Raycasting 3d worlds ๐งฑ๐ซ, a slightly-rushed competition entry for maths education (will improve and post to CalcWithDec)
- includes coordinated views
Mixed up examples:
- calcy-quarty-vizys | devtools
- My calculang Observable HQ collection
- mostly developed before time of calcuvizspec => mostly more custom, time-consuming and harder to develop!
- Arty examples Function from Copacabana ๐ถ๐๐๐ฉด, Function from Sรฃo Paulo ๐๐จ (not using calcuvizspec)
calculang is not opinionated
Important
calculang is a language for calculations. It is not opinionated about how calculations are used.
Iโve called calculang models from Python and Julia; it is feasible to integrate into any programming language or system.
โcalcuvizspecโ is a tool in the development process for calculang models. It can also be useful for publishing. But calculang doesnโt tie you to any tool for development or for publishing.
calculang.party
A short-term goal is to publish a community gallery of calculang models and visualizations.
Feel free to talk to me if you want to publish a model! ๐
My Youtube Channel, @CalcWithDec includes early videos of developer experience; more to come there!
Connect
Twitter @calculang
Fosstodon @calculang
Questions?