โ€˜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
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});
mrr_in = 155000
mrr_growth_in = 0.15
vc_1_in = 1000000
vc_2_in = 2000000
salary_per_employee_in = 15000
employees_0_in = 26
new_employees_per_month_in = 2
rent_in = 200000
last_month_in = 17
npv_i_in = 0

NPV โ‚ฌ 1,083,592.14

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'})
-101234567891011121314151617month_inโˆ’1,500,000โˆ’1,000,000โˆ’500,0000500,0001,000,0001,500,0002,000,0002,500,0003,000,000valuemrr_cfpayroll_cfrent_cfvc_cfformula
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'})
employeesmrr_cfnpvpayroll_cfrent_cftotal_cfvc_cfformula-101234567891011121314151617month_in0.0026.0028.0030.0032.0034.0036.0038.0040.0042.0044.0046.0048.0050.0052.0054.0056.0058.0060.000.00155,000.00180,084.31209,228.12243,088.39282,428.41328,135.00381,238.48442,935.92514,618.12597,900.96694,661.81807,081.87937,695.361,089,446.571,265,756.341,470,599.051,708,592.341,985,101.090.00โˆ’200,000.00โˆ’200,000.00โˆ’200,000.00โˆ’200,000.00โˆ’200,000.00โˆ’200,000.00โˆ’200,000.00โˆ’200,000.00โˆ’200,000.00โˆ’200,000.00โˆ’200,000.00โˆ’200,000.00โˆ’200,000.00โˆ’200,000.00โˆ’200,000.00โˆ’200,000.00โˆ’200,000.00โˆ’200,000.001,083,592.141,518,592.141,958,507.832,399,279.721,836,191.332,263,762.922,675,627.913,064,389.433,421,453.513,736,835.383,998,934.434,194,272.622,307,190.752,319,495.392,210,048.821,954,292.481,523,693.43885,101.090.000.00โˆ’390,000.00โˆ’420,000.00โˆ’450,000.00โˆ’480,000.00โˆ’510,000.00โˆ’540,000.00โˆ’570,000.00โˆ’600,000.00โˆ’630,000.00โˆ’660,000.00โˆ’690,000.00โˆ’720,000.00โˆ’750,000.00โˆ’780,000.00โˆ’810,000.00โˆ’840,000.00โˆ’870,000.00โˆ’900,000.000.00โˆ’435,000.00โˆ’439,915.69โˆ’440,771.88563,088.39โˆ’427,571.59โˆ’411,865.00โˆ’388,761.52โˆ’357,064.08โˆ’315,381.88โˆ’262,099.04โˆ’195,338.191,887,081.87โˆ’12,304.64109,446.57255,756.34430,599.05638,592.34885,101.090.000.000.000.001,000,000.000.000.000.000.000.000.000.002,000,000.000.000.000.000.000.000.00
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'})
formulaemployeesmrr_cfpayroll_cfrent_cftotal_cfvc_cf-101234567891011121314151617month_in050value02,000,000value0valueโˆ’200,0000value02,000,000value02,000,000value
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'})
formulaemployeesmrr_cfpayroll_cfrent_cftotal_cfvc_cf-101234567891011121314151617month_in050value02,000,000value0valueโˆ’200,0000value02,000,000value02,000,000value
cfs = Module {mrr_cf$m: ฦ’(key), mrr_cf: ฦ’(a), vc_cf$m: ฦ’(key), vc_cf: ฦ’(a), rent_cf$m: ฦ’(key), rent_cf: ฦ’(a), employees$m: ฦ’(key), employees: ฦ’(a), payroll_cf$m: ฦ’(key), payroll_cf: ฦ’(a), total_cf$m: ฦ’(key), total_cf: ฦ’(a), npv$m: ฦ’(key), npv: ฦ’(a), mrr$m: ฦ’(key), mrr: ฦ’(a), month$m: ฦ’(key), month: ฦ’(a), mrr_growth$m: ฦ’(key), mrr_growth: ฦ’(a), โ€ฆ}

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}`
)]
form = Object {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 = Mutable {}
inputs_history = Array(1) [Object]
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'})
0input_cursor_idaccumulated_empee_contributionโ€ฆaccumulated_empee_contributionsprojected_fund_valuesalaries_per_projected_fundformula1,181,418.27249,972.3999,988.9611.81
No results.
pension_calculator_formulae = Array(9) ["fund_value", "unit_balance", "unit_allocation", "unit_price", "empee_contribution", "accumulated_empee_contributions", "empee_contribution_tax_relief", "emper_contribution", "salary"]

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'})
accumulated_eโ€ฆempee_contribโ€ฆempee_contribโ€ฆemper_contribuโ€ฆfund_valuesalaryunit_allocationunit_balanceunit_priceformula29303132333435363738394041424344454647484950515253545556575859606162636465age_in0.009,600.0019,872.0030,853.4442,583.7155,104.2468,458.6382,692.7297,854.74113,995.41131,168.07149,428.82168,836.65189,453.60211,344.91234,579.15259,228.44285,368.61313,079.36342,444.49373,552.10406,494.80441,369.94478,279.84517,332.06558,639.66602,321.46648,502.35697,313.58748,893.09803,385.85860,944.22921,728.28985,906.281,053,655.021,125,160.261,181,418.270.009,600.0018,925.7127,984.9836,785.4145,334.4053,639.1361,706.5869,543.5477,156.5884,552.1091,736.3398,715.29105,494.86112,080.72118,478.41124,693.31130,730.65136,595.49142,292.76147,827.25153,203.61158,426.37163,499.90168,428.48173,216.23177,867.20182,385.28186,774.27191,037.86195,179.64199,203.08203,111.56206,908.37210,596.70214,179.66214,179.660.009,600.009,325.719,059.278,800.438,548.998,304.738,067.457,836.957,613.047,395.537,184.236,978.966,779.566,585.866,397.696,214.906,037.335,864.845,697.275,534.495,376.365,222.755,073.534,928.574,787.764,650.964,518.084,388.994,263.594,141.784,023.443,908.483,796.813,688.333,582.950.001.001.001.051.101.161.221.281.341.411.481.551.631.711.801.891.982.082.182.292.412.532.652.792.933.073.233.393.563.733.924.124.324.544.765.005.255.520.005,000.005,100.005,202.005,306.045,412.165,520.405,630.815,743.435,858.305,975.466,094.976,216.876,341.216,468.036,597.396,729.346,863.937,001.217,141.237,284.067,429.747,578.337,729.907,884.508,042.198,203.038,367.098,534.438,705.128,879.229,056.819,237.949,422.709,611.169,803.380.000.005,000.0010,100.0015,302.0020,608.0426,020.2031,540.6037,171.4242,914.8548,773.1454,748.6060,843.5867,060.4573,401.6679,869.6986,467.0893,196.43100,060.35107,061.56114,202.79121,486.85128,916.59136,494.92144,224.82152,109.31160,151.50168,354.53176,721.62185,256.05193,961.17202,840.40211,897.20221,135.15230,557.85240,169.01249,972.39249,972.390.002,000.002,040.002,080.802,122.422,164.862,208.162,252.322,297.372,343.322,390.192,437.992,486.752,536.482,587.212,638.962,691.742,745.572,800.482,856.492,913.622,971.893,031.333,091.963,153.803,216.873,281.213,346.843,413.773,482.053,551.693,622.723,695.183,769.083,844.463,921.350.000.005,000.005,100.005,202.005,306.045,412.165,520.405,630.815,743.435,858.305,975.466,094.976,216.876,341.216,468.036,597.396,729.346,863.937,001.217,141.237,284.067,429.747,578.337,729.907,884.508,042.198,203.038,367.098,534.438,705.128,879.229,056.819,237.949,422.709,611.169,803.380.0050,000.0051,000.0052,020.0053,060.4054,121.6155,204.0456,308.1257,434.2858,582.9759,754.6360,949.7262,168.7263,412.0964,680.3365,973.9467,293.4268,639.2970,012.0771,412.3172,840.5674,297.3775,783.3277,298.9878,844.9680,421.8682,030.3083,670.9185,344.3287,051.2188,792.2390,568.0892,379.4494,227.0396,111.5798,033.8099,994.480.00
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'})
formulaaccumulateโ€ฆempee_conโ€ฆempee_conโ€ฆemper_contโ€ฆfund_valuesalaryunit_allocatโ€ฆunit_balanceunit_price29303132333435363738394041424344454647484950515253545556575859606162636465age_in0200,000value010,000value0value010,000value01,000,000value0100,000value010,000value0200,000value05value
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'})
formulaaccumulateโ€ฆempee_conโ€ฆempee_conโ€ฆemper_contโ€ฆfund_valuesalaryunit_allocatโ€ฆunit_balanceunit_price29303132333435363738394041424344454647484950515253545556575859606162636465age_in0200,000value010,000value0value010,000value01,000,000value0100,000value010,000value0200,000value05value

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

TypeError: Cannot destructure property 'ordinalDomain' of 'r.fieldSchema(...)' as it is undefined.

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

TypeError: Cannot destructure property 'ordinalDomain' of 'r.fieldSchema(...)' as it is undefined.

densesmall = "small"
small = true
Caution

Known salary growth error heatmap results

pension_calculator = Module {fund_value: ฦ’(โ€ฆ), unit_balance: ฦ’(โ€ฆ), unit_allocation: ฦ’(โ€ฆ), unit_price: ฦ’(โ€ฆ), empee_contribution: ฦ’(โ€ฆ), accumulated_empee_contributions: ฦ’(โ€ฆ), accumulated_empee_contribution_tax_relief: ฦ’(โ€ฆ), pension_tax_relief_ratio: ฦ’(โ€ฆ), empee_contribution_tax_relief: ฦ’(โ€ฆ), emper_contribution: ฦ’(โ€ฆ), salary: ฦ’(โ€ฆ), projected_fund_value: ฦ’(โ€ฆ), salaries_per_projected_fund: ฦ’(โ€ฆ), age: ฦ’(โ€ฆ), age_0: ฦ’(โ€ฆ), retirement_age: ฦ’(โ€ฆ), salary_0: ฦ’(โ€ฆ), salary_inflation_rate: ฦ’(โ€ฆ), empee_contribution_rate: ฦ’(โ€ฆ), emper_contribution_rate: ฦ’(โ€ฆ), โ€ฆ}

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]}`
)]
  import {interval as interval} from "@mootari/range-slider"
