default_example_id = q . get ( 'id' ) ?? ''
show_hidden = q . get ( 'hidden' ) == 'hidden' ?? false
a_thing2 = html `<select style="font-size: 1.5em">
${ examples . map (category => `<optgroup label=" ${ category . category } ">
${ category . tiles . filter (tile => (( ! tile . disabled ) || show_hidden)) . map (d => `<option value=" ${ d . id } " ${ d . id == default_example_id ? ' selected="selected"' : '' } }> ${ d . title } </option>` ) }
${ category . tiles . filter (d => d . id == default_example_id && d . disabled && ( ! show_hidden)) . map (d => `<option value=" ${ d . id } " ${ d . id == default_example_id ? ' selected="selected"' : '' } }> ${ d . title } </option>` ) }
</optgroup>
` ) } </select>`
md `* ${ example_details . subtitle } *`
cul_resources = {
if (cul_js_cul)
return Object . values (introspection_nomemo . cul_scope_ids_to_resource ) . map (loc => loc . slice ( 2 ) . slice ( 0 , loc . indexOf ( '?' ) != - 1 ? loc . indexOf ( '?' ) - 2 : 999 )) . map (d => d + ((d . slice ( - 3 ) != '.js' ) ? '.js' : '' )) . map (loc => dir + loc)
else return [ ... Array ( Object . keys (introspection_nomemo . cul_scope_ids_to_resource ) . length )] . map ((_ , i) => entrypoint_no_cul_js /* this is not modelname! this should be related to entrypoit, not id */ + (calculang_source_nomemo . length ? '-nomemo' : '' ) + '_esm/' + 'cul_scope_' + i + '.mjs' )
}
//cul_resources
output = {
let o = [] ;
cul_resources . forEach (loc => {
o . push ( fetch (loc) . then (d => d . text ()))
})
return o
}
// TODO replace duplicate (source) code by a message
calculang = [ await output[ 0 ] , cul_resources . length > 1 ? await output[ 1 ] : ''
, cul_resources . length > 2 ? await output[ 2 ] : ''
, cul_resources . length > 3 ? await output[ 3 ] : ''
, cul_resources . length > 4 ? await output[ 4 ] : ''
, cul_resources . length > 5 ? await output[ 5 ] : ''
, cul_resources . length > 6 ? await output[ 6 ] : '' // TODO warn when overflows
, cul_resources . length > 7 ? await output[ 7 ] : '' ] . filter (d => d != '' )
//calculang
// "x.cul.js formulae?"
fff = calculang . map (d => d . split ( ' \n ' )) . map (d => [d . findIndex (e => e . indexOf ( 'inputs:' + (options . includes ( 'inputs' ) ? 'BLAH' : '' )) != - 1 ) ?? 999 , ... d]) . map (d => d . filter ((e , i) => i <= d[ 0 ] || d[ 0 ] == - 1 )) . map (d => d . slice ( 1 )) . map (d => d . join ( ' \n ' )) . map ((d , i) => `***[ ${ introspection_nomemo . cul_scope_ids_to_resource [i] . split ( '?' )[ 0 ] . replaceAll ( '-nomemo' , '' ) } ]( ${ cul_resources[i] . replaceAll ( '-nomemo' , '' ) } ) ${ cul_js_cul ? `-> [js]( ${ entrypoint_no_cul_js } _esm/cul_scope_ ${ i } .mjs)` : `` } ***
~~~js
${ d . replaceAll ( 'export const ' + (options . includes ( 'export consts' )
? 'BLAH' : '' ) , '' ) }
~~~
`
)
md `<br/>**inputs ⚙️** \`${ inputs . filter (d => fns0 . includes (d)) . join ( ' \` , \` ' ) }\`${ not_inputs . length ? ' | ~~ \` ' : '' }${ not_inputs . map ((d , i) => ` ${ d }\` ~~ ${ i != not_inputs . length - 1 ? ', ~~ \` ' : '' } ` ) . join ( '' ) } `
not_inputs = inputs . filter (d => ! fns0 . includes (d))
dot ` ${ scope_id_graph } `
md ` ${ fff . join ( ' \n\n ' ) } `
viewof options = Inputs . checkbox ([ "export consts" , "inputs" /*, "lists (todo)"*/ ] , { label : "unhide?" , value : [ "" ]})
//md`---`
viewof cul_js = Inputs . radio ([ "📝 calculang source 💬" , "✨ JS/ESM output ✨" ] , { label : "🫶" , value : "📝 calculang source 💬" })
cul_js_cul = cul_js == "📝 calculang source 💬" ? true : false
viewof calculang_source_nomemo = Inputs . checkbox ([ "nomemo" ] , { value : [ "nomemo" ] , label : 'see memo? 💀' , disabled : cul_js == "📝 calculang source 💬" })
import {Tangle} from "@declann/colored-tangle"
viewof i_start = Inputs . input ( 1 )
md `*starting i*: ${ Inputs . bind (Inputs . range ([ - 10 , 150 ] , { step : 1 , width : 50 }) , viewof i_start) } `
embed (
calcuvizspec ({
models : modelname == 'fizzbuzz' ? [model] : [] ,
input_cursors : [{}] ,
mark : { type : 'text' , fontSize : 15 , fontWeight : 'bold' } ,
encodings : {
x : { name : 'formula' , type : 'nominal' , domain : [ 'fizz' , 'buzz' , 'fizzbuzz' ] , axis : { labelFontSize : 60 } , sort : [ 'fizzbuzz' ]} ,
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 . background = "#fff" ; spec . encoding . y . axis = { grid : true } ; return spec ; }
}))
viewof leap_year_start = Inputs . input ( 1990 )
md `*starting year*: ${ Inputs . bind ( Tangle ({ min : - 1000 , max : 5000 , step : 1 , power : 1.8 , color : 'darkblue' }) , viewof leap_year_start) } `
import { calcuvegadata } from "baafa4b071a5b66a" // draft API!
leap_year_spec = ({
background : 'white' ,
"data" : { "name" : "data" } ,
"datasets" : { "data" : leap_year_data0} ,
"vconcat" : [{
"width" : 480 ,
"mark" : { type : "line" , point : true } ,
"encoding" : {
"x" : {
"field" : "year_in" ,
"type" : "quantitative" ,
"scale" : { "domain" : { "param" : "brush" } , zero : false , nice : false } ,
"axis" : { "title" : "" , format : ".0f" , grid : false , format : '.0f' , ticks : true }
} ,
"y" : { "field" : "is_leap_year" , "type" : "quantitative" ,
scale : { domain : [ - 0.1 , 1.1 ]} ,
"axis" : { format : ".0f" , grid : false , values : [ 0 , 1 ] , format : '.0f' , ticks : true }
} ,
"order" : { "field" : "year_in" } ,
"tooltip" : { field : "year_in" }
}
} , {
"width" : 480 ,
"height" : 60 ,
"mark" : { type : "line" , size : 0.1 } ,
"params" : [{
"name" : "brush" ,
value : { x : [leap_year_start , leap_year_start + 50 ]} , // initial value here fails whenever I switch to this model after loading a different one ... understand
//"value": {"year_in": [2000, 2010]},
"select" : { "type" : "interval" , "encodings" : [ "x" ]}
}] ,
"encoding" : {
"x" : {
"field" : "year_in" ,
"type" : "quantitative" ,
scale : { zero : false , nice : false } ,
axis : { format : ".0f" , grid : false , tickCount : 12 , ticks : true }
} ,
"y" : {
"field" : "is_leap_year" ,
"type" : "quantitative" ,
"axis" : { "grid" : false , values : [ 0 , 1 ] , format : '.0f' }
}
}
}]
})
leap_year_data = calcuvegadata ({
models : modelname == 'leap-year' ? [model] : [] ,
spec : leap_year_spec ,
domains : { year_in : _ . range (leap_year_start , leap_year_start + 1001 ) } ,
input_cursors : [{}]
})
viewof leap_year_viz = embed (leap_year_spec)
{
leap_year_viz . data ( "data" , leap_year_data) . resize () . run () ;
}
mutable leap_year_data0 = [] // includes data in spec for when I Open in Vega Editor
htl . html `<button onclick= ${ () => (mutable leap_year_data0 = leap_year_data) } >insert data -> vega editor (dev)</button>`
viewof frame_in = Scrubber (_ . range ( 0 , 60 * 5 ) , { autoplay : false , delay : 50 , label : 'frame' , initial : 2 })
viewof speed_in = Inputs . range ([ - 3 , 15 ] , { value : 3 , label : 'speed' , step : 0.01 })
starfield_spec = ({
background : 'black' , //'lightblue',
"data" : { "name" : "data" } ,
"datasets" : { "data" : starfield_data0} ,
"height" : 300 , width : 300 ,
layer : [{
"mark" : { type : "point" , tooltip : true , clip : false } ,
/*"params": [{
"name": "brush",
value: { x: [leap_year_start, leap_year_start+50]}, // initial value here fails whenever I switch to this model after loading a different one ... understand
//"value": {"year_in": [2000, 2010]},
"select": {"type": "interval", "encodings": ["x"]}
}],*/
"encoding" : {
"x" : {
"field" : "x" ,
"type" : "quantitative" ,
scale : { zero : false , nice : false , domain : [ - 50 , 50 ]} ,
//axis: {format:".0f", grid: false, tickCount:12, ticks:true }
} ,
"y" : {
"field" : "y" ,
"type" : "quantitative" ,
scale : { zero : false , nice : false , domain : [ - 50 , 50 ]} ,
//"axis": { "grid": false, values: [0,1], format: '.0f'}
} ,
"size" : {
"field" : "z" ,
"type" : "quantitative" ,
scale : { reverse : true } ,
legend : false
//scale: {zero:false, nice: false, domain:[-50,50]},
//"axis": { "grid": false, values: [0,1], format: '.0f'}
} ,
color : { value : 'white' } , //opacity: {value:1}
}
} ,
{
mark : 'rule' ,
encoding : {
"x" : {
"field" : "x" ,
"type" : "quantitative" ,
scale : { zero : false , nice : false , domain : [ - 50 , 50 ]} ,
//axis: {format:".0f", grid: false, tickCount:12, ticks:true }
} ,
"y" : {
"field" : "y" ,
"type" : "quantitative" ,
scale : { zero : false , nice : false , domain : [ - 50 , 50 ]} ,
//"axis": { "grid": false, values: [0,1], format: '.0f'}
} ,
"x2" : {
"field" : "px" ,
"type" : "quantitative" ,
scale : { zero : false , nice : false , domain : [ - 50 , 50 ]} ,
//axis: {format:".0f", grid: false, tickCount:12, ticks:true }
} ,
"y2" : {
"field" : "py" ,
"type" : "quantitative" ,
scale : { zero : false , nice : false , domain : [ - 50 , 50 ]} ,
//"axis": { "grid": false, values: [0,1], format: '.0f'}
} ,
color : { value : 'white' }
}
}]
})
starfield_data = calcuvegadata ({
models : modelname == 'starfield' ? [model] : [] ,
spec : starfield_spec ,
domains : { star_in : _ . range ( 0 , 100 ) } ,
input_cursors : [{speed_in , frame_in , random_seed_in : 'hello' }]
})
viewof starfield_viz = embed (starfield_spec)
{
starfield_viz . data ( "data" , starfield_data) /*.resize()*/ . run () ;
}
mutable starfield_data0 = [] // includes data in spec for when I Open in Vega Editor
htl . html `<button onclick= ${ () => (mutable starfield_data0 = starfield_data) } >insert data -> vega editor (dev)</button>`
maze_in = [
[ 33 , 59 , 92 , 97 , 80 , 25 , 71 , 67 , 29 , 36 , 76 , 22 , 58 , 14 , 42 ] ,
[ 26 , 2 , 87 , 64 , 40 , 76 , 70 , 75 , 24 , 19 , 94 , 86 , 87 , 40 , 25 ] ,
[ 26 , 21 , 57 , 63 , 100 , 96 , 21 , 82 , 22 , 93 , 15 , 57 , 49 , 81 , 73 ] ,
[ 68 , 55 , 11 , 67 , 46 , 73 , 44 , 32 , 84 , 100 , 51 , 87 , 18 , 75 , 82 ] ,
[ 80 , 41 , 36 , 11 , 43 , 74 , 39 , 93 , 35 , 53 , 73 , 59 , 27 , 8 , 30 ] ,
[ 28 , 86 , 21 , 75 , 69 , 27 , 41 , 88 , 84 , 83 , 75 , 90 , 60 , 11 , 12 ] ,
[ 77 , 87 , 0 , 59 , 35 , 5 , 79 , 37 , 45 , 2 , 29 , 4 , 99 , 4 , 43 ] ,
[ 55 , 91 , 69 , 24 , 9 , 67 , 84 , 100 , 99 , 41 , 29 , 62 , 65 , 27 , 87 ] ,
[ 30 , 27 , 12 , 86 , 21 , 8 , 40 , 81 , 81 , 39 , 56 , 82 , 40 , 36 , 79 ] ,
[ 72 , 6 , 94 , 64 , 51 , 42 , 97 , 93 , 74 , 1 , 70 , 97 , 68 , 42 , 72 ] ,
[ 0 , 16 , 20 , 11 , 1 , 59 , 48 , 41 , 87 , 51 , 53 , 35 , 25 , 46 , 21 ] ,
[ 34 , 57 , 0 , 98 , 86 , 30 , 69 , 25 , 82 , 84 , 86 , 2 , 17 , 67 , 25 ] ,
[ 88 , 100 , 23 , 72 , 12 , 73 , 51 , 47 , 4 , 61 , 44 , 44 , 49 , 53 , 27 ] ,
[ 95 , 31 , 31 , 39 , 55 , 64 , 61 , 52 , 41 , 33 , 86 , 0 , 40 , 48 , 90 ] ,
[ 79 , 3 , 62 , 88 , 27 , 93 , 29 , 0 , 91 , 24 , 17 , 49 , 2 , 68 , 33 ] ,
] ;
embed (
calcuvizspec ({
models : modelname == 'maze' ? [model] : [] ,
input_cursors : [{maze_in}] ,
mark : { type : 'text' , fontSize : 11 , fontWeight : 'bold' } ,
encodings : {
row : { name : 'formula' , type : 'nominal' , domain : [ 'value' , 'max' , 'arrow' , 'arrow_path' , 'arrow_path_pretty' ] , axis : { labelFontSize : 60 } , sort : [ 'value' ]} ,
x : { name : 'x_in' , type : 'nominal' , domain : _ . range ( 0 , 15 ) , axis : { grid : false }} ,
y : { name : 'y_in' , type : 'nominal' , domain : _ . range ( 0 , 15 ) , axis : { grid : false }} ,
text : { name : 'value' , type : 'nominal' } ,
//color: {name: 'value', type: 'quantitative', independent:true, legend:false},
} ,
width : 400 , // height:50,
//spec_post_process: spec => { spec.background = 'white'; spec.encoding.color.scale = {scheme:'viridis'}; return spec;}
spec_post_process : spec => { spec . encoding . row . header = { labelLimit : 100 } ; spec . background = 'white' ; spec . encoding . color = { value : 'darkblue' } ; return spec ; }
}))
viewof num_snowflakes = Inputs . range ([ 0 , 100 ] , { label : '# snowflakes' , value : 20 , step : 1 })
viewof w_in = Inputs . range ([ 0 , 2 ] , { label : 'angular speed (w_in)' , value : 0.6 , step : 0.01 })
viewof height_in = Inputs . range ([ 100 , 500 ] , { label : 'height' , value : 200 , step : 20 })
viewof f_in = Scrubber (_ . range ( 0 , 60 * 100 ) , { autoplay : false , delay : 10 , label : 'frame' })
embed (
calcuvizspec ({
models : modelname == 'snowflakes' ? [model] : [] ,
input_cursors : [{f_in , w_in , width_in : 400 , height_in}] ,
mark : { type : 'text' , clip : true } ,
encodings : {
x : { name : 'x' , type : 'quantitative' , scale : { domain : [ 0 , 400 ] } } ,
y : { name : 'y' , type : 'quantitative' , sort : 'descending' , scale : { domain : [ 0 , height_in] } } ,
size : { name : 'size' , type : 'quantitative' , legend : false } ,
angle : { name : 'x' , type : 'quantitative' , scale : { range : [ 0 , 300 ] } } ,
// different snowflakes:
detail : { name : 'p_in' , type : 'nominal' , domain : _ . range ( 0 , num_snowflakes , 1 ) } ,
} ,
width : 300 ,
height : 260 ,
// viz customizations:
spec_post_process : spec => {
spec . encoding . text = { value : '❄️' }
spec . background = 'darkred' ;
return spec
}
})
, { theme : 'vox' , renderer : 'canvas' })
embed (
calcuvizspec ({
models : modelname == 'snowflakes' ? [model] : [] ,
input_cursors : [{ w_in , width_in : 400 , height_in}] ,
mark : { type : 'line' , point : true , strokeWidth : '1px' } ,
encodings : {
x : { name : 'f_in' , type : 'quantitative' , domain : _ . range ( 0 , 501 , 5 ) } ,
y : { name : 'value' , type : 'quantitative' , independent : true } ,
color : { name : 'p_in' , type : 'nominal' , domain : [ 0 , 1 , snowflake_in] } ,
row : { name : 'formula' , type : 'nominal' , domain : [ 'x' , 'y' , 'angle' , 'size' , 'radius' , 'initial_angle' ] , sort : 'descending' } ,
} ,
width : 200 ,
height : 30 ,
// viz customizations:
spec_post_process : spec => {
spec . config = { point : { size : 10 } }
spec . encoding . y . title = null ;
return spec
}
})
, {})
viewof snowflake_in = Inputs . range ([ 0 , num_snowflakes] , { value : 10 , label : 'chosen extra ❄️' , step : 1 })
viewof petal_num_in = Inputs . range ([ 0 , 50 ] , { label : '# petals' , value : 7 , step : 1 })
viewof petal_size_in = Inputs . range ([ 0 , 50 ] , { label : 'petal size' , value : 30 , step : 1 })
viewof petals_roundy_spikiness_in = Inputs . range ([ 0 , 2 ] , { label : 'petals roundy/spikiness' , value : 0.5 , step : 0.1 })
viewof flower_big = Inputs . toggle ({ label : 'big?' , value : false })
embed (
calcuvizspec ({
models : modelname == 'flower' ? [model] : [] ,
input_cursors : [{petal_num_in , petal_size_in , petals_roundy_spikiness_in}] ,
mark : 'rect' , // heatmap
encodings : {
x : { name : 'column_in' , type : 'nominal' , domain : _ . range ( 0 , 101 , 1 ) } ,
y : { name : 'row_in' , type : 'nominal' , domain : _ . range ( 0 , 101 , 1 ) } ,
color : { 'name' : 'value' , type : 'quantitative' , scale : { scheme : 'brownbluegreen' , reverse : true } , legend : false , } ,
row : {
name : 'formula' , sort : 'descending' ,
domain : [ 'result' , 'petals' , 'head' , 'face' ]
}
} ,
width : 300 * (flower_big ? 1.5 : 0.5 ) ,
height : 250 * (flower_big ? 1.5 : 0.5 ) ,
// viz customizations:
spec_post_process : spec => {
spec . encoding . x . axis = { grid : false , domain : false , ticks : 0 , labels : false }
spec . encoding . y . axis = { grid : false , domain : false , ticks : 0 , labels : false }
return spec
}
})
)
viewof timing_in = Inputs . select ([ "start of today" , "start of tomorrow" ] , { label : 'from' })
md `** ${ (d3 . format ( '.2%' )(model . fractionLeftInYear ({ date_in : new Date () , timing_in}))) } ** of year reamining, or visually:`
Plot . plot ({
//marginLeft: 90,
height : 80 ,
//width:5000,
color : { legend : true } ,
marks : [ Plot . axisX ({ anchor : "top" }) ,
, Plot . barX (
[{ sort : 1 , status : 'remaining' , percent : 100 * model . fractionLeftInYear ({ date_in : new Date () , timing_in})} ,
{ sort : 0 , status : 'complete' , percent : 100 * model . fractionIntoYear ({ date_in : new Date () , timing_in})
}]
, { x : "percent" , fill : "status" , sort : 'sort' })]
})
embed (
calcuvizspec ({
models : modelname == 'monte-carlo-pension-calculator' ? [model] : [] ,
input_cursors : [{
random_seed_in : "something really random" ,
"age_0_in" : 30 ,
"fund_value_0_in" : 0 ,
"retirement_age_in" : 65 ,
"missed_contribution_age_in" : 0 ,
"salary_age_0_in" : 30 ,
"salary_0_in" : 30000 ,
"salary_inflation_rate_in" : 0.02 ,
"empee_contribution_rate_in" : 0.1 ,
"emper_matching_rate_in" : 1 ,
"contribution_charge_rate_in" : 0.1 ,
"management_charge_rate_in" : 0.01 }] ,
mark : { type : 'text' , fontSize : 20 , fontWeight : 'bold' } ,
encodings : {
x : { name : 'formula' , type : 'nominal' , domain : [ 'retirement_fund_value' ] , axis : { labelFontSize : 60 }} ,
y : { name : 'simulation_in' , type : 'ordinal' , domain : _ . range ( 0 , 10 , 1 )} ,
text : { name : 'value' , type : 'quantitative' , format : ',.0f' } ,
//color: {name: 'formula', type:'nominal', domain: formulae_not_inputs, legend:false},
} ,
width : 90 , // height:50,
}))
embed (
calcuvizspec ({
models : modelname == 'monte-carlo-pension-calculator' ? [model] : [] ,
input_cursors : [{
random_seed_in : "something really random" ,
"age_0_in" : 30 ,
"fund_value_0_in" : 0 ,
"retirement_age_in" : 65 ,
"missed_contribution_age_in" : 0 ,
"salary_age_0_in" : 30 ,
"salary_0_in" : 30000 ,
"salary_inflation_rate_in" : 0.02 ,
"empee_contribution_rate_in" : 0.1 ,
"emper_matching_rate_in" : 1 ,
"contribution_charge_rate_in" : 0.1 ,
"management_charge_rate_in" : 0.01 }] ,
mark : { type : 'line' , point : false , fontSize : 20 , fontWeight : 'bold' } ,
encodings : {
//x: {name: 'formula', type:'nominal', domain: ['retirement_fund_value'], axis: {labelFontSize: 60}},
x : { name : 'simulation_in' , type : 'quantitative' , domain : _ . range ( 0 , 100 , 1 )} ,
y : { name : 'retirement_fund_value' , type : 'quantitative' , format : ',.0f' } ,
//color: {name: 'formula', type:'nominal', domain: formulae_not_inputs, legend:false},
} ,
width : 300 , height : 100 ,
}))
embed (
calcuvizspec ({
models : modelname == 'monte-carlo-pension-calculator' ? [model] : [] ,
input_cursors : [{
random_seed_in : "something really random" ,
"age_0_in" : 30 ,
"fund_value_0_in" : 0 ,
"retirement_age_in" : 65 ,
"missed_contribution_age_in" : 0 ,
"salary_age_0_in" : 30 ,
"salary_0_in" : 30000 ,
"salary_inflation_rate_in" : 0.02 ,
"empee_contribution_rate_in" : 0.1 ,
"emper_matching_rate_in" : 1 ,
"contribution_charge_rate_in" : 0.1 ,
"management_charge_rate_in" : 0.01 }] ,
mark : { type : 'line' , point : false , fontSize : 20 , fontWeight : 'bold' } ,
encodings : {
//x: {name: 'formula', type:'nominal', domain: ['retirement_fund_value'], axis: {labelFontSize: 60}},
color : { name : 'simulation_in' , type : 'nominal' , domain : _ . range ( 0 , 100 , 1 )} ,
x : { name : 'age_in' , type : 'quantitative' , domain : _ . range ( 20 , 70 , 1 )} ,
y : { name : 'fund_value' , type : 'quantitative' , format : ',.0f' } ,
//color: {name: 'formula', type:'nominal', domain: formulae_not_inputs, legend:false},
} ,
width : 300 , height : 400 ,
}))
viewof s = Inputs . toggle ({ value : false , label : "persist interaction 🔨" })
viewof form = {
let form = Inputs . form ({
age_0_in : Inputs . range ([ 18 , 65 ] , { label : 'starting age' , value : q . get ( 'age_0_in' ) ?? 50 , step : 1 }) ,
retirement_age_in : Inputs . range ([ 50 , 75 ] , { label : 'retirement age' , value : 65 , step : 1 }) ,
fund_value_0_in : Inputs . range ([ 0 , 1000000 ] , { label : 'starting fund value' , value : 0 , step : 1000 }) ,
empee_contribution_rate_in : Inputs . range ([ 0.00 , 0.30 ] , { label : 'empee contribution as % of salary' , value : 0.1 , step : 0.01 }) ,
emper_matching_rate_in : Inputs . range ([ 0.00 , 2 ] , { label : 'emper matching rate' , value : q . get ( 'emper_matching_rate_in' ) ?? 1 , step : 0.02 }) ,
salary_0_in : Inputs . range ([ 5000 , 300000 ] , { label : 'salary' , value : q . get ( 'salary_0_in' ) ?? 50000 , step : 1000 }) ,
salary_age_0_in : Inputs . range ([ 18 , 65 ] , { label : 'salary ref. age' , value : q . get ( 'salary_age_0_in' ) ?? 50 , step : 1 }) ,
salary_inflation_rate_in : Inputs . range ([ - 0.02 , 0.10 ] , { label : 'salary growth rate' , value : 0.02 , step : 0.002 }) ,
unit_growth_rate_in : Inputs . range ([ - 0.02 * 0 /*banned (along with neg charges) for technical reasons with the visual !!*/ , 0.10 ] , { label : 'investment growth rate' , value : q . get ( 'unit_growth_rate_in' ) ?? 0.05 , step : 0.002 }) ,
contribution_charge_rate_in : Inputs . range ([ - 0.02 * 0 , 0.10 ] , { label : 'contribution charge rate' , value : 0.04 , step : 0.01 }) ,
management_charge_rate_in : Inputs . range ([ - 0.02 * 0 , 0.10 ] , { label : 'management charge rate' , value : 0.01 , step : 0.002 }) ,
missed_contribution_age_in : Inputs . range ([ 15 , 75 ] , { label : 'Missed contribution (scenario)' , value : 15 , step : 1 }) ,
} , {template}) ;
let state = false ; // need to avoid circ. defn.
form . oninput = (d) => { console . log ( 'oninput' , d) ; if (state == false ) {mutable inputs_history = [ ... mutable inputs_history , form . value ] ; state = true ; mutable statey = true ; mutable changing = d . srcElement . parentElement . previousElementSibling . textContent } mutable inputs_history = [ ... mutable inputs_history . slice ( 0 ,- 1 ) , form . value ]}
form . onchange = () => { console . log ( 'onchange' ) ; state = false ; mutable statey = s /* false */ ; mutable inputs_history[mutable inputs_history . length - 1 ] = form . value } ;
return form ;
}
md `**retirement fund projection = <span style="color:darkorange"><span style="font-size:2em">€ ${ f (new_value) } **</span></span> <span style="color: ${ good_guy ? 'green' : 'red' } ; font-size:13px">** ${ statey ? (good_guy ? '↑' : '↓' ) + f ( Math . abs (new_value - old_value)) : ' ' } **</span>
*The retirement fund projection is the **pre-retirement area under inflows less the pre-retirement area under outflows** below.*`
{
if ( ! statey) return md `.................................................................................................................................................................... ....................................................................................................................................................................` ;
else return md `impact <span style="color: ${ good_guy ? 'green' : 'red' } ">€** ${ statey ? (good_guy ? '↑' : '↓' ) + f ( Math . abs (new_value - old_value)) : ' ' } **</span> due to **Δ ${ changing } ** ( ${ d3 . format ( ',.2f' )(old_in) } -> ${ d3 . format ( ',.2f' )(new_in) } ) is visualised as **<span style="color:green">green boxes</span> - <span style="color:red">red boxes</span>** (inflows and outflows). 📦📦`
}
data_all2 = calcudata ({
models : modelname == 'pension-calculator' ? [model] : [] ,
input_domains : { age_in : _ . range ( Math . min (inputs_history[input_cursor_id] . age_0_in , inputs_history[input_cursor_id - 1 ] . age_0_in ) - 1 , Math . max (inputs_history[input_cursor_id] . retirement_age_in , inputs_history[input_cursor_id - 1 ] . retirement_age_in ) + addl_years , 1 )} ,
input_cursors : inputs_history . map (d => ({ ... d , /*age_in:d.retirement_age_in*/ /* TODO hacky? */ })) ,
outputs : [ ... formulae , 'check_cfs_vs_fund_value' ] ,
pivot : false
}) //.map(d => d.formula == 'fund_charges' ? ({...d, value:-d.value}) : d) // TEMP9
. filter (d => d . input_cursor_id == input_cursor_id || (d . input_cursor_id == input_cursor_id - 1 && statey))
// check_cfs_vs_fund_value is weakened as a check
embed ({
"$schema" : "https://vega.github.io/schema/vega/v5.json" ,
//"background": "#eef",
"padding" : 5 ,
"width" : 400 ,
//"height": 500,
"style" : "cell" ,
signals : [ { "name" : "offset" , "value" : 80 } ,
{ "name" : "cellHeight" , "value" : 150 } ,
{ "name" : "height" , "update" : "2 * (offset + cellHeight)" }
] ,
"data" : [
{
"name" : "data" ,
"values" : data_all2 ,
"transform" : [
{
type : 'formula' ,
//expr: 'datum.value >=0', This is bloody hard to debug and do correctly, and must be correct for posneg swaps etc. So just banning negative charges for now and will fix this later!
expr : 'indexof(["management_charge","contribution_charge"],datum.formula) == -1' ,
as : 'posneg'
} ,
{
type : 'formula' , expr : 'abs(datum.value)' , as : 'value'
} ,
{
"type" : "stack" ,
"groupby" : [ "age_in" ] ,
"field" : "value" ,
"sort" : { "field" : [ "formula" , "input_cursor_id" ] , "order" : [ "ascending" , "descending" ]} ,
"as" : [ "value1_start" , "value1_end" ] ,
"offset" : "zero"
} ,
]
} ,
{
name : 'data_old' ,
source : 'data' ,
"transform" : [
{
type : 'filter' ,
expr : `datum.input_cursor_id == ${ input_cursor_id - 1 } ` , // eliminates chk
} ,
{
"type" : "stack" ,
"groupby" : [ "age_in" ] ,
"field" : "value" ,
"sort" : { "field" : [ "formula" , "input_cursor_id" ] , "order" : [ "ascending" , "descending" ]} ,
"as" : [ "value1_start" , "value1_end" ] ,
"offset" : "zero"
} ,
]}
] ,
scales : [ {
"name" : "gscale" ,
"type" : "band" ,
"range" : [ 0 , { "signal" : "height" }] ,
"round" : true ,
"domain" : {
"data" : "data" ,
"field" : "posneg" ,
/*"sort": {
"field": "posneg",
"op": "median",
"order": "descending"
}*/
}
} ,
{
"name" : "y_shared" ,
"type" : "linear" ,
domainMin : '0' , // domainMax: '60000', // doesn't generalise
"domain" : { "data" : statey ? "data_old" : 'data' , "fields" : [ "value1_start" , "value1_end" ]} , // give this a cushion ??
"range" : [{ "signal" : "cellHeight" } , 0 ] ,
"nice" : false ,
"zero" : true ,
} ,
{
"name" : "y_bad" , // how to share y scale on the stack total ?!
"type" : "linear" ,
domainMin : '0' , domainMax : { signal : "domain('yyy')[1]*2" } , // doesn't generalise. Can I not x2 or update when dragging?
"domain" : { "data" : "data" , "fields" : [ "value1_start" , "value1_end" ]} , // give this a cushion ??
"range" : [{ "signal" : "cellHeight" } , 0 ] ,
"nice" : false ,
"zero" : true ,
} ,
{
"name" : "color" ,
"type" : "ordinal" ,
"domain" : { "data" : "data" , "field" : "formula" , "sort" : [ "empee_contribution_cost" , "check_cfs_vs_fund_value" ]} ,
"range" : "category"
} ,
] ,
"marks" : [
{
type : 'group' ,
name : 'posneg' ,
from : {
facet : {
data : 'data' ,
name : 'posnegs' ,
groupby : 'posneg'
}
} ,
data : [{ name : 'data_windowed' , source : 'posnegs' , transform : [
{
"type" : "window" ,
"params" : [ null ] ,
"as" : [ "B_value" ] ,
"ops" : [ "lag" ] ,
"fields" : [ "value" ] ,
"sort" : {
"field" : [ "input_cursor_id" ] ,
"order" : [ "descending" ]
} ,
"groupby" : [ "formula" , "age_in" ] ,
"frame" : [ - 1 , 0 ]
} ,
{
"type" : "formula" ,
"expr" : "datum.value-datum.B_value" ,
"as" : "impact"
} ,
{ "type" : "formula" , "expr" : `datum.value > datum.B_value ? 0 : datum.B_value - datum.value` , "as" : "adj" } , // I need to record the adj in id-1 for use in calc for id w. a follow-up window fn !
{ "type" : "formula" , "expr" : `datum.input_cursor_id == ${ input_cursor_id - 1 } ? datum.value > datum.B_value ? datum.value - datum.B_value : datum.B_value - datum.value : datum.value` , "as" : "BplusAminusB" } , // I need to record the adj in id-1 for use in calc for id w. a follow-up window fn !
{ "type" : "formula" , "expr" : `datum.adj` , "as" : "adjposneg" } ,
{
"type" : "window" ,
"params" : [ null ] ,
"as" : [ "B_adj" ] ,
"ops" : [ "lag" ] ,
"fields" : [ "adj" ] ,
"sort" : {
"field" : [ "input_cursor_id" ] ,
"order" : [ "ascending" ]
} ,
"groupby" : [ "formula" , "age_in" ] ,
"frame" : [ - 1 , 0 ]
} ,
{
"type" : "formula" ,
"expr" : "datum.BplusAminusB-datum.B_adj" ,
"as" : "BplusAminusB"
} ,
{ "type" : "filter" , "expr" : "abs(datum.BplusAminusB)>=0.01" } ,
]} ,
{
"name" : "data_0" ,
"source" : "data_windowed" ,
"transform" : [
{
"type" : "stack" ,
"groupby" : [ "age_in" ] ,
"field" : "BplusAminusB" ,
"sort" : { "field" : [ "formula" , "input_cursor_id" ] , "order" : [ "ascending" , "descending" ]} ,
"as" : [ "value_start" , "value_end" ] ,
"offset" : "zero"
} ,
{ type : 'formula' , 'expr' : 'abs(datum.value_end)' , as : 'abs_value_end' } ,
{ type : 'formula' , 'expr' : 'abs(datum.value_start)' , as : 'abs_value_start' } ,
/*{
"type": "filter",
"expr": "isValid(datum[\"age_in\"]) && isFinite(+datum[\"age_in\"]) && isValid(datum[\"value\"]) && isFinite(+datum[\"value\"])"
}*/
]
}] ,
"encode" : {
"enter" : {
"y" : { "scale" : "gscale" , "field" : "posneg" , "offset" : { "signal" : "offset" }} ,
"height" : { "signal" : "cellHeight" } ,
"width" : { "signal" : "width" } ,
"stroke" : { "value" : "#ccc" }
}
} ,
marks : [
{
"name" : "marks" ,
"type" : "rect" ,
"style" : [ "bar" ] ,
"from" : { "data" : "data_0" } ,
"encode" : {
"update" : {
"stroke" : { "signal" : `datum.posneg ? (datum.impact > 0 ? 'red' : 'green') : (datum.impact > 0 ? 'green' : 'red')` } ,
//"stroke": { "signal": `datum.posneg ? (datum.input_cursor_id == ${input_cursor_id} ? 100 : -100) : (datum.input_cursor_id == ${input_cursor_id} ? -100 : 100)`},
"strokeOpacity" : { "value" : 1 } ,
"tooltip" : {
"signal" : "{formula: datum['formula'], value: format(datum.value, ',.2f'), Age: datum['age_in']}"
} ,
"fill" : { "scale" : "color" , "field" : "formula" } ,
"opacity" : { "scale" : "opacity" , "field" : "input_cursor_id" } ,
"strokeWidth" : { "scale" : "strokeWidth" , "field" : "input_cursor_id" } ,
"ariaRoleDescription" : { "value" : "bar" } ,
"description" : {
"signal" : " \" age_in: \" + (format(datum[ \" age_in \" ], \"\" )) + \" ; value: \" + (format(datum[ \" value \" ], \"\" )) + \" ; formula: \" + (isValid(datum[ \" formula \" ]) ? datum[ \" formula \" ] : \"\" +datum[ \" formula \" ])"
} ,
"x" : { "scale" : "x" , "field" : "age_in" } ,
"width" : { "signal" : "max(0.25, bandwidth('x'))" } ,
"y" : { "scale" : y , "field" : "abs_value_end" } ,
"y2" : { "scale" : y , "field" : "abs_value_start" }
}
}
}
] ,
"scales" : [ {
"name" : "y" ,
"type" : "linear" ,
domainMin : '0' , // domainMax: '60000', // doesn't generalise
"domain" : { "data" : 'data_0' , "fields" : [ "abs_value_start" , "abs_value_end" ]} , // give this a cushion ??
"range" : [{ "signal" : "cellHeight" } , 0 ] ,
"nice" : false ,
"zero" : true ,
} ,
{
"name" : "x" ,
"type" : "band" ,
"domain" : { "data" : "data_0" , "field" : "age_in" , "sort" : true } ,
"range" : [ 0 , { "signal" : "width" }] ,
"paddingInner" : 0.2 ,
"paddingOuter" : 0.1
} ,
{ name : 'opacity' , type : 'ordinal' , "domain" : [input_cursor_id - 1 , input_cursor_id] , range : [ 1 , statey ? 0.3 : 1 ]} ,
{ name : 'strokeWidth' , type : 'ordinal' , "domain" : [input_cursor_id , input_cursor_id - 1 ] , range : [ 0 , 2 ]} ,
{
"name" : "threshold" ,
"type" : "threshold" ,
"domain" : [ - 99999999 , 1 ] ,
"range" : [ "green" , "red" , "green" ]
}
] ,
"axes" : [
{
"scale" : "x" ,
"orient" : "bottom" ,
"grid" : false ,
//"title": "age_in",
//"labelAlign": "center",
//"labelAngle": -10,
"labelBaseline" : "top" ,
"zindex" : 0 ,
ticks : true ,
values : _ . range ( 15 , 105 , 5 )
} ,
{
"scale" : y ,
"orient" : "left" ,
"grid" : true ,
gridOpacity : 0.2 ,
"title" : ` ${ statey ? 'Δ ' : '' } fund value` ,
"labelOverlap" : true ,
"tickCount" : 5 ,
"zindex" : 0
}
] ,
} ,
{
"type" : "text" ,
"from" : { "data" : "posneg" } ,
"encode" : {
"enter" : {
"x" : { "field" : "width" , "mult" : 0.5 } ,
"y" : { "field" : "y" , offset :- 25 } ,
"fontSize" : { "value" : 31 } ,
opacity : { value : 0.7 } ,
//"fontWeight": {"value": "bold"},
"text" : { "signal" : "datum.datum.posneg ? 'Inflows 💰' : 'Outflows 💸' " } ,
"align" : { "value" : "center" } ,
"baseline" : { "value" : "top" } ,
"fill" : { "value" : "#000" }
}
}
}] ,
"legends" : [{ "fill" : "color" , "symbolType" : "square" , "title" : "formula" }] ,
"config" : {
"legend" : { "orient" : "right" , "layout" : { "right" : { "anchor" : "middle" }}}
}
})
viewof shared_y_scale = Inputs . toggle ({ label : 'shared y scale?' , value : true })
md `<br/>`
DOM . download ( serialize (data_all2) , "pension_savings_calculator.csv" , "csv download" )
md `<br/>`
DOM . download ( new Blob ([unpivot . toCSV ()] , { type : "text/csv" }) , "pension_savings_calculator_unpivot.csv" , "csv download (pivoted)" )
old_in = inputs_history[inputs_history . length - 2 ][changing_i_in]
new_in = inputs_history[inputs_history . length - 1 ][changing_i_in]
mutable changing = ''
changing_i = Array . from (viewof form . children . item ( 0 ) . children ) . map ((d , i) => ({d , i})) . find (e => e . d . textContent . includes (changing)) . i
changing_i_in = Object . entries (viewof form . value )[changing_i][ 0 ]
formulae = [ 'empee_contribution_tax_relief' , 'empee_contribution_cost' , 'emper_contribution' , 'fund_growth' , 'contribution_charge' , 'management_charge' ]
y = shared_y_scale ? 'y_shared' : 'y'
f = d3 . format ( ',.2f' )
mutable statey = false ;
mutable inputs_history = [ JSON . parse ( `{"age_0_in": ${ q . get ( 'age_0_in' ) ?? 50 } ,"fund_value_0_in":0,"unit_growth_rate_in": ${ q . get ( 'unit_growth_rate_in' ) ?? 0.05 } ,"retirement_age_in":65,"salary_0_in": ${ q . get ( 'salary_0_in' ) ?? 50000 } ,"salary_inflation_rate_in":0.02,"empee_contribution_rate_in":0.1,"emper_matching_rate_in": ${ q . get ( 'emper_matching_rate_in' ) ?? 1 } ,"contribution_charge_rate_in":0.04,"management_charge_rate_in":0.01,"salary_age_0_in": ${ q . get ( 'salary_age_0_in' ) ?? 50 } ,"missed_contribution_age_in":15}`
) , JSON . parse ( `{"age_0_in": ${ q . get ( 'age_0_in' ) ?? 50 } ,"fund_value_0_in":0,"unit_growth_rate_in": ${ q . get ( 'unit_growth_rate_in' ) ?? 0.05 } ,"retirement_age_in":65,"salary_0_in": ${ q . get ( 'salary_0_in' ) ?? 50000 } ,"salary_inflation_rate_in":0.02,"empee_contribution_rate_in":0.1,"emper_matching_rate_in": ${ q . get ( 'emper_matching_rate_in' ) ?? 1 } ,"contribution_charge_rate_in":0.04,"management_charge_rate_in":0.01,"salary_age_0_in": ${ q . get ( 'salary_age_0_in' ) ?? 50 } ,"missed_contribution_age_in":15}`
)]
input_cursor_id = inputs_history . length - 1
addl_years = 2 // 2
new_value = model . retirement_fund_value (inputs_history[inputs_history . length - 1 ])
old_value = model . retirement_fund_value (inputs_history[inputs_history . length - 2 ])
good_guy = new_value - old_value > 0
viewof shop_application = Inputs . select ([ 'shop-sliders' , 'shop-x-sales-price' ] , { value : 'shop-sliders' , label : 'Application ❓' , width : 150 })
viewof purchase_price_in = Inputs . range ([ 5 , 7 ] , { step : 0.05 , label : 'purchase_price' , value : 5 , width : 200 })
viewof sales_price_in = Inputs . range ([ 5 , 7 ] , { step : 0.05 , label : 'sales_price' , value : 6 , width : 200 , disabled : shop_application == 'shop-sliders' ? false : true })
viewof expenses_in = Inputs . range ([ 0 , 20000 ] , { step : 1000 , label : 'expenses' , value : 15000 , width : 200 })
viewof units_in = Inputs . range ([ 0 , 50000 ] , { step : 1000 , label : 'units' , value : 20000 , width : 200 })
embed (
calcuvizspec ({
models : modelname == 'shop' ? [model] : [] ,
input_cursors : [{sales_price_in , purchase_price_in , expenses_in , 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 ; }
}))
embed (
calcuvizspec ({
models : modelname == 'shop' ? [model] : [] ,
input_cursors : [{purchase_price_in , expenses_in , units_in}] ,
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 ; }
}))
embed (
calcuvizspec ({
models : modelname == 'shop' ? [model] : [] ,
input_cursors : [{purchase_price_in , expenses_in , units_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 : 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 ; }
}))
embed (
calcuvizspec ({
models : modelname == 'shop' ? [model] : [] ,
input_cursors : [{purchase_price_in , expenses_in , units_in}] ,
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 ; }
}))
Inputs . bind (Inputs . range ([ 5 , 7 ] , { step : 0.20 , label : 'purchase_price_in' , value : 5 , width : 200 }) , viewof purchase_price_in)
Inputs . bind (Inputs . range ([ 0 , 20000 ] , { step : 1000 , label : 'expenses_in' , value : 15000 , width : 200 }) , viewof expenses_in)
formulae_not_inputs = Object . values (introspection . cul_functions ) . filter (d => d . reason == 'definition' && inputs . indexOf (d . name + '_in' ) == - 1 ) . map (d => d . name )
//formulae_not_inputs = //['profit','sales','purchases','expenses']
import {radio} from "@jgrunik/inputs"
embed (
calcuvizspec ({
models : [modelname == 'shop-demand-curve' ? model : (modelname == 'shop-demand-curve-modular' ? 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 ; }
}))
ff = [ ... formulae_not_inputs , 'units' ]
embed (
calcuvizspec ({
models : [modelname == 'shop-demand-curve' ? model : (modelname == 'shop-demand-curve-modular' ? 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 ; }
}))
/*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'})*/
// trying out lil-gui controls
function r2 (){
let preset = {}
let savings_options = {
annual_payment_in : 1000 ,
interest_rate : 2 ,
duration_in : 5 ,
savePreset : () => {
// save current values to an object
preset = gui . save () ;
loadButton . enable () ;
} ,
loadPreset : () => {
gui . load ( preset ) ;
}
}
const container = DOM . element ( "div" ) ;
const form = DOM . element ( "form" )
//autoPlace is true by default, which will display outside cell,so change to false.
const gui = new dat . GUI ({ autoPlace : false , closeOnTop : true })
gui . add (savings_options , 'annual_payment_in' , 0 , 1000 ) . onChange (v => form . dispatchEvent ( new CustomEvent ( "input" , { bubbles : true })))
gui . add (savings_options , 'interest_rate' , - 1 , 10 , 1 ) . onChange (v => form . dispatchEvent ( new CustomEvent ( "input" , { bubbles : true })))
gui . add (savings_options , 'duration_in' , 0 , 10 , 1 ) . onChange (v => form . dispatchEvent ( new CustomEvent ( "input" , { bubbles : true })))
gui . add ( savings_options , 'savePreset' ) ;
const loadButton = gui . add ( savings_options , 'loadPreset' ) ;
loadButton . disable () ;
form . append (gui . domElement )
container . value = savings_options
container . append (form)
return container
}
viewof c = r2 ()
embed (
calcuvizspec ({
models : modelname == 'savings' ? [model] : [] ,
input_cursors : [{ ... c , interest_rate_in : c . interest_rate / 100 }] ,
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 , c . 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
}))
/*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'})*/
function r3 (){
let preset = {}
let options = {
skewness_in : - 1 ,
step_size : 0.25 ,
savePreset : () => {
// save current values to an object
preset = gui . save () ;
loadButton . enable () ;
} ,
loadPreset : () => {
gui . load ( preset ) ;
}
}
const container = DOM . element ( "div" ) ;
const form = DOM . element ( "form" )
//autoPlace is true by default, which will display outside cell,so change to false.
const gui = new dat . GUI ({ autoPlace : false , closeOnTop : true })
/*
!!! IMPORTANT:
special handle for color,or observable will not observe the change imediately.
same with range control
so you can simplely add .onChange for all control to dispatchEvent again
*/
//gui.addColor(options,'color').onChange(v=>form.dispatchEvent(new CustomEvent("input", {bubbles: true})))
//gui.add(options,'types',['one','two','three'])
//gui.add(options,'speed',{slow:1,fast:40})
//gui.add(options,'age',1,40)
//gui.addFolder('inputs ⚙️')
gui . add (options , 'skewness_in' , - 2 , 2 ) . onChange (v => form . dispatchEvent ( new CustomEvent ( "input" , { bubbles : true })))
gui . add (options , 'step_size' , 0.15 , 0.3 ) . onChange (v => form . dispatchEvent ( new CustomEvent ( "input" , { bubbles : true })))
gui . add ( options , 'savePreset' ) ;
preset = gui . save () ;
const loadButton = gui . add ( options , 'loadPreset' ) ;
//loadButton.disable();
form . append (gui . domElement )
container . value = options
container . append (form)
return container
}
viewof sp_padrao_options = r3 ()
sp_padrao_data = calcudata ({
models : modelname == 'sp-padrao' ? [model] : [] ,
input_domains : { x_in : _ . range ( 9 , 21 , sp_padrao_options . step_size ) , y_in : _ . range ( - 4 , 7 , sp_padrao_options . step_size )} ,
input_cursors : [{ ... sp_padrao_options}] ,
outputs : [ 'x' , 'y' , 'color' ] ,
pivot : true
})
//Inputs.table(sp_padrao_data)
embed ({
"data" : {
"name" : "projection"
} ,
"mark" : {
"type" : "point" ,
"tooltip" : true ,
"strokeWidth" : 5 ,
"clip" : true
} ,
"encoding" : {
"x" : {
"field" : "x_in" ,
"type" : "quantitative" ,
"scale" : {
"zero" : false ,
"domain" : [
9 ,
21
]
} ,
"axis" : {
"labelAngle" : 0 ,
"orient" : "top"
}
} ,
"y" : {
"field" : "y" ,
"type" : "quantitative" ,
"scale" : {
"zero" : false ,
"domain" : [
- 3.5 ,
6.7
]
}
} ,
"detail" : {
"field" : "x_in" ,
"type" : "nominal"
} ,
"shape" : {
"field" : "y_in" ,
"type" : "nominal" ,
"scale" : {
"range" : [
"square"
]
}
} ,
"color" : {
"field" : "color" ,
"type" : "nominal" ,
"scale" : {
"range" : (color_range . includes ( "🟢🟡🔵" ) ? [ "green" , "yellow" , "blue" ]
: (color_range . includes ( "xyz" ) ? [ "black" , "#f0f0f0" ]
: [ "black" , "#f0f0f0" ]))
}
} ,
"size" : {
"value" : 100
} ,
"opacity" :
opacity . length ? { "field" : "y_in" } : {}
} ,
"config" : {
"legend" : {
"disable" : true
}
} ,
"width" : 300 * (sp_padrao_size + 1 ) ,
"height" : 250 * (sp_padrao_size + 1 ) ,
"datasets" : { 'projection' : sp_padrao_data}
})
viewof color_range = Inputs . select ([ "classic ⚫⚪" , "🟢🟡🔵" ] , { label : "color scheme" , value : "classic ⚫⚪" })
viewof opacity = Inputs . checkbox ([ "opacity" ] , { label : "effects" , value : [ "opacity" ]})
viewof sp_padrao_size = Inputs . toggle ({ label : 'large' })
// trying out lil-gui controls
function r5 (){
let preset = {}
let options = {
operation_in : 'union' ,
i_in : 10 ,
alpha_in : 69 ,
beta_in : 38 ,
dist_in : 1.88 ,
fov_in : 34 ,
screen_width_in : 30 ,
screen_height_in : 30 ,
shape_A_in : 'cube' , shape_x_A_in : 0 , shape_y_A_in : 0 , shape_z_A_in : 0 , shape_size_A_in : 0.4 ,
shape_B_in : 'sphere' , shape_x_B_in : 0 , shape_y_B_in : 0.3 , shape_z_B_in : 0 , shape_size_B_in : 0.4 ,
savePreset : () => {
// save current values to an object
preset = gui . save () ;
loadButton . enable () ;
} ,
loadPreset : () => {
gui . load ( preset ) ;
}
}
const container = DOM . element ( "div" ) ;
const form = DOM . element ( "form" )
//autoPlace is true by default, which will display outside cell,so change to false.
const gui = new dat . GUI ({ autoPlace : false , closeOnTop : true })
gui . add (options , 'i_in' , 0 , 50 , 1 ) . name ( '# raymarching iterations' ) ;
gui . add (options , 'operation_in' , [ 'union' , 'intersection' , 'B-A' , 'A-B' ]) . name ( 'operation?' ) ;
const shape_A = gui . addFolder ( 'shape A' ) ;
shape_A . add (options , 'shape_A_in' , [ 'sphere' , 'cube' , 'teapot' ]) ;
shape_A . add (options , 'shape_x_A_in' , - 1 , 1 , 0.1 ) ;
shape_A . add (options , 'shape_y_A_in' , - 1 , 1 , 0.1 ) ;
shape_A . add (options , 'shape_z_A_in' , - 1 , 1 , 0.1 ) ;
shape_A . add (options , 'shape_size_A_in' , 0 , 1 , 0.1 ) ;
const shape_B = gui . addFolder ( 'shape B' ) ;
shape_B . add (options , 'shape_B_in' , [ 'sphere' , 'cube' /*, 'teapot'*/ ]) ;
shape_B . add (options , 'shape_x_B_in' , - 1 , 1 , 0.1 ) ;
shape_B . add (options , 'shape_y_B_in' , - 1 , 1 , 0.1 ) ;
shape_B . add (options , 'shape_z_B_in' , - 1 , 1 , 0.1 ) ;
shape_B . add (options , 'shape_size_B_in' , 0 , 1 , 0.1 ) ;
gui . onChange (v => form . dispatchEvent ( new CustomEvent ( "input" , { bubbles : true }))) ;
gui . add ( options , 'savePreset' ) ;
preset = gui . save () ;
const loadButton = gui . add ( options , 'loadPreset' ) ;
//loadButton.disable();
form . append (gui . domElement )
container . value = options
container . append (form)
return container
}
viewof raymarching_inputs = r5 ()
viewof alpha_in = Inputs . range ([ - 180 , 180 ] , { value : 69 , step : 1 , label : "alpha_in" })
viewof beta_in = Inputs . range ([ - 180 , 180 ] , { value : 38 , step : 1 , label : "beta_in" })
viewof dist_in = Inputs . range ([ 1 , 3 ] , { value : 1.88 , step : 0.01 , label : "dist_in" })
viewof fov_in = Inputs . range ([ 1 , 180 ] , { value : 34 , step : 1 , label : "fov_in" })
viewof sdf_shape_in = Inputs . select ([
'sphere' ,
'cube' ,
'cube_with_holes' ,
'cube_with_holes2' ,
'cube_with_holes3' ,
'cube_with_holes4' ,
'cube_and_torus' ,
'teapot' ,
] , { value : 'cube_with_holes4' , label : "sdf_shape" , disabled : true })
viewof screen_width_in = Inputs . range ([ 0 , 150 ] , { value : 30 , step : 10 , label : "screen_width_in" })
viewof screen_height_in = Inputs . range ([ 0 , 150 ] , { value : 30 , step : 10 , label : "screen_height_in" })
embed (
calcuvizspec ({
models : modelname == 'raymarching' ? [model] : [] ,
input_cursors : [{alpha_in , beta_in , dist_in , fov_in , shape_A_in : 'cube' , screen_width_in , screen_height_in ,... raymarching_inputs}] ,
mark : 'rect' , // heatmap
encodings : {
x : { 'name' : 'xq_in' , type : 'nominal' , domain : _ . range ( 0 , screen_width_in , 1 )} ,
y : { 'name' : 'yq_in' , type : 'nominal' , domain : _ . range ( 0 , screen_height_in , 1 )} ,
color : { 'name' : 'value' , type : 'quantitative' } ,
row : { 'name' : 'formula' , domain : [ 'brightness_2' /* can put more formulae here */ ]}
} ,
spec_post_process : spec => {
spec . width = 300 ;
spec . height = 250 ;
spec . encoding . y . sort = 'descending'
spec . encoding . x . axis = { grid : false , domain : false , ticks : 0 , labels : false }
spec . encoding . y . axis = { grid : false , domain : false , ticks : 0 , labels : false }
spec . config = { "legend" : { "disable" : true }}
spec . resolve = { scale : { color : 'independent' }}
spec . mark . clip = true ;
return spec ;
}
})
)
sdf = require ( '/models/raymarching/sdf_composed.js' )
data_sdf = calcudata ({
models : modelname == 'raymarching' ? [sdf] : [] ,
input_cursors : [{ z_in : 0 , alpha_in , beta_in , dist_in , fov_in , shape_A_in : 'cube' , screen_width_in , screen_height_in ,... raymarching_inputs , i_in : 100 }] ,
input_domains : { x_in : _ . range ( - 2 , 2.001 , 0.1 / 4 ) ,
y_in : _ . range ( - 2.5 , 2.001 , 0.1 / 4 ) }
,
outputs : [ 'sdf' ]
})
md `<br/>Plot of sdf_composed sdf @ z=0, iteration 100:`
viewof plot_sdf = Plot . plot ({
//width:400,
//height:260,
style : {
background : 'rgba(0,0,0,0)' // not applied to legend
} ,
color : { legend : true , reverse : true , label : `sdf` } ,
x : { label : 'x_in' , tickFormat : d3 . format ( '.2f' ) , ticks : _ . uniq (data_sdf . map (d => d . x_in ))} ,
y : { label : 'y_in' , tickFormat : d3 . format ( '.2f' ) , ticks : _ . uniq (data_sdf . map (d => d . y_in ))} ,
marks : [
Plot . contour (data_sdf , { // https://observablehq.com/plot/marks/contour
x : "x_in" ,
y : "y_in" ,
fill : "value" , interpolate : Plot . interpolateNearest , blur : false ,
interval : 0.15 ,
stroke : "black"
})
]
})
viewof saas_form = {
let form = Inputs . form ({
mrr_in : Inputs . range ([ 0 , 500000 ] , { label : "monthly revenue (mrr)" , step : 5000 , value : 155000 }) ,
mrr_growth_in : Inputs . range ([ - 0.3 , . 8 ] , { label : "mrr growth factor" , step : 0.01 , value : 0.15 }) ,
vc_1_in : Inputs . range ([ 0 , 1e7 ] , { label : "venture capital R1 (vc)" , step : 100000 , value : 1000000 }) ,
vc_2_in : Inputs . range ([ 0 , 1e7 ] , { label : "venture capital R2 (vc)" , step : 100000 , value : 2000000 }) ,
salary_per_employee_in : Inputs . range ([ 0 , 30000 ] , { label : "salary/employee" , step : 1000 }) ,
employees_0_in : Inputs . range ([ 0 , 50 ] , { label : "employees @ start" , step : 1 , value : 26 }) ,
new_employees_per_month_in : Inputs . range ([ 0 , 10 ] , { label : "new employees per mth" , step : 1 , value : 2 }) ,
rent_in : Inputs . range ([ 0 , 300000 ] , { label : "rent" , step : 10000 , value : 200000 }) ,
} , /*{template}*/ ) ;
return form
}
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 })
viewof grid = Inputs . checkbox ([ "☰ gridlines ☰" ] , { label : "" , value : [ "" /*"☰ gridlines ☰"*/ ]})
saas_data = calcudata ({
models : modelname == 'saas-cashflows' ? [model] : [] ,
input_domains : { month_in : _ . range ( - 1 , last_month_in + 1 , 1 )} ,
input_cursors : [{ ... saas_form , npv_i_in , last_month_in}] ,
outputs : formulae_not_inputs ,
//pivot:true
})
md `The NPV over the ${ last_month_in + 1 } months is **€ ${ d3 . format ( ',.2f' )(model . npv ({ ... saas_form , last_month_in , npv_i_in , month_in :- 1 })) } **`
embed ({
"$schema" : "https://vega.github.io/schema/vega-lite/v5.json" ,
"data" : { "name" : "projection" } ,
"datasets" : { projection : saas_data} ,
"height" : 200 , "width" : 230 ,
"transform" : [{ 'filter' : 'indexof(datum.formula, "_cf") != -1' }] ,
"encoding" : {
"x" : { "field" : "month_in" } ,
"y" : { "field" : "value" , "type" : "quantitative" , "axis" : { "grid" : grid . length , labelAngle : 90 }} ,
} ,
"layer" : [
{
"transform" : [{ "filter" : "datum.formula != 'total_cf'" }] ,
"mark" : { "type" : "bar" , "tooltip" : true } ,
"encoding" : {
"color" : { "field" : "formula" , "type" : "nominal" }
}
} ,
{
"transform" : [{ "filter" : "datum.formula == 'total_cf'" }] ,
"mark" : { "type" : "line" , "point" : true , size : 3 , "tooltip" : true } ,
"encoding" : {
"color" : { "value" : "black" } ,
"opacity" : { "value" : 1 } ,
"strokeWidth" : { "value" : 2 } ,
"y" : { "field" : "value" , "type" : "quantitative" , "title" : "total_cf" }
}
}]
} , { theme : 'fivethirtyeight' })
embed (
calcuvizspec ({
models : modelname == 'saas-cashflows' ? [model] : [] ,
input_cursors : [{ ... saas_form , npv_i_in , last_month_in}] ,
mark : 'bar' ,
encodings : {
x : { name : 'month_in' , type : 'nominal' , domain : _ . range ( - 1 , last_month_in + 1 , 1 )} ,
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 ; }
}))
embed (
calcuvizspec ({
models : modelname == 'saas-cashflows' ? [model] : [] ,
input_cursors : [{ ... saas_form , npv_i_in , last_month_in}] ,
mark : { type : 'line' , point : true /*, interpolate: 'step-after'*/ } ,
encodings : {
x : { name : 'month_in' , type : 'nominal' , domain : _ . range ( - 1 , last_month_in + 1 , 1 )} ,
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 ; }
}))
viewof point = location_ui ({ value : [ - 6.475492 , 53.694712 ] , label : "Co-ords" })
viewof clip1 = Inputs . toggle ({ label : "clip1" , value : true })
viewof clip2 = Inputs . toggle ({ label : "clip2" , value : true })
import { location as location_ui } from "@roelandschoukens/inputs"
viewof date_in = Inputs . date ({ label : "start date" , value : new Date ( 2023 , 11 , 22 , 18 , 0 , 0 )})
md `**offset days:**`
d = date_fns . addDays (date_in , offset)
viewof fast = Inputs . toggle ({ label : 'fast' , value : false })
viewof offset = Scrubber (_ . range ( 0 , 365 , fast ? 2 : 1 ) , { autoplay : false , delay : fast ? 3 : 60 , label : 'offset' })
title = d3 . timeFormat ( '%B' )(d)
embed ( calcuvizspec ({
models : modelname == 'sunsets' ? [model] : [] ,
input_cursors : [{
lat_in : point[ 1 ] , lng_in : point[ 0 ] , obj_in : 'sun' ,
}] ,
mark : { type : 'point' , size : 2000 , filled : true , tooltip : true , clip : clip1} ,
encodings : {
y : { grid : false , name : 'altitude_obj' , type : 'quantitative' , scale : { zero : false , domain : [ 0 , 1 ]}} ,
x : { grid : false , name : 'azimuth_obj' , type : 'quantitative' , scale : { zero : false , domain : [ 0 , 2 ]}} ,
color : { name : 'date_in' , sort : 'ascending' , type : 'quantitative' , legend : true , domain :
d3 . timeHour . range (
new Date (d) . setHours (clip2 ? 13 : 0 ) ,
new Date (d) . setHours (clip2 ? 22 : 24 ) ,
1
) , } //_.range(-60,24*60,30).map(d => date_fns.addMinutes(new Date(date_in), d))},
// row: additional months ..?
} ,
width : 500 *. 6 * (clip1 ? 1 : 0.5 ) ,
height : 400 *. 6 * (clip1 ? 1 : 0.5 ) ,
spec_post_process : s => { s . encoding . color . timeUnit = 'hours' ; s . title = title ; s . titleFontSize = 40 ;
s . encoding . color . scale = { "scheme" : "lightmulti" } ; return s }
}) , { actions : true , config : { view : { fill : '#fdfaff' }}})
/*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'})*/
function r4 (){
let preset = {}
let options = { no_points : 10000 ,
savePreset : () => {
// save current values to an object
preset = gui . save () ;
loadButton . enable () ;
} ,
loadPreset : () => {
gui . load ( preset ) ;
}
}
const container = DOM . element ( "div" ) ;
const form = DOM . element ( "form" )
//autoPlace is true by default, which will display outside cell,so change to false.
const gui = new dat . GUI ({ autoPlace : false , closeOnTop : true })
/*
!!! IMPORTANT:
special handle for color,or observable will not observe the change imediately.
same with range control
so you can simplely add .onChange for all control to dispatchEvent again
*/
//gui.addColor(options,'color').onChange(v=>form.dispatchEvent(new CustomEvent("input", {bubbles: true})))
//gui.add(options,'types',['one','two','three'])
//gui.add(options,'speed',{slow:1,fast:40})
//gui.add(options,'age',1,40)
gui . add (options , 'no_points' , 0 , 20000 ) . onChange (v => form . dispatchEvent ( new CustomEvent ( "input" , { bubbles : true })))
gui . add ( options , 'savePreset' ) ;
preset = gui . save () ;
const loadButton = gui . add ( options , 'loadPreset' ) ;
//loadButton.disable();
form . append (gui . domElement )
container . value = options
container . append (form)
return container
}
viewof copacabana_options = r4 ()
copacabana_data = calcudata ({
models : modelname == 'copacabana' ? [model] : [] ,
input_domains : { p_in : _ . range ( 0 , copacabana_options . no_points )} ,
input_cursors : [{}] ,
outputs : [ 'x' , 'y' , 'color' ] ,
pivot : true
})
Inputs . table (copacabana_data)
embed ({
"data" : { "name" : "projection" } ,
"mark" : { "type" : "point" , "tooltip" : true , "opacity" : 0.7 , "strokeWidth" : 5 } ,
"encoding" : {
"x" : {
"field" : "x" ,
"type" : "quantitative" ,
"scale" : { "zero" : false } ,
"axis" : { "labelAngle" : 0 , "orient" : "top" }
} ,
"y" : { "field" : "y" , "type" : "quantitative" , "scale" : { "zero" : false }} ,
"color" : { "field" : "color" , "type" : "nominal" , scale} ,
"shape" : { "field" : "p_in" , "type" : "nominal" } ,
"size" : { "value" : 50 }
} ,
"config" : { "legend" : { "disable" : true }} ,
"width" : 300 * (copacabana_size + 1 ) ,
"height" : 250 * (copacabana_size + 1 ) ,
"datasets" : { projection : copacabana_data}})
scale = {
if (copacabana_color_range == 'classic ⚫⚪' )
return ({ "range" : [ "#dddde9" , "black" ]})
else if (copacabana_color_range == 'colorful 🎨' )
return ({ scheme : 'category10' })
else if (copacabana_color_range == '🟢🟡🔵' )
return ({ range : [ "green" , "yellow" , "blue" ]})
else
return ({ scheme : copacabana_color_range})
}
viewof copacabana_color_range = Inputs . select ([ "classic ⚫⚪" , "colorful 🎨" , "🟢🟡🔵" , "blues" , "browns" ] , { label : "color scheme" , value : "classic ⚫⚪" })
viewof copacabana_size = Inputs . toggle ({ label : 'large' })
viewof actual_interest_rate_co_in = Inputs . range ([ - 1 , 6 ] , { value : 2 , label : 'actual_interest_rate_rate_co_in' , step : 1 })
savings_rec_data = calcudata ({
models : modelname == 'savings-rec' ? [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' ]
})
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
})
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
input_domains = ({
x_in : _ . range ( - 3 , 3.001 , 0.25 ) ,
y_in : _ . range ( - 3.5 , 5.001 , 0.25 ) ,
})
data = calcudata ({
models : modelname == 'heart-contour' ? [model] : [] ,
input_cursors : [{}] ,
input_domains ,
outputs : [ 'f' ]
})
viewof plot = Plot . plot ({
//width:400,
//height:260,
style : {
background : 'rgba(0,0,0,0)' // not applied to legend
} ,
color : {legend , reverse : true , label : `f(x_in,y_in)` } ,
x : { label : 'x_in' , tickFormat : d3 . format ( '.2f' ) , ticks : _ . uniq (data . map (d => d . x_in ))} ,
y : { label : 'y_in' , tickFormat : d3 . format ( '.2f' ) , ticks : _ . uniq (data . map (d => d . y_in ))} ,
marks : [
Plot . contour (data , { // https://observablehq.com/plot/marks/contour
x : "x_in" ,
y : "y_in" ,
fill : "value" , interpolate : Plot . interpolateNearest , blur ,
interval : intervals
})
]
})
viewof legend = Inputs . toggle ({ value : false , label : 'legend' })
viewof intervals = Inputs . range ([ 1 , 10 ] , { value : 4 , label : 'contour intervals' , step : 0.1 , width : 100 })
viewof blur = Inputs . range ([ 0 , 50 ] , { value : 4 , label : 'blur' , step : 0.5 , width : 100 })
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]}`
)]
clip = true
// NOTE : adding some hurried customisation and experimentation for fireworks effect! should be cleaner
embed (
calcuvizspec ({
models : modelname == 'projectile' ? [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}
}))
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 ] } ` }))]
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' })
bounce_data = calcudata ({ models : modelname == 'bounce-6' ? [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 ++;
}
})
import {p5} from "@tmcw/p5"
Inputs . table (bounce_data)
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 : modelname == 'raycasting' ? [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
}
}))
viewof labels = Inputs . toggle ({ label : "labels" , value : true })
viewof level = embed (
calcuvizspec ({
models : modelname == 'raycasting' ? [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 ;
}
}))
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
import {viewof keys} from 'd/867e1424e1d093ab'
viewof keys
viewof time_in = Inputs . range ([ 0 , keys . length ] , { step : 1 , value : keys . length , label : 'time_in' })
//time_in = keys.length
fov = model . fov_calcd ({time_in , keys_in : keys . map (d => d . key ) . filter ((d , i) => i < time_in) , speed_in : 3 , fov_0_in : [ - 1 - Math . PI / 2 , 1 - Math . PI / 2 ]})
fov
keys
md `keys: ${ keys . length == 0 ? '⌛' : keys . map (d => d . key ) . reverse () . slice ( 0 , 10 ) . map (d => d == 'ArrowUp' ? '⬆️' : d == 'ArrowDown' ? '⬇️' : d == 'ArrowLeft' ? '⬅️' : d == 'ArrowRight' ? '➡️' : d) . join ( ' ' ) } `
e = model . inverse_ray_length$m . cache
e
Object . keys (e) . length
viewof scene2 = embed (
calcuvizspec ({
models : modelname == 'raycasting-playable' ? [model] : [] ,
input_cursors : [{time_in , keys_in : keys . map (d => d . key ) . filter ((d , i) => i < time_in) , speed_in : 3 , fov_0_in : [ - 1 - Math . PI / 2 , 1 - Math . PI / 2 ] , }] ,
mark : { type : 'bar' , point : false , clip : true , tooltip : false } ,
encodings : {
x : { grid : false , name : 'ray_angle_in' , type : 'quantitative' , domain :
[clamped_ray_angle_in2 , ... _ . range (fov[ 0 ] , fov[ 1 ] , ray_angle_in_step_size)]
//[1,1.1,1.2]
, 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 = labels2 ? { tickCount : 2 , grid : false } : null
spec . encoding . y . axis = labels2 ? { 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_in2 || 999}`,"value": "red"}
spec . encoding . opacity . condition = { "test" : `datum.ray_angle_in== ${ clamped_ray_angle_in2 || 999 } ` , "value" : 1 }
return spec
}
}))
viewof labels2 = Inputs . toggle ({ label : "labels" , value : true })
4 + 4
clamped_ray_angle_in2
raycasting_playable_level_data = calcuvegadata ({
models : modelname == 'raycasting-playable' ? [model] : [] ,
spec : raycasting_playable_level_spec . layer [ 0 ] ,
domains : { level_x_in : _ . range ( 0 , 63.1 , 1 ) , level_y_in : _ . range ( 0 , 63.1 , 1 ) } ,
input_cursors : [{ }]
})
raycasting_playable_level_player_data = calcuvegadata ({
models : modelname == 'raycasting-playable' ? [model] : [] ,
spec : raycasting_playable_level_spec . layer [ 1 ] ,
domains : {} ,
input_cursors : [{time_in , keys_in : keys . map (d => d . key ) , speed_in : 3 , fov_0_in : [ - 1 - Math . PI / 2 , 1 - Math . PI / 2 ] }]
})
raycasting_playable_level_player_data
raycasting_playable_level_spec = ({
//"data": {"name": "data"},
"datasets" : { data : raycasting_playable_level_data0 ,
player : raycasting_playable_level_player_data0 } ,
layer : [{
"data" : { "name" : "data" } ,
"mark" : { "type" : "rect" , "tooltip" : false } ,
"encoding" : {
"x" : {
"field" : "level_x_in" ,
"type" : "nominal" ,
"scale" : { "rangeStep" : 12 } ,
"axis" : { "grid" : false , "ticks" : false , "labels" : false }
} ,
"y" : {
"field" : "level_y_in" ,
"type" : "nominal" ,
"scale" : { "rangeStep" : 12 } ,
"axis" : { "grid" : false , "ticks" : false , "labels" : false }
} ,
"color" : {
"field" : "level" ,
"type" : "quantitative" ,
"scale" : { "scheme" : "turbo" }
}
} ,
} ,
{
"data" : { "name" : "player" } ,
mark : 'text' ,
encoding : {
x : { field : 'player_x_calcd' } ,
y : { field : 'player_y_calcd' } ,
text : { value : '🧑' } ,
}
}] ,
"config" : { "legend" : { "disable" : true }} ,
width : 280 , height : 280 ,
})
mutable raycasting_playable_level_data0 = [] // includes data in spec for when I Open in Vega Editor
mutable raycasting_playable_level_player_data0 = []
htl . html `<button onclick= ${ () => {mutable raycasting_playable_level_data0 = raycasting_playable_level_data ; mutable raycasting_playable_level_player_data0 = raycasting_playable_level_player_data} } >LEVEL insert data -> vega editor (dev)</button>`
viewof raycasting_playable_level_viz = embed (raycasting_playable_level_spec)
{
raycasting_playable_level_viz . data ( "data" , raycasting_playable_level_data) . resize () . run () ;
}
{
console . log ( Object . entries (model . inverse_ray_length$m . cache ) . length )
console . log ( Object . entries (model . player_x_raw$m . cache ) . length )
raycasting_playable_level_viz . data ( "player" , raycasting_playable_level_player_data) . resize () . run () ;
}
viewof observable_world_v2 = Inputs . checkbox ([ "overlay field of view" ] , { value : true })
observable_world2 = observable_world_v2 . length
viewof climate_sensitivity_in = Inputs . range ([ 0 , 5 ] , { step : 0.2 , value : 3 , label : 'climate_sensitivity_in' })
viewof drawdown_factor_in = Inputs . range ([ 0 , 0.001 * 2 ] , { step : 0.001 / 5 , value : 0.001 , label : 'drawdown_factor_in' })
md `**45%** emissions absorption currently hardcoded`
viewof emissions = embed (
{
"$schema" : "https://vega.github.io/schema/vega/v5.json" ,
"width" : 350 ,
"height" : 200 , //+ 0* climate_sensitivity_in,
"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 ? '-' : 'max Δ T ≈ ' + format(datum.temperature_delta, ',.2f')" } ,
} ,
"update" : {
//"x": {"scale": "xscale", "field": "year_in", "band": 1,},
"x" : { "scale" : "xscale" , "value" : 2075 } ,
"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 }
}
}
}
]
}
)
//viewof emissions = Inputs.input([])
mutable emissions_ = emissions . data ( 'interpolated' )
climate1 = calcudata ({
models : modelname == 'simple-climate' ? [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}] ,
input_domains : {
year_in : [ 2015 , 2016 ,... _ . range ( 2020 , 2151 , 10 )] ,
} ,
outputs : [ 'emissions_rate' , 'temperature_delta' , 'CO2_concentration' , 'concentration_factor' ]
})
// "gradual calculation"
// this should be calcd in-model
Tmax_year_1 = d3 . greatest (climate1 , d => d . value ) . year_in
climate2 = calcudata ({
models : modelname == 'simple-climate' ? [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}] ,
input_domains : {
year_in : _ . range ( Math . max ( 2015 , Tmax_year_1 - 10 ) , Tmax_year_1 + 11 , 1 ) ,
} ,
outputs : [ 'temperature_delta' ]
})
Tmax_year_2 = d3 . greatest (climate2 , d => d . value ) //.year_in
// used in layer in table viz
climate_Tmax = calcudata ({
models : modelname == 'simple-climate' ? [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 ,
year_in : Tmax_year_2 . year_in }] ,
input_domains : {
//year_in: [2015,2016,..._.range(2020,2151,10)],
} ,
outputs : [ 'emissions_rate' , 'temperature_delta' , 'CO2_concentration' , 'concentration_factor' ]
})
embed ({
width : 380 , height : 340 ,
"data" : { "name" : "data" } ,
"encoding" : {
"y" : {
"field" : "year_in" ,
"type" : "quantitative" ,
"scale" : { "zero" : false } ,
"sort" : "ascending"
} ,
"x" : {
"field" : "formula" ,
"type" : "nominal" ,
"axis" : { "labelAngle" : 0 , "orient" : "top" , "labelLimit" : 90 }
} ,
"color" : { "field" : "formula" , "type" : "nominal" } ,
"opacity" : { "value" : 0.7 } ,
"text" : {
"field" : "value" ,
"type" : "quantitative" ,
"format" : ".2f" ,
"axis" : { "format" : ".2f" }
}
} ,
"layer" : [
{
"transform" : [{ "filter" : "datum.year_in == " + Tmax_year_2 . year_in }] ,
"mark" : {
"type" : "text" ,
"fontSize" : 15 ,
"fontWeight" : "bold" ,
"tooltip" : true ,
//"dx": 1, "dy": 1
} ,
"encoding" : { //"color": {"value": "red"},
"opacity" : { "value" : 1 }}
} ,
{
"transform" : [{ "filter" : "datum.year_in <= 2100" }] ,
"mark" : {
"type" : "text" ,
"fontSize" : 16 ,
"fontWeight" : "bold" ,
"tooltip" : true
}
} ,
] ,
"config" : { "legend" : { "disable" : true }} ,
"datasets" : {
"data" : [ ... climate1 ,... climate_Tmax]
}
})
update_climate = (name , value) => {
if (value != undefined )
mutable emissions_ = value ;
/*emissions.change('results2',emissions.insert('results2',calcudata({
models: modelname == 'simple-climate' ? [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}],
outputs: ['temperature_delta'],
pivot:true
}).map(({year_in,temperature_delta}) => ({year_in,temperature_delta})))).remove('results2',d => true).run()*/
emissions . data ( 'results2' ,
/*calcudata({
models: modelname == 'simple-climate' ? [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}],
outputs: ['temperature_delta'],
pivot:true
}).map(({year_in,temperature_delta}) => ({year_in,temperature_delta}))*/
[Tmax_year_2] . map (d => ({ ... d , temperature_delta : d . value }))
) . run ()
}
{emissions . addDataListener ( 'interpolated' , update_climate) } ;
{
drawdown_factor_in ;
climate_sensitivity_in ; // force recalc/coordination on input change
update_climate ()
//emissions.insert('interpolated',[]).run()
}