calculang is a language for calculations

Design properties
  • calculations defined by pure-functions without side-effects (or ‘formulae’)
  • input inference: argument-passing boilerplate or ‘threading’ is inferred
  • modularity with parent-definition precedence
  • everything defined as a function - including constants and inputs

These are designed to promote flexible & reusable calculation code, and logically-structured models which are maintainable, general, and with low refactoring needs.

calculang generates pure-computation and portable Javascript modules, which are usable across application languages.
Why?

calculang is only for calculations; this forces separation of concerns: calculations (=> numbers and workings) encapsulated without application programming complexity

For calculations (=> numbers and workings) this:

  • facilitates frictionless transparency
  • facilitates specialised tooling for numbers
    • tools to explore, trace, interpret, visualize, …
Examples

In ‘calculang model’ below showing calculang code you can also inspect generated Javascript code using options under 🤫

Application code includes calcuvizspec visualization helper and calcudata convenience functions, and can be seen in ➤ Code blocks.

calculang code can be used in multiple applications (wider than those shown) - like a library, but also in multiple calculang models with control over calculation details due to parent-definition precedence (examples: shop-demand-curve-modular, shop-stressed-sales and savings-rec).

calculang model

🤫

📺 application(s)

Code
import {Tangle} from "@declann/colored-tangle"

viewof i_start = Inputs.input(1)

md`*starting i*: ${Inputs.bind(Tangle({min: -10, max:150, step:1, power:1.3, color:'darkblue'}), viewof i_start)}`
Code
embed(
  calcuvizspec({
    models: [model],
    input_cursors: [{}],
    mark: {type: 'text', fontSize:15, fontWeight: 'bold'},
    encodings: {
      x: {name: 'formula', type:'nominal', domain: ['fizz','buzz','fizzbuzz'], axis: {labelFontSize: 60}, sort: 'descending'},
      y: {name: 'i_in', type:'nominal', domain: _.range(i_start, i_start+20), axis: {grid:true}},
      text: {name: 'value', type: 'nominal'},
      color: {name: 'value', type: 'nominal', legend:false},
    },
    width: 250,// height:50,
    spec_post_process: spec => { spec.encoding.y.axis = {grid:true}; return spec;}
}), {theme: theme, actions})
Code
viewof shop_application = Inputs.select(['shop-sliders','shop-x-sales-price'], {value:'shop-sliders',label:'application', width:150})
🍼
Code
viewof purchase_price_in1 = Inputs.range([5,7], {step:0.05, label:'purchase_price_in', value:5, width:200})
viewof sales_price_in = Inputs.range([5,7], {step:0.05, label:'sales_price_in', value:6, width:200})
viewof expenses_in1 = Inputs.range([0,20000], {step:1000, label:'expenses_in', value:15000, width:200})
viewof units_in = Inputs.range([0,50000], {step:1000, label:'units_in', value:20000, width:200})

👀

