‘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

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
viewof mrr_in = Inputs.range([0, 500000], {label: "monthly revenue (mrr)", step: 5000, value:155000});
viewof mrr_growth_in = Inputs.range([-0.3, .8], {label: "mrr growth factor (monthly!)", step: 0.01, value:0.15});
viewof vc_1_in = Inputs.range([0, 1e7], {label: "venture capital R1 (vc)", step: 100000, value: 1000000 });
viewof vc_2_in = Inputs.range([0, 1e7], {label: "venture capital R2 (vc)", step: 100000, value: 2000000 });
viewof salary_per_employee_in = Inputs.range([0, 30000], {label: "salary/employee", step: 1000});
viewof employees_0_in = Inputs.range([0, 50], {label: "employees @ start", step: 1, value:26});
viewof new_employees_per_month_in = Inputs.range([0, 10], {label: "new employees per mth", step: 1, value:2});
viewof rent_in = Inputs.range([0, 300000], {label: "rent", step: 10000, value:200000});
viewof last_month_in = Inputs.range([0, 36], {label: "last month (0-index)", step: 1, value:17});
viewof npv_i_in = Inputs.range([-0.1, .2], {label: "npv interest rate (monthly!)", step: 0.001, value:0.0});
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'})

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;

  form.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  };

  return form;
}

mutable inputs_history = [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}`
)]
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
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'})

Known salary growth error heatmap results

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;

  form.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  };

  return form;
}

mutable inputs_history_projectile = [JSON.parse(`{"angle_in":1,"power_in":30,"g_in":1,"drag_coefficient_in":0.01,"t_interval":[0,60]}`
)]
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
projectile_formulae = ['x','y','dx','dy','drag_x','drag_y']

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'})

Loan

Correct?


inputs ⚙️
Code
viewof principal_in = Inputs.range([0, 500000], {label: "Principal 💰 €", step:10000, value:+(q.get('principal_in') ?? 100000)});

viewof i_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 term_in = Inputs.select([25, 10, 5, 30, 35, 40, 1, 480], {label: "Term (years)", value: +(q.get('term_in')??10)});
Code
simple_loan_formulae = ['repayment_amount','v_pow_term_left','interest','capital_repayment','repayment_due','interest_repayment','repayment','balance']

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'})

In the wild 🐍🦜🌳

Examples published using calcuvizspec:

Mixed up examples:

calculang is not opinionated

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

CalcWithDec.dev

Twitter @calculang

Fosstodon @calculang

Questions?