time โ†”๏ธ
0 โ€ฆ 60
form_projectile = Object {angle_in: 1, power_in: 30, g_in: 1, drag_coefficient_in: 0.01, t_interval: Array(2)}
mutable inputs_history_projectile = Mutable {}
inputs_history_projectile = Array(1) [Object]
No results.
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'})
024681012141618202224xโˆ’200โˆ’1000100200300400y0input_cursor_id
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'})
projectile_formulae = Array(6) ["x", "y", "dx", "dy", "drag_x", "drag_y"]
drag_xdrag_ydxdyxyformula051015202530354045505560t_in0.002.575.017.339.5311.6313.6315.5317.3319.0520.6822.2323.710.00132.21233.43305.18348.92366.00357.75325.39270.11193.0495.24โˆ’22.27โˆ’158.53NaN0.500.480.450.430.410.390.370.350.340.320.300.29NaN23.9117.8412.066.571.35โˆ’3.62โˆ’8.34โˆ’12.83โˆ’17.11โˆ’21.17โˆ’25.03โˆ’28.71NaN0.020.050.070.090.110.130.150.170.190.200.220.23NaN1.082.162.933.423.653.613.342.832.101.160.03โˆ’1.30
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'})
formuladrag_xdrag_ydxdyxyโˆ’505101520253035404550556065t_in0.00.2value0value0.00.5value0value020value0value
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'})
formuladrag_xdrag_ydxdyxy051015202530354045505560t_in0.00.2value0value0.00.5value0value020value0value0input_cursor_id
projectile = Module {x$m: ฦ’(key), x: ฦ’(a), dx$m: ฦ’(key), dx: ฦ’(a), drag_x$m: ฦ’(key), drag_x: ฦ’(a), drag_y$m: ฦ’(key), drag_y: ฦ’(a), y$m: ฦ’(key), y: ฦ’(a), dy$m: ฦ’(key), dy: ฦ’(a), t$m: ฦ’(key), t: ฦ’(a), angle$m: ฦ’(key), angle: ฦ’(a), power$m: ฦ’(key), power: ฦ’(a), g$m: ฦ’(key), g: ฦ’(a), โ€ฆ}