Code
embed(
  calcuvizspec({
    models: [model],
    input_cursors: [{sales_price_in,purchase_price_in:purchase_price_in1,expenses_in:expenses_in1,units_in}],
    mark: {type: 'text', fontSize:20, fontWeight: 'bold'},
    encodings: {
      y: {name: 'formula', type:'nominal', domain: formulae_not_inputs, axis: {labelFontSize: 60}},
      text: {name: 'value', type: 'quantitative', format:',.0f'},
      color: {name: 'formula', type:'nominal', domain: formulae_not_inputs, legend:false},
    },
    width: 90,// height:50,
    spec_post_process: spec => { spec.encoding.y.title = null; spec.encoding.y.sort = ["profit"]; spec.encoding.y.axis = {labelFontSize: 20, labelFontWeight: 'bold'}; return spec;}
}), {theme: theme, actions})
🍼
Code
viewof purchase_price_in2 = Inputs.range([5,7], {step:0.05, label:'purchase_price_in', value:5, width:200})
viewof sales_price_in2 = Inputs.range([5,7], {step:0.05, label:'sales_price_in', value:6, width:200, disabled:true})
viewof expenses_in2 = Inputs.range([0,20000], {step:1000, label:'expenses_in', value:15000, width:200})
viewof units_in2 = Inputs.range([0,50000], {step:1000, label:'units_in', value:20000, width:200})
Code
embed(
  calcuvizspec({
    models: [model],
    input_cursors: [{purchase_price_in:purchase_price_in2,expenses_in:expenses_in2,units_in:units_in2}],
    mark: {type: 'text', fontSize:15, fontWeight: 'bold'},
    encodings: {
      x: {name: 'sales_price_in', type:'quantitative', domain: [5,6,7],  scale: {domain:[4,7]}},
      y: {name: 'formula', type:'nominal', domain: formulae_not_inputs, axis: {labelFontSize: 60}},
      text: {name: 'value', type: 'quantitative', format:',.0f'},
      color: {name: 'formula', type:'nominal', domain: formulae_not_inputs, legend:false},
    },
    width: 200,// height:50,
    spec_post_process: spec => { spec.encoding.y.title = null; spec.encoding.y.sort = ["profit"]; spec.encoding.y.axis = {labelFontSize: 15, labelFontWeight: 'bold'}; return spec;}
}), {theme: theme, actions})
Code
embed(
  calcuvizspec({
    models: [model],
    input_cursors: [{purchase_price_in:purchase_price_in2,expenses_in:expenses_in2,units_in:units_in2}],
    mark: 'bar',
    encodings: {
      x: {name: 'sales_price_in', type:'nominal', domain: _.range(5,7.01,.1), format:',.2f'},
      y: {name: 'value', type: 'quantitative', format:',.0f', independent: true, zero: false},
      color: {name: 'formula', type:'nominal', domain: formulae_not_inputs, legend:false},
      row: {name: 'formula', type:'nominal', domain: formulae_not_inputs, axis: {labelFontSize: 60}},
    },
    width: 230, height:50,
    spec_post_process: spec => { spec.encoding.y.title = null; spec.encoding.row.sort = ["profit"]; spec.encoding.row.title = null; spec.encoding.row.header = {labelFontSize: 16, labelFontWeight: 'bold'};  return spec;}
}), {theme: theme, actions})
Code
embed(
  calcuvizspec({
    models: [model],
    input_cursors: [{purchase_price_in:purchase_price_in2,expenses_in:expenses_in2,units_in:units_in2}],
    mark: {type:'line', point:true},
    encodings: {
      x: {name: 'sales_price_in', type:'quantitative', domain: _.range(5,7.01,.2), format:',.2f'},
      y: {name: 'value', type: 'quantitative', format:',.0f', independent: true, zero: false},
      color: {name: 'formula', type:'nominal', domain: formulae_not_inputs, legend:false},
      row: {name: 'formula', type:'nominal', domain: formulae_not_inputs, axis: {labelFontSize: 60}},
    },
    width: 230, height:50,
    spec_post_process: spec => { spec.encoding.y.title = null; spec.encoding.row.sort = ["profit"]; spec.encoding.row.title = null; spec.encoding.row.header = {labelFontSize: 16, labelFontWeight: 'bold'};  return spec;}
}), {theme: theme, actions})
🍼
Code
embed(
  calcuvizspec({
    models: [model],
    input_cursors: [{purchase_price_in,expenses_in}],
    mark: 'bar',
    encodings: {
      x: {name: 'sales_price_in', type:'nominal', domain: _.range(5,7.01,.1), format:',.2f'},
      y: {name: 'value', type: 'quantitative', format:',.0f', independent: true, zero: false},
      color: {name: 'formula', type:'nominal', domain: ff, legend:false},
      row: {name: 'formula', type:'nominal', domain: ff},
    },
    width: 230, height:50,
    spec_post_process: spec => { spec.encoding.y.title = null; spec.encoding.row.sort = ["profit"]; spec.encoding.row.title = null; spec.encoding.row.header = {labelFontSize: 16, labelFontWeight: 'bold'};  return spec;}
}), {theme: theme, actions})
Code
ff = [...formulae_not_inputs, 'units']

embed(
  calcuvizspec({
    models: [model],
    input_cursors: [{purchase_price_in,expenses_in}],
    mark: {type:'line', point:true},
    encodings: {
      x: {name: 'sales_price_in', type:'quantitative', domain: _.range(5,7.01,.2)},
      y: {name: 'value', type: 'quantitative', format:',.0f', independent: true, zero: false},
      color: {name: 'formula', type:'nominal', domain: ff, legend:false},
      row: {name: 'formula', type:'nominal', domain: ff},
    },
    width: 230, height:50,
    spec_post_process: spec => { spec.encoding.y.title = null; spec.encoding.row.sort = ["profit"]; spec.encoding.row.title = null; spec.encoding.row.header = {labelFontSize: 16, labelFontWeight: 'bold'};  return spec;}
}), {theme: theme, actions})
🍼
Code
viewof annual_payment_in = Inputs.range([0,1000], {step: 100, value:1000, label: 'annual_payment_in'})
viewof interest_rate_in = Inputs.range([0,0.5], {step:0.01, label: 'interest_rate_in', value: 0.02})
viewof duration_in = Inputs.range([0,10], {step:1, value:5, label:'duration_in'})

👀

Code
embed(
  calcuvizspec({
    models: [model],
    input_cursors: [{annual_payment_in,interest_rate_in,duration_in}],
    mark: {type: 'text', fontSize:15, fontWeight: 'bold'},
    encodings: {
      x: {name: 'formula', type:'nominal', domain: ['balance','interest','deposits','interest_rate'], axis: {labelFontSize: 60}},
      y: {name: 'year_in', type:'nominal', domain: _.range(0,duration_in+1,1), sort:'ascending'},
      text: {name: 'value', type: 'quantitative', format:',.2f'},
      color: {name: 'formula', type:'nominal', domain: ['balance','interest','deposits','interest_rate'], legend:false},
    },
  "width": 340, height:160
}), {theme: theme, actions})
🍼

👀

Code
savings_rec_data = calcudata({
  models:[model],
  input_domains: {year_in:_.range(0,6), actual_interest_rate_co_in: [actual_interest_rate_co_in-1,actual_interest_rate_co_in]},
  input_cursors: [{annual_payment_in:1000}], 
  outputs: ['balance','interest','deposits','interest_rate']
})


savings_actualator.addSignalListener("y", (_, r) => { // this didn't improve perf a lot vs event listener: nearly every mousemove is a signal change, I suppose
  //if (observable_world == 0) return;
  viewof actual_interest_rate_co_in.value = r.year_in[0];
  viewof actual_interest_rate_co_in.dispatchEvent(new CustomEvent('input'), {bubbles:true});
}) && 1
Code
viewof savings_actualator = embed(
  {
  "data": {"name": "data"},
  "transform": [
    {
      "window": [{"op": "lag", "field": "value", "as": "A_value"}],
      "groupby": ["formula","year_in"],
      "sort": [{"field": "actual_interest_rate_co_in"}],
      "frame": [-1, 0]
    },
    {"calculate": "datum.value-datum.A_value", "as": "impact"},
  ],
  "encoding": {
    "y": {"field": "year_in", "type": "nominal", "sort": "ascending"},
    "color": {"field": "formula", "type": "nominal"},
    "x": {
      "field": "formula",
      "type": "nominal",
      "axis": {"labelAngle": 0, "orient": "top", "labelLimit": 90}
    }
  },
  layer: [{
            "params": [{"name": "y", "select": { "type": "point", "on": "mouseover", "nearest": true, "encodings": ["y"], "toggle":false}}],
  "mark": {
    "type": "text",
    "fontSize": 15,
    "fontWeight": "bold",
    "tooltip": false
  },
        "transform": [{"filter": `datum.actual_interest_rate_co_in != ${actual_interest_rate_co_in-1}`},
        //{"calculate": "datum.year_in > datum.actual_interest_rate_co_in", "as": "fut"}
        ],

  encoding: {
    "opacity":             {"condition": {"test": "datum.year_in > datum.actual_interest_rate_co_in", "value": 0.5}, "value": 1},
// {"field": "fut", "type": "nominal", "sort": "descending"},
        "text": {
      "field": "value",
      "type": "quantitative",
      "format": ",.2f",
      "axis": {"format": ",.2f"}
    },

  }
  },{
  "mark": {
    "type": "text", dx:20, dy: -10,
    "fontSize": 12,
    "fontWeight": "bold",//
    "tooltip": false
  },
        "transform": [{"filter": `abs(datum.impact) >= 0.00001 && datum.actual_interest_rate_co_in != ${actual_interest_rate_co_in-1}`}],

  encoding: {
            "color": {"value": "green", "condition": {"test": "datum.impact < 0", "value": "red"}},
                "opacity":             {"condition": {"test": "datum.year_in > datum.actual_interest_rate_co_in", "value": 0.5}, "value": 1},

        "text": {
      "field": "impact",
      "type": "quantitative",
      "format": ",.2f",
      "axis": {"format": ",.2f"}
    },

  }
  }],
  "config": {"legend": {"disable": true}},
  "datasets": { data: savings_rec_data
  },
  "width": 340, height:160
}, {theme: theme, actions})


contour visual of f(x_in,y_in):

🍼