Loan

You borrow โ‚ฌ 100,000 at 4.0% for 10 years.

Your repayment amount is โ‚ฌ 12,329.09 pa.

Correct?


show_qr_code = Array(0) []
qr = Object {create: ฦ’(t, r), toCanvas: ฦ’(), toDataURL: ฦ’(), toString: ฦ’()}
params = Object {principal_in: 100000, i_in: 0.04, term_in: 10}
qr_params = "principal_in=100000&i_in=0.04&term_in=10"
qr_url = "https://calcuvizspec-presentation.pages.dev/calcuvizspec-revealjs.html?principal_in=100000&i_in=0.04&term_in=10#/simple-loan"

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)});
principal_in = 100000
i_in = 0.04
term_in = 10

Calculated repayment is โ‚ฌ 12,329.09 pa.

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'})
simple_loan_formulae = Array(8) ["repayment_amount", "v_pow_term_left", "interest", "capital_repayment", "repayment_due", "interest_repayment", "repayment", "balance"]
balancecapital_repaymโ€ฆinterestinterest_repayโ€ฆrepaymentrepayment_amโ€ฆrepayment_duev_pow_term_leftformula-101234567891011year_in0.000.0012,329.0912,329.0912,329.0912,329.0912,329.0912,329.0912,329.0912,329.0912,329.0912,329.090.000.620.650.680.700.730.760.790.820.850.890.920.961.000.000.004,000.003,666.843,320.352,960.002,585.232,195.481,790.131,368.57930.15474.200.000.000.008,329.098,662.269,008.759,369.109,743.8610,133.6210,538.9610,960.5211,398.9411,854.900.001.000.001.001.001.001.001.001.001.001.001.001.000.000.000.004,000.003,666.843,320.352,960.002,585.232,195.481,790.131,368.57930.15474.200.000.000.0012,329.0912,329.0912,329.0912,329.0912,329.0912,329.0912,329.0912,329.0912,329.0912,329.090.000.00100,000.0091,670.9183,008.6573,999.9064,630.8054,886.9444,753.3234,214.3623,253.8411,854.900.000.00
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'})
formulabalancecapital_repโ€ฆinterestinterest_repโ€ฆrepaymentrepayment_โ€ฆrepayment_โ€ฆv_pow_terโ€ฆ-101234567891011year_in0100,000value010,000value0value0value010,000value010,000value01value01value
simple_loan = Module {v: ฦ’(โ€ฆ), v_pow_term_left: ฦ’(โ€ฆ), repayment_amount: ฦ’(โ€ฆ), interest: ฦ’(โ€ฆ), capital_repayment: ฦ’(โ€ฆ), interest_repayment: ฦ’(โ€ฆ), repayment_due: ฦ’(โ€ฆ), repayment: ฦ’(โ€ฆ), balance: ฦ’(โ€ฆ), interest_rate: ฦ’(โ€ฆ), principal: ฦ’(โ€ฆ), i: ฦ’(โ€ฆ), term: ฦ’(โ€ฆ), year: ฦ’(โ€ฆ), missed_repayment_year: ฦ’(โ€ฆ), skip_interest: ฦ’(โ€ฆ), d_i_year: ฦ’(โ€ฆ), d_i: ฦ’(โ€ฆ), Symbol(Symbol.toStringTag): "Module"}

In the wild ๐Ÿ๐Ÿฆœ๐ŸŒณ

Examples published using calcuvizspec:

Mixed up examples:

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

CalcWithDec.dev

Twitter @calculang

Fosstodon @calculang

Questions?

  import {calcuvizspec as calcuvizspec} from "@declann/little-calcu-helpers"
embed = ฦ’(โ€ฆ)