See also Hearty maths with calculang ❤️

inputs ⚙️
Code
import {interval} from '@mootari/range-slider'

viewof form_projectile = {
  let form = Inputs.form({
    angle_in: Inputs.range([-2,2], {value: 1, step:0.002, 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: "Air resistance" }),
    t_interval: interval([0,100], {step:1, label: 'time ↔️', color: 'skyblue', value:[0,60], width:200}),
  });

  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]}`
)]

Projectile Path 🧭

Code
clip = true


// NOTE: adding some hurried customisation and experimentation for fireworks effect! should be cleaner
embed(
  calcuvizspec({
    models: [model],
    input_cursors: inputs_history_projectile,
    mark: {type:'point',clip},
    encodings: {
      detail: {name: 't_in', type: 'quantitative', domain: _.range(form_projectile.t_interval[0],form_projectile.t_interval[1]+0.1,1)},
      //opacity: {name: 't_in', type: 'quantitative', sort:'descending', scale:{domain:[0,100]}, legend:null/*not working => spec_post_process*/, domain: _.range(form_projectile.t_interval[0],form_projectile.t_interval[1]+0.1,1)},
      y: {name: 'y', type:'quantitative', sort: 'ascending', scale:{domain:[-1200,800], sort: 'descending'}},
      x: {name: 'x', grid:false, type: 'quantitative', scale:{domain:[-30,40]}},
      color: {name: 'input_cursor_id', type: 'nominal'},
    },
    width: 300, height: 220,
    spec_post_process: (spec) => {spec.encoding.y.axis = {tickCount:1, grid:true}; spec.datasets.tags = tags;
    spec.encoding.x.axis = {tickCount:3, grid:false};
    spec.transform = [
    {"lookup": "input_cursor_id", "from": {"data": {"name": "tags"}, "key":"i", "fields": ["tag"]}, "as": ["tag"]}
  ];
    spec.encoding.color = {"field": "tag", "type": "nominal", "sort": {"field": "input_cursor_id"}, "title": "scenario"}

    return spec}
}), {theme: theme, actions})
Code
tags = [{i:0, tag:'initial'},...inputs_history_projectile.slice(1).map((d,i) => ({i:i+1, tag:Object.entries(d).filter(([k,v]) => v != inputs_history_projectile[i-1+1][k])[0]})).map(d => ({...d,tag:`${d.tag[0]} ${inputs_history_projectile[d.i-1][d.tag[0]]} -> ${d.tag[1]}`}))]

n/a

showing use of design properties in calculang code

🍼
Code
viewof dampener_in = Inputs.range([-1,2], {value: 0.90, step:0.01, label: "dampener factor"})//viewof sales_price_in = Inputs.range([5,7], {step:0.20, label:'sales_price_in', value:6, disabled: true})
viewof dx_in = Inputs.range([-5,5], {value: 3, step:0.5, label: "dx"})

viewof t = interval([0,120], {
  step: 1,
  value: [0,60],
  label: 'time',
  width:200
})

viewof frame_rate = Inputs.range([0,60], {step: 5, value:40, label: 'frame rate'})
Code
bounce_data = calcudata({models:[model],
                input_domains: {t_in: _.range(t[0], t[1], 1)},
                input_cursors: [{dx_in,dampener_in}], 
                outputs: ['x','y','dy','bouncing'],
                pivot: true
                })


p5(sketch => {
  //sync; 
  
  sketch.setup = function () {
    sketch.createCanvas(300, 120);
    sketch.frameRate(frame_rate);

  };

  var t = 0, w, h;
  
  sketch.draw = function () {

    sketch.background("#fed");
    sketch.stroke("black");
    if (bounce_data[t] == undefined) t = 0;
    w = 30; h = 30;
    if (bounce_data[t].bouncing) { w = 36; h = 18; }

    sketch.ellipse(100+bounce_data[t].x, 5+100 - bounce_data[t].y, w, h); // poor on move to calcudata but works

    // just for fun
    //if(t>0) { sketch.stroke("red"); sketch.line(bounce_data[t].x, bounce_data[t].y, bounce_data[t-1].x, bounce_data[t-1].y); } 
    
     t++;
    }
})
Code
import {p5} from "@tmcw/p5"
Code
Inputs.table(bounce_data)

Scrub the field of view interval below to ‘look around’

Click on bars to highlight rays (note 2-way co-ordination).

Code
viewof fov2 = interval([-3,3], {
  step: 0.005,
  value: [-1-Math.PI/2,1-Math.PI/2],
  label: 'field of view',
  width:200
})

viewof scene = embed(
  calcuvizspec({
    models: [model],
    input_cursors: [{player_x_in,player_y_in}],
    mark: {type:'bar', point:false, clip:true, tooltip:false},
    encodings: {
      x: {grid: false, name: 'ray_angle_in', type: 'quantitative', domain: [clamped_ray_angle_in, ..._.range(fov2[0],fov2[1],ray_angle_in_step_size)], nice:false, ticks:2},
      color: {name: 'ray_hit_color', type:'nominal', legend:false},
      y: {grid: false, name: 'inverse_ray_length', type: 'quantitative'},
      opacity: {name: 'inverse_ray_length', type: 'quantitative', legend:false},
    },
    width: 400, height:150,
    spec_post_process: spec => {
      spec.encoding.y.scale = {domain:[0,0.3],};
      spec.encoding.x.axis = labels ? {tickCount:2, grid:false} : null
      spec.encoding.y.axis = labels ? {grid:false} : null
      
      spec.params = [{"name": "ray_angle_in", "value": { "ray_angle_in": 0}, "select": { "type": "point", "on": "mouseover", "nearest": true, "encodings": ["x"], toggle:false}}];
      spec.encoding.color.condition = {"test": `datum.ray_angle_in==${clamped_ray_angle_in || 999}`,"value": "red"}
      spec.encoding.opacity.condition = {"test": `datum.ray_angle_in==${clamped_ray_angle_in || 999}`,"value": 1}
      return spec
    }
}), {theme: theme, actions, renderer:'svg'})

viewof labels = Inputs.toggle({label: "labels", value: true})
🗺️ level
Code
viewof level = embed(
  calcuvizspec({
    models: [model],
    input_cursors: [{player_x_in,player_y_in,ray_angle_in: clamped_ray_angle_in/*:(fov[0]+fov[1])/2*/
    ,fov_in:fov2}],
    mark: {type:'rect',tooltip:false},
    encodings: {
      x: {grid: false, name: 'level_x_in', type:'nominal', domain: _.range(0,63.1,1)},
      y: {grid: false, name: 'level_y_in', type: 'nominal', domain: _.range(0,63.1,1)},
      color: {name: (observable_world ? 'level_player_ray_fov' : 'level_player'), type: 'quantitative', legend:false},
      //row: {name: 'formula', type:'nominal', domain: ['level', 'level_plus_player', 'level_plus_player_plus_ray']},
    },
    width:280, height:280,
    spec_post_process: spec => {
            spec.params = [{"name": "xy", "select": { "type": "point", "on": "mouseover", "nearest": true, "encodings": ["x", "y"], toggle:false}}];
            let a = spec.encoding.x.axis; spec.encoding.x.axis = {...a, ticks:false, labels:false}
            a = spec.encoding.y.axis; spec.encoding.y.axis = {...a, ticks:false, labels:false}
            spec.encoding.color.scale = {scheme: 'turbo'}
      return spec;
    }
}), {theme: theme, actions})


viewof player_x_in = Inputs.range([3,61], {step:1, value:32, label:'player x'})
viewof player_y_in = Inputs.range([3,61], {step:1, value:32, label:'player y'})

//viewof observable_world_v = Inputs.checkbox(["overlay field of view"], {value: true})

observable_world = true

Playble version (on a computer: click on Keyboard Target then use arrow keys)

🍼
Code
viewof climate_sensitivity_in = Inputs.range([0,5], {step: 0.2, value:3, label: 'climate_sensitivity_in (buggy in viz 1)'})

emissions (warning: not accurate):

T is at 2100 (not Tmax)

Code
update_climate = (name, value) => {
  mutable emissions_ = value;

  /*emissions.change('results2',emissions.insert('results2',calcudata({
    models: [model],
    input_domains: {year_in: [2015,2100]},
    input_cursors: [{emissions_table_in: emissions_.map(d => ({year_in:d.data, emissions_rate:d.interpolated})),climate_sensitivity_in,ppm_to_GtC_in:2.3,drawdown_factor_in:0.001}],
    outputs: ['temperature_delta'],
    pivot:true
  }).map(({year_in,temperature_delta}) => ({year_in,temperature_delta})))).remove('results2',d => true).run()*/
  emissions.data('results2', 
    calcudata({
      models: [model],
      input_domains: {year_in: [2100]}, // should show more, to demo drawdown
      input_cursors: [{emissions_table_in: emissions_.map(d => ({year_in:d.data, emissions_rate:d.interpolated})),climate_sensitivity_in,ppm_to_GtC_in:2.3,drawdown_factor_in:0.001}],
      outputs: ['temperature_delta'],
      pivot:true
    }).map(({year_in,temperature_delta}) => ({year_in,temperature_delta}))
  )
}
{emissions.addDataListener('interpolated', update_climate) };
Code
viewof emissions = embed(
{
  "$schema": "https://vega.github.io/schema/vega/v5.json",
  "width": 350,
  "height": 200,
  "padding": 5,
  "data": [
    // using climate_results makes this responsive but resets visual on refresh
    {"name": "results2", "values": [{"year_in":2100, "temperature_delta":1.23 /*BAD*/}]},
    {
      "name": "table",
      "values": [
        {"category": 1960, "amount": 4},
        {"category": 1990, "amount": 8},
        {"category": 2020, "amount": 28},
        {"category": 2025, "amount": 29},
        {"category": 2030, "amount": 30},
        {"category": 2035, "amount": 32},
        {"category": 2040, "amount": 33},
        {"category": 2045, "amount": 34},
        {"category": 2050, "amount": 38},
        {"category": 2100, "amount": 90},
        {"category": 2150, "amount": 5}
      ]
    },
    {
      "name": "seq",
      "transform": [
        {"type": "sequence", "start": 1960, "stop": 2200, "step": 5},
        {"type": "filter", "expr": "datum.data >= bau.category"}
      ]
    },
    {
      "name": "seq2",
      "transform": [
        {"type": "sequence", "start": 1960, "stop": 2200, "step": 5},
        {
          "type": "lookup",
          "from": "table",
          "key": "category",
          "fields": ["data"],
          "as": ["table"]
        },
        {"type": "filter", "expr": "datum.table != null"},
        {"type": "window", "ops": ["lag"], "fields": ["table"]},
        {"type": "filter", "expr": "datum.lag_table != null"},
        {
          "type": "formula",
          "expr": "datum.lag_table.amount+(datum.table.amount-datum.lag_table.amount)*(datum.table.category-datum.lag_table.category)",
          "as": "interpolated"
        }
      ]
    },
    {
      "name": "interpolated",
      "transform": [
        {"type": "sequence", "start": 1960, "stop": 2200, "step": 1},
        {
          "type": "lookup",
          "from": "table",
          "key": "category",
          "fields": ["data"],
          "values": ["category"],
          "as": ["group"]
        },
        {"type": "window", "ops": ["next_value"], "fields": ["group"]},
        {
          "type": "lookup",
          "from": "seq2",
          "key": "data",
          "fields": ["next_value_group"],
          "as": ["group"]
        },
        {"type": "filter", "expr": "datum.group != null"},
        {
          "type": "formula",
          "expr": "max(0,datum.data <= bau.category ? datum.group.lag_table.amount+(datum.group.table.amount-datum.group.lag_table.amount)*(datum.data-datum.group.lag_table.category)/(datum.group.table.category-datum.group.lag_table.category) : bau.amount+(0-bau.amount)*(datum.data-bau.category)/(zero_category-bau.category))",
          "as": "interpolated"
        },
        {
          "type": "formula",
          "expr": "datum.data < 2020 ? 'old' : 'future'",
          "as": "old"
        }
      ]
    },
    {
      "name": "interpolated_baseline",
      "transform": [
        {"type": "sequence", "start": 1960, "stop": 2200, "step": 1},
        {
          "type": "lookup",
          "from": "table",
          "key": "category",
          "fields": ["data"],
          "values": ["category"],
          "as": ["group"]
        },
        {"type": "window", "ops": ["next_value"], "fields": ["group"]},
        {
          "type": "lookup",
          "from": "seq2",
          "key": "data",
          "fields": ["next_value_group"],
          "as": ["group"]
        },
        {"type": "filter", "expr": "datum.group != null"},
        {
          "type": "formula",
          "expr": "datum.group.lag_table.amount+(datum.group.table.amount-datum.group.lag_table.amount)*(datum.data-datum.group.lag_table.category)/(datum.group.table.category-datum.group.lag_table.category)",
          "as": "interpolated"
        }
      ]
    },
    {
      "name": "seq3",
      "transform": [
        {"type": "sequence", "start": 1960, "stop": 2200, "step": 5},
        {
          "type": "lookup",
          "from": "seq2",
          "key": "data",
          "fields": ["data"],
          "as": ["table"]
        },
        {
          "type": "window",
          "ops": ["first_value"],
          "sort": {"field": "table"},
          "fields": ["table"]
        },
        {
          "type": "formula",
          "as": "aaa",
          "expr": "isValid(datum.table) ?  0 :  datum.lag_table"
        }
      ]
    },
    {
      "name": "table-first",
      "source": "table",
      "transform": [{"type": "filter", "expr": "datum.category == 1960"}]
    },
    {
      "name": "table-baus",
      "source": "table",
      "transform": [
        {"type": "filter", "expr": "datum.category <= bau.category"}
      ]
    }
  ],
  "signals": [
    {
      "name": "bau",
      "value": {"category": 2030, "amount": 30},
      "on": [{"events": "@baus:mouseover", "update": "datum"}]
    },
    {
      "name": "zero_category",
      "update": "max(zero_category,bau.category)",
      "value": 2050,
      "on": [
        {"events": "@zeros:mouseover", "update": "max(datum.data,bau.category)"}
      ]
    },
    {
      "name": "history",
      "value": "2030|2050",
      "on": [
        {
          "events": "@baus:mouseover,@zeros:mouseover",
          "update": "history+','+bau.category+'|'+zero_category"
        }
      ]
    },
  ],
  "scales": [
    {
      "name": "xscale",
      "type": "linear",
      "zero": false,
      "domain": {"data": "table", "field": "category"},
      "range": "width",
      "padding": 0.05,
      "round": true
    },
    {
      "name": "yscale",
      "domain": {"data": "table", "field": "amount"},
      "nice": true,
      "range": "height"
    },
    {
      "name": "emissions",
      "type": "ordinal",
      "domain": ["old", "future"],
      "range": ["darkgrey", "steelblue"]
    },
    {
      "name": "bemissions",
      "type": "ordinal",
      "domain": ["old", "future"],
      "range": ["brown", "blue"]
    }
  ],
  "axes": [
    {
      "orient": "bottom",
      "scale": "xscale",
      "format": ".0f",
      "labelFontSize": 20,
      "tickCount": 6
    },
    {"orient": "left", "scale": "yscale"}
  ],
  "marks": [
    {
      "type": "rect",
      "from": {"data": "interpolated_baseline"},
      "encode": {
        "enter": {"fill": {"value": "pink"}, "opacity": {"value": 0.1}},
        "update": {
          "x": {"scale": "xscale", "field": "data"},
          "width": {"value": 10},
          "y": {"scale": "yscale", "field": "interpolated"},
          "y2": {"scale": "yscale", "value": 0}
        }
      }
    },
    {
      "type": "rect",
      "from": {"data": "interpolated"},
      "encode": {
        "enter": {
          "fill": {"scale": "emissions", "field": "old"},
          "opacity": {"value": 0.5}
        },
        "update": {
          "x": {"scale": "xscale", "field": "data"},
          "width": {"value": 9},
          "y": {"scale": "yscale", "field": "interpolated"},
          "y2": {"scale": "yscale", "value": 0}
        }
      }
    },
    {
      "type": "rect",
      "from": {"data": "table"},
      "encode": {
        "enter": {
          "x": {"scale": "xscale", "field": "category"},
          "width": {"scale": "xscale", "band": 1},
          "y": {"scale": "yscale", "field": "amount"},
          "y2": {"scale": "yscale", "value": 0},
          "opacity": {"value": 0.1}
        },
        "update": {"fill": {"value": "steelblue"}}
      }
    },
    {
      "type": "line",
      "from": {"data": "table-baus"},
      "encode": {
        "enter": {
          "strokeWidth": {"value": 5},
          "stroke": {"value": "black"},
          "interpolate": {"value": "linear"},
          "strokeCap": {"value": "round"}
        },
        "update": {
          "x": {"scale": "xscale", "field": "category", "band": 1},
          "y": {"scale": "yscale", "field": "amount"}
        }
      }
    },
    {
      "type": "rule",
      "encode": {
        "enter": {"strokeWidth": {"value": 5}, "strokeCap": {"value": "round"}},
        "update": {
          "x": {"scale": "xscale", "signal": "bau.category", "band": 1},
          "y": {"scale": "yscale", "signal": "bau.amount", "offset": 0},
          "x2": {"scale": "xscale", "signal": "zero_category", "band": 1},
          "y2": {"scale": "yscale", "value": 0, "offset": 0}
        }
      }
    },
    {
      "type": "rule",
      "encode": {
        "enter": {"strokeWidth": {"value": 5}, "strokeCap": {"value": "round"}},
        "update": {
          "x2": {"scale": "xscale", "value": 2200, "band": 1},
          "x": {"scale": "xscale", "signal": "zero_category", "band": 1},
          "y": {"scale": "yscale", "value": 0, "offset": 0}
        }
      }
    },
    {
      "type": "rule",
      "from": {"data": "table-first"},
      "encode": {
        "enter": {"strokeWidth": {"value": 5}, "strokeCap": {"value": "round"}},
        "update": {
          "x2": {"scale": "xscale", "field": "category", "band": 1},
          "x": {"scale": "xscale", "field": "category", "band": 0},
          "y": {"scale": "yscale", "field": "amount", "offset": 0}
        }
      }
    },
    {
      "type": "symbol",
      "name": "zeros",
      "from": {"data": "seq"},
      "encode": {
        "enter": {
          "size": {"value": 500},
          "fill": {"value": "lightblue"},
          "stroke": {"value": "black"},
          "shape": {"value": "triangle"},
          "opacity": {"value": 0.8}
        },
        "update": {
          "x": {"scale": "xscale", "field": "data", "band": 1},
          "y": {"scale": "yscale", "value": "0", "offset": 0}
        }
      }
    },
    {
      "type": "text",
      "name": "results",
      "from": {"data": "results2"},
      "encode": {
        "enter": {
          "fontSize": {"value": 50},
          "fill": {"value": "red"},
          "stroke": {"value": "black"},
          "align": {"value": "center"},
          "fontWeight": {"value": "bold"},
          "text": {"signal": "datum.temperature_delta == 999 ? '-' : 'Δ T = ' + format(datum.temperature_delta, ',.2f')"},
        },
        "update": {
          "x": {"scale": "xscale", "field": "year_in", "band": 1},
          "y": {"scale": "yscale", "value": "-30", "offset": 0}
        }
      }
    },
    {
      "type": "symbol",
      "name": "baus",
      "from": {"data": "table"},
      "encode": {
        "enter": {
          "size": {"value": 500},
          "fill": {"value": "pink"},
          "stroke": {"value": "black"},
          "shape": {"value": "square"},
          "opacity": {"value": 0.8}
        },
        "update": {
          "x": {"scale": "xscale", "field": "category", "band": 1},
          "y": {"scale": "yscale", "field": "amount", "offset": 0}
        }
      }
    }
  ]
}
, {theme: theme, actions})



//viewof emissions = Inputs.input([])

mutable emissions_ = emissions.data('interpolated')

output (warning: not accurate)

Code
embed(
  calcuvizspec({
    models: [model],
    input_cursors: [{emissions_table_in: emissions_.map(d => ({year_in:d.data, emissions_rate:d.interpolated})),climate_sensitivity_in,ppm_to_GtC_in:2.3,drawdown_factor_in:0.001}],
    mark: {type: 'text', fontSize:15, fontWeight: 'bold'},
    encodings: {
      y: {name: 'year_in', type:'quantitative',zero: false, domain: [2015,2020,2050,2100], sort:'ascending'/*_.range(2015,2101,15)*/, format:'k'},
      x: {name: 'formula', type:'nominal', domain: ['temperature','temperature_delta', 'CO2_concentration','concentration_factor']},
      color: {legend: false, name: 'formula', type:'nominal', domain: ['temperature','temperature_delta', 'CO2_concentration', 'concentration_factor']},
      text: {name:'value', type:'quantitative', format: '.2f'},
      //color: {name:'value', type:'quantitative', format: ',.2f'}
    },
    width:340, height:500,
    /*spec_post_process: spec => {
            spec.params = [{"name": "xy", "select": { "type": "point", "on": "mouseover", "nearest": true, "encodings": ["x", "y"], toggle:false}}];
            let a = spec.encoding.x.axis; spec.encoding.x.axis = {...a, ticks:false, labels:false}
            a = spec.encoding.y.axis; spec.encoding.y.axis = {...a, ticks:false, labels:false}
            spec.encoding.color.scale = {scheme: 'turbo'}
      return spec;
    }*/
}), {theme: theme, actions})


calculang is free software licensed under AGPLv3

GitHub calculang/calculang Twitter @calculang Fosstodon @calculang

community gallery soon ⌛ - get in touch if you want to contribute a model!


calculang dev tools 🛠️🧰 (some of)

formula-inputs matrix

cul scope id graph

















appendix

|