calcy-quarty-vizy
  • More Models

simple loan model

  • Show All Code
  • Hide All Code

  • View Source
simple loan model
Author

Declan Naughton 🧮👨‍💻🧉

Code
md`##### input values ⚙️`
Code
viewof ui
Code
stepp = step

mutable playing = 0

  • 📺
  • 📈
  • 👀
Code
mutable spec_now = spec_post_process(c_spec1)

x = vega_interactive(spec_now, { renderer: 'svg'})

I need sep./addl. UIs for this.

Code
domains11 = ({...domains, formula: Object.values(introspection.cul_functions).filter(d => d.reason == 'definition' && inputs.indexOf(d.name+'_in') == -1).map(d => d.name) /*domains.formula??domains.formulae*/})

p = projection_fn ({ mapped:['formula'], // no 'value'
  domains: domains11, cursor:ui // problems when something missing from cursor, in this case an input mapped in the main story
                 })

xx = vega_interactive({ // modd from https://observablehq.com/@declann/some-cashflows?collection=@declann/calculang v~908
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "data": {"name": "projection"},
  "height": 480,
  "width": 550,
  "transform": [
    {"calculate": "round(100*datum.value)/100", "as": "amount2"}
  ],
  "mark": {"type": "text", "tooltip": true},
  "encoding": {
    "y": {"field": "formula", "axis": {"orient": "left", "labelAngle": 30}},
    /*"y": {
      "field": "month_in",
      "type": "quantitative",
      "sort": "descending",
      "axis": {"tickOffset": 2, "tickCount": 20, "grid": 0}
    },*/
    "color": {"field": "formula", "type": "nominal"},
    "text": {"field": "amount2", "format": ",.2f"}
  },
  "config": {"legend": {"disable": true}},
  "datasets": {
    "projection": p
  }
}, { renderer: 'svg'})

p
  • calculang 📝💬
  • 🔗
  • 🌲
  • ✨ js
  • data ⬇
Code
md`
~~~js
${
formulae_objs.map(f => cul_0.split('\n').filter((d,i) => i >= f.loc.start.line-1 && i < f.loc.end.line).join('\n').slice(13)).join('\n\n')
}
~~~
`

formula-input dependence map (todo pre-pop all values?):

Code
md`
formula | ${inputs/*.map(d => d.slice(0, -3))*/.join(' | ')}
-------- | ${inputs.map(d => ':--------:').join(' | ')}
 | ${inputs.map(d => '<img width=80/>').join(' | ')}
${formulae_objs.map(f => `${f.name} | ${inputs.map(d => /*this will only work if I populate negs in cul_functions OR if I use cul_links. f.negs.includes(d) ? 'NEG' : */ (f.inputs.includes(d) ? '✔️' : '')).join(' | ')}`).join('\n')}
`
Code
fixedDot = {
 let start = introspection.dot.split('\n')

  let subgraph = []

  inputs.forEach(input => {
    subgraph.push(start.find(s => s.indexOf(`${input}" [`) != -1))
    subgraph.push(start.find(s => s.indexOf(`${input.slice(0,-3)}" [`) != -1))
  })

  let out = start.filter(d => subgraph.indexOf(d) == -1)

// https://graphviz.org/Gallery/directed/cluster.html
  out[0] = out[0] + `subgraph cluster_0 { style=filled;color=lightgrey; node [style=filled,color=white];label = "inputs";` + subgraph.join(';') + "}";
  
  return out.join('\n')
}


g = dot`${fixedDot}`

// todo svg download button

DOM.download(() => serializeSVG(g), undefined, "Save as SVG")
Code
md`
~~~js
${
formulae_objs.map(f => esm_0.split('\n').filter((d,i) => i >= f.loc.start.line-1 && i < f.loc.end.line).join('\n').slice(13)).join('\n\n')
}
~~~
`
Code
viewof raw = Inputs.radio(["raw", "nomemo"], {value:"nomemo", label:""})
Code
Inputs.table(projection)
Code
DOM.download(serialize(projection), "projection", "csv download")
Code
DOM.download(serializeJSON(spec_now), "spec", "spec download (VL)")

.

.

.

.

You may ignore notebook workings below this line

debug

Code
html`formula must be defined in here:`
Code
input_combos_projection

`used for cp`
Code
input_domains_projection
Code
introspection
Code
formulae
Code
domains
Code
ui
Code
html`here`
Code
input_domains
Code
input_domains_projection
Code
input_combos_projection
Code
mapped
Code
projection
Code
inputs_history
Code
html`encodings:`
Code
encodings

other

Code
q = new URLSearchParams(location.search)

formulae = Object.values(introspection.cul_functions).filter(d => d.reason == 'definition' && inputs.indexOf(d.name+'_in') == -1).map(d => d.name)

formulae_next = Object.values(introspection_next.cul_functions).filter(d => d.reason == 'definition' && inputs.indexOf(d.name+'_in') == -1).map(d => d.name)

formulae_objs = Object.values(introspection.cul_functions).filter(d => d.reason == 'definition' && inputs.indexOf(d.name+'_in') == -1)

mutable inputs_history = inputs_default

domains
Code
input_domains = {
  // if formula mapped => not something to include
  var o = {}
  mapped.filter(d => !formulae.includes(d)).forEach(i => { // only use mapped
    
    o[i] = domains[i]
  })
  if (mapped.includes("interaction"))
    o.interaction = inputs_history.map((d,i) => i)
  return o
}

input_domains_next = {
  // if formula mapped => not something to include
  var o = {}
  mapped_next.filter(d => !formulae_next.includes(d)).forEach(i => { // only use mapped
    o[i] = domains_next[i]
  })
  if (mapped_next.includes("interaction"))
    o.interaction = inputs_history.map((d,i) => i)
  return o
}


input_combos_projection = cartesianProduct(Object.entries(input_domains_projection).map(([k,v]) => ({[k == 'formulae' ? 'formula' : k]: v})))

input_combos_projection_next = cartesianProduct(Object.entries(input_domains_projection_next).map(([k,v]) => ({[k == 'formulae' ? 'formula' : k]: v})))
Code
// cursor shouldn't include invalid inputs

// this is not designed to replace existing code - e.g. formulae needs to change to formula in specs
// this relies on domains.formula having revelent list of model formulae if formula is mapped

function projection_fn ({mapped, domains, cursor}) {
  
  let input_domains_projection = {}
  Object.entries(cursor).forEach(([k,v]) => {
    input_domains_projection[k] = [v] // inputs defined in cursor => a one-entry array
  })
  
  Object.entries(domains).forEach(([k,v]) => {
    if (mapped.includes(k)) input_domains_projection[k] = v // mapped domain => include that domain
  })

  let input_combos_projection = cartesianProduct(Object.entries(input_domains_projection).map(([k,v]) => ({[k]:v})))

  // inputs...|formula|value
  // ^ whenever a set of formulae mapped

  // inputs...|formulae...
  // ^ whenever >1 formula is mapped
  // =1 case also falls here, its ok

  /*let sets = 0; // I could just look for formula in mapped, since
  mapped.forEach(m => {
    if (m.slice(-3) == '_in') return;
    if (m != 'interaction')
      sets++;
    // not even checking domains keys
  })*/

  if (mapped.includes('formula')) {

    //return input_combos_projection.map(combos => ({...combos, value: /*+*/model[combos.formula](combos)}))

    let o = []

    input_combos_projection.forEach(combos => {
      let ans = 'ERROR'; // or NaN for viz purposes?
      try {
        ans = /*+*/model[combos.formula](combos)
      } catch(e) {
        console.log(e)
      }

      o.push({...combos, value:ans})
    })

    return o;

  } else {
    
    let o = [];

    input_combos_projection.forEach(combos => {
      let oo = {...combos};

      mapped.filter(m => m.slice(-3) != '_in') // 'interaction' case todo
        .forEach(f => {
          oo[f] = /*+*/model[f](oo);
        })

      o.push(oo);

    })

    return o;

  }
}
Code
projection = {
  if (mapped.includes('formulae'))
    return input_combos_projection.map(combos => {
      if (!mapped.includes('interaction')) return combos
      if (combos.interaction == inputs_history.length-1) return combos
      else
        return ({...combos, ...inputs_history[combos.interaction]/*adding too much here e.g. t_interval, restrict to inputs?*/}) // cant set to input histories here !
    }).map(combos => ({...combos, value: /*+*/model[combos.formula](combos)/*.toFixed(2)*/}))
  else {
    // do all mapped formulae at once
    var o = [];
   input_combos_projection.forEach(combo => {
    var oo = (combos => {
          if (!mapped.includes('interaction')) return combos
      if (combos.interaction == inputs_history.length-1) return combos
else
        return ({...combos, ...inputs_history[combos.interaction]/*adding too much here e.g. t_interval, restrict to inputs?*/}) // cant set to input histories here !
    //mapped.filter(d => formulae.includes(d)).for
    })(combo);
      mapped.filter(d => formulae.includes(d)).forEach(formula => {
        var oooo = {}
        Object.keys(oo).forEach(k => {
          if (k.slice(-3) == '_in') oooo[k] = oo[k]
        })
        oo[formula] = /*+*/model[formula](oooo); // interaction going into call and other things on scatters messes up memo when its needed where randomness is used in model
      })
     o.push(oo);
   })

    return o
     
     
     //combos => ({...combos, [formula]}))
    
    //mapped.filter(formula => formulae.includes(formula)).map(formula => input_combos_projection.map(combos => ({...combos, [formula]}))
    // here look for mapped functions and loop/flatten
    // what will be in c-p for functions? nothing?
  }
}

projection_next = {
  if (mapped_next.includes('formulae'))
    return input_combos_projection_next.map(combos => {
      if (!mapped_next.includes('interaction')) return combos
      if (combos.interaction == inputs_history.length-1) return combos
      else
        return ({...combos, ...inputs_history[combos.interaction]/*adding too much here e.g. t_interval, restrict to inputs?*/}) // cant set to input histories here !
    }).map(combos => ({...combos, value: /*+*/model_next[combos.formula](combos)/*.toFixed(2)*/}))
  else {
    // do all mapped formulae at once
    var o = [];
   input_combos_projection_next.forEach(combo => {
    var oo = (combos => {
          if (!mapped_next.includes('interaction')) return combos
      if (combos.interaction == inputs_history.length-1) return combos
else
        return ({...combos, ...inputs_history[combos.interaction]/*adding too much here e.g. t_interval, restrict to inputs?*/}) // cant set to input histories here !
    //mapped.filter(d => formulae.includes(d)).for
    })(combo);
      mapped_next.filter(d => formulae.includes(d)).forEach(formula => {
        var oooo = {}
        Object.keys(oo).forEach(k => {
          if (k.slice(-3) == '_in') oooo[k] = oo[k]
        })
        oo[formula] = /*+*/model_next[formula](oooo); // interaction going into call and other things on scatters messes up memo when its needed where randomness is used in model
      })
     o.push(oo);
   })

    return o
     
     
     //combos => ({...combos, [formula]}))
    
    //mapped.filter(formula => formulae.includes(formula)).map(formula => input_combos_projection.map(combos => ({...combos, [formula]}))
    // here look for mapped functions and loop/flatten
    // what will be in c-p for functions? nothing?
  }
}
Code
inputs = Object.values(introspection.cul_functions).filter(d => d.reason == 'input definition').map(d => d.name).sort()

inputs_next = Object.values(introspection_next.cul_functions).filter(d => d.reason == 'input definition').map(d => d.name).sort()


input_domains_projection = { // think RE blanket using all here ! filter for inputs? See t_interval
  var o = {}
  Object.entries(/*viz_spec.cursor*/ui).filter(([k,v]) => inputs.includes(k)).forEach(([k,v]) => {
    
    o[k] = [v]
  })
  Object.entries(input_domains).forEach(([k,v]) => {
    o[k] = v
  })
  return o
}

input_domains_projection_next = { // think RE blanket using all here ! filter for inputs? See t_interval
  var o = {}
  Object.entries(/*viz_spec.cursor*/ui).filter(([k,v]) => inputs_next.includes(k)).forEach(([k,v]) => {
    
    o[k] = [v]
  })
  Object.entries(input_domains_next).forEach(([k,v]) => {
    o[k] = v
  })
  return o
}
Code
function cartesianProduct(input, current) {
    if (!input || !input.length) { return []; }
 
    var head = input[0];
    var tail = input.slice(1);
    var output = [];
 
     for (var key in head) {
       for (var i = 0; i < head[key].length; i++) {
             var newCurrent = copy(current);         
             newCurrent[key] = head[key][i];
             if (tail.length) {
                  var productOfTail = 
                          cartesianProduct(tail, newCurrent);
                  output = output.concat(productOfTail);
             } else output.push(newCurrent);
        }
      }    
     return output;
 }

// https://stackoverflow.com/questions/18957972/cartesian-product-of-objects-in-javascript

function copy(obj) {
   var res = {};
   for (var p in obj) res[p] = obj[p];
   return res;
 }
Code
json2csv = require("json2csv@5.0.7/dist/json2csv.umd.js")

// thx to https://observablehq.com/@palewire/saving-csv
// Ben Welsh and comments from Christophe Yamahata
function serialize (data) {
 let parser = new json2csv.Parser();
 let csv = parser.parse(data);
 return new Blob([csv], {type: "text/csv"}) 
}


function serializeJSON (data) {
 return new Blob([JSON.stringify(data,null,2)], {type: "text/json"}) 
}
Code
cql = require("compassql")

schema = cql.schema.build([...projection, ...projection, ...projection, ...projection])

encodings = Object.entries(spec.channels).filter(([k,v]) => k != 'detail_only_proj').map(([k,v]) => ({type:v.type??'nominal', channel:k, field:(v.name??v) == 'formulae' ? 'formula' : (v.name??v)/*nominal, type todo*/}))

output = cql.recommend({
  spec: {data: projection,
    mark: spec.mark == 'bar' ? 'line' : spec.mark,
    encodings
  },
  chooseBy: "effectiveness",
}, schema);

vlTree = cql.result.mapLeaves(output.result, function (item) {
  return item//.toSpec();
});

c_spec = vlTree.items[0].toSpec()

c_spec1 = {
  var s = c_spec;
  s.data = {name: 'projection'};
  s.width = /*viz.width ??*/ 500;
  s.height = spec.height;
  s.datasets = {projection:projection/*.filter(d => d.y>-100)*/}
  var r = {}
  // todo independent scales Object.entries(viz_spec.independent_scales).filter(([k,v]) => v == true).forEach(([k,v]) => { r[k] = 'independent' })
  //s.resolve = {scale: r}
  if (spec.mark == "bar") s.mark = "bar"; //{"type": "bar", "tooltip": true};
  var p = s.mark;
  s.mark = {type: p, tooltip:true}
  if (spec.mark == "line") {s.mark.point = true; s.encoding.order={field:spec.channels.detail_only_proj}/*s.encoding.size = {value:20}*/}
  if (spec.mark == "point") {s.encoding.size = {value:100};
  s.mark.strokeWidth = 5};
  
  //s["config"] ={"legend": {"disable": true}}

  //s['encoding']['x']['axis']['labelAngle'] = 0 //.x.axis.labelAngle=0// = {"orient": "top", "labelAngle":0};

  return s
}
Code
vega_interactive = { // credit to Mike Bostock (starting point): https://observablehq.com/@mbostock/hello-vega-embed
  const v = window.vega = await require("vega");
  const vl = window.vl = await require("vega-lite");
  const ve = await require("vega-embed");
  async function vega(spec, options) {
    const div = document.createElement("div");
        div.setAttribute('id','chart-out');
    div.value = (await ve(div, spec, options)).view;
    div.value.addEventListener('mousemove', (event, item) => {
      //console.log(item);
      if (item != undefined && item.datum != undefined && item.datum.formula != undefined) {
              /*DN off viewof formula_select.value = item.datum.formula;
      viewof formula_select.dispatchEvent(new CustomEvent("input"))
*/
      }
    })
    div.value.addEventListener('click', (event, item) => {
      console.log(item.datum);
      /*viewof formula_select.value = item.datum.formula;
      viewof formula_select.dispatchEvent(new CustomEvent("input"));
      viewof month_select.value = item.datum.month_in;
      viewof month_select.dispatchEvent(new CustomEvent("input"));*/
      //if (item.datum.age_0_in != undefined) {viewof inputs.value = {...viewof inputs.value, age_0_in:item.datum.age_0_in}; //cursor.month_in.push(item.datum.month_in);
      //viewof inputs.dispatchEvent(new CustomEvent("input"))};

       //     if (item.datum.age_in != undefined) {viewof inputs.value = {...viewof inputs.value, age_in:item.datum.age_in}; //cursor.month_in.push(item.datum.month_in);
      //viewof inputs.dispatchEvent(new CustomEvent("input"))};

      //viewof inputs.value = { ...viewof inputs.value, ...item.datum };
      //viewof inputs.dispatchEvent(new CustomEvent("input"))

      /*viewof formula_select.value = item.datum.formula;
      viewof formula_select.dispatchEvent(new CustomEvent("input"))
*/
//y.value.insert('selection_age_0_in_store', [{unit: "concat_1_layer_0", fields: [{"field":"age_0_in","channel":"x","type":"E"}]    , values:[10]}])
//y.value.insert('selection_dampener_in_store', [{unit: "concat_2_concat_2_layer_0", fields: [{"field":"dampener_in","channel":"x","type":"E"}] , values:[0.95]}])
  //.run()
      
// NOW TODO addSignalListener stuff... is it trigerred for above??
// until there is a selection api...
//https://github.com/vega/vega-lite/issues/2790#issuecomment-976633121
// https://github.com/vega/vega-lite/issues/1830#issuecomment-926138326
      
    }) // DN
    return div;
  }
  vega.changeset = v.changeset;
  return vega;
}
Code
// reference: https://observablehq.com/@mbostock/saving-svg
serializeSVG = {
  const xmlns = "http://www.w3.org/2000/xmlns/";
  const xlinkns = "http://www.w3.org/1999/xlink";
  const svgns = "http://www.w3.org/2000/svg";
  return function serialize(svg) {
    svg = svg.cloneNode(true);
    const fragment = window.location.href + "#";
    const walker = document.createTreeWalker(svg, NodeFilter.SHOW_ELEMENT);
    while (walker.nextNode()) {
      for (const attr of walker.currentNode.attributes) {
        if (attr.value.includes(fragment)) {
          attr.value = attr.value.replace(fragment, "#");
        }
      }
    }
    svg.setAttributeNS(xmlns, "xmlns", svgns);
    svg.setAttributeNS(xmlns, "xmlns:xlink", xlinkns);
    const serializer = new window.XMLSerializer;
    const string = serializer.serializeToString(svg);
    return new Blob([string], {type: "image/svg+xml"});
  };
}
Code
import {inputs_default} from "./simple-loan.ojs" // for enable/disable of inputs

import {viewof ui} with {uis, introspection, mutable inputs_history} from "./simple-loan.ojs" // for enable/disable of inputs

  // AA:
  import {introspection as introspection_0, spec_post_process/*, viewof ui*//*, domains*//*, mutable inputs_history*/, viewof field, uis as uis_0, spec as spec_0, mapped as mapped_0 } from "./simple-loan.ojs"




  import { domains as domains_0 } with {viewof ui} from "./simple-loan.ojs"


model_0 = require('../../models/loan-validator/simple-loan.js');
//introspection_0 = await FileAttachment('../../models/loan-validator/simple-loan-nomemo.introspection.json').json({typed:true});

cul_0_0 = await FileAttachment('../../models/loan-validator/simple-loan-nomemo_esm/cul_scope_0.cul.js').text();
esm_0_0 = await FileAttachment('../../models/loan-validator/simple-loan-nomemo_esm/cul_scope_0.mjs').text();
Code
models = [model_0,]
uis1 = [uis_0,]
specs = [spec_0,]
cul_0s = [cul_0_0,]
esm_0s = [esm_0_0,]
introspections = [introspection_0,]
domains1 = [domains_0,]
mappeds = [mapped_0,]



viewof step = Inputs.range([0,1], {label:'spec', value:q.get('spec') ?? 0, step:1})

viewof shadow_step = Inputs.range([0,1], {label:'shadow spec', value:q.get('spec') ?? 0, step:0.5}) // hmmmm

viewof step_next = Inputs.range([0,1], {label:'spec next', value: step ==0 ? 0 : step + 1, step:1})


emojis = [/*"🅾️"*/"⏮️","➡️1️⃣","➡️2️⃣","➡️3️⃣","➡️4️⃣","➡️5️⃣","➡️6️⃣"]//➡️


model = models[step]
uis = uis1[step]
spec = specs[step]
cul_0 = cul_0s[step]
esm_0 = esm_0s[step]
introspection = introspections[step]
domains = domains1[step]
mapped = mappeds[step]



domains_next = domains1[step_next]
mapped_next = mappeds[step_next]

model_next = models[step_next]
introspection_next = introspections[step_next]
Source Code
---
title: "simple loan model"
author:
  - name: "Declan Naughton 🧮👨‍💻🧉"
    url: "https://calcwithdec.dev/about.html"
description: "simple loan model"
execute:
  echo: false # set to false for publish
code-tools: true
format:
  html:
    #page-layout: full
    page-layout: custom
    resources:
      - "../../models/loan-validator/*"
      # specs?
    code-fold: true
    embed-resources: false # faster with true, I think
---



```{ojs}
md`##### input values ⚙️`

viewof ui

```



```{ojs}
stepp = step

mutable playing = 0


//viewof field == undefined ? '' : viewof field // move to conditional
```

---


::: {layout-ncol=2}

::: {.panel-tabset}

## 📺

```{ojs}
mutable spec_now = spec_post_process(c_spec1)

x = vega_interactive(spec_now, { renderer: 'svg'})
```

## 📈

## 👀

I need sep./addl. UIs for this.

```{ojs}
/*projection_fn ({ mapped:['formula', 'price_in'], // no 'value'
  domains: {price_in:[5,6,7], formula:['profit','costs','revenue']},
  cursor:{cost_in:4,cups_in:75}
                 })*/

domains11 = ({...domains, formula: Object.values(introspection.cul_functions).filter(d => d.reason == 'definition' && inputs.indexOf(d.name+'_in') == -1).map(d => d.name) /*domains.formula??domains.formulae*/})

p = projection_fn ({ mapped:['formula'], // no 'value'
  domains: domains11, cursor:ui // problems when something missing from cursor, in this case an input mapped in the main story
                 })

xx = vega_interactive({ // modd from https://observablehq.com/@declann/some-cashflows?collection=@declann/calculang v~908
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "data": {"name": "projection"},
  "height": 480,
  "width": 550,
  "transform": [
    {"calculate": "round(100*datum.value)/100", "as": "amount2"}
  ],
  "mark": {"type": "text", "tooltip": true},
  "encoding": {
    "y": {"field": "formula", "axis": {"orient": "left", "labelAngle": 30}},
    /*"y": {
      "field": "month_in",
      "type": "quantitative",
      "sort": "descending",
      "axis": {"tickOffset": 2, "tickCount": 20, "grid": 0}
    },*/
    "color": {"field": "formula", "type": "nominal"},
    "text": {"field": "amount2", "format": ",.2f"}
  },
  "config": {"legend": {"disable": true}},
  "datasets": {
    "projection": p
  }
}, { renderer: 'svg'})

p


                 
```

:::

::: {.panel-tabset}

## calculang 📝💬

```{ojs}
md`
~~~js
${
formulae_objs.map(f => cul_0.split('\n').filter((d,i) => i >= f.loc.start.line-1 && i < f.loc.end.line).join('\n').slice(13)).join('\n\n')
}
~~~
`
```

## 🔗

formula-input dependence map (todo pre-pop all values?):

```{ojs}
md`
formula | ${inputs/*.map(d => d.slice(0, -3))*/.join(' | ')}
-------- | ${inputs.map(d => ':--------:').join(' | ')}
 | ${inputs.map(d => '<img width=80/>').join(' | ')}
${formulae_objs.map(f => `${f.name} | ${inputs.map(d => /*this will only work if I populate negs in cul_functions OR if I use cul_links. f.negs.includes(d) ? 'NEG' : */ (f.inputs.includes(d) ? '✔️' : '')).join(' | ')}`).join('\n')}
`
```

## 🌲

```{ojs}
fixedDot = {
 let start = introspection.dot.split('\n')

  let subgraph = []

  inputs.forEach(input => {
    subgraph.push(start.find(s => s.indexOf(`${input}" [`) != -1))
    subgraph.push(start.find(s => s.indexOf(`${input.slice(0,-3)}" [`) != -1))
  })

  let out = start.filter(d => subgraph.indexOf(d) == -1)

// https://graphviz.org/Gallery/directed/cluster.html
  out[0] = out[0] + `subgraph cluster_0 { style=filled;color=lightgrey; node [style=filled,color=white];label = "inputs";` + subgraph.join(';') + "}";
  
  return out.join('\n')
}


g = dot`${fixedDot}`

// todo svg download button

DOM.download(() => serializeSVG(g), undefined, "Save as SVG")
```

## ✨ js

```{ojs}
md`
~~~js
${
formulae_objs.map(f => esm_0.split('\n').filter((d,i) => i >= f.loc.start.line-1 && i < f.loc.end.line).join('\n').slice(13)).join('\n\n')
}
~~~
`

viewof raw = Inputs.radio(["raw", "nomemo"], {value:"nomemo", label:""})

```

## data ⬇

```{ojs}
Inputs.table(projection)
```

```{ojs}
DOM.download(serialize(projection), "projection", "csv download") // pivot/unpivot?

DOM.download(serializeJSON(spec_now), "spec", "spec download (VL)") // pivot/unpivot?
```

:::

:::

.

.

.

.

*You may ignore notebook workings below this line*


## debug

```{ojs}
html`formula must be defined in here:`

input_combos_projection

`used for cp`

input_domains_projection

introspection

formulae

domains

ui

html`here`

input_domains // but this doesn't ?

input_domains_projection

input_combos_projection

mapped

projection

inputs_history // this updates

html`encodings:`

encodings
```

## other

```{ojs}
q = new URLSearchParams(location.search)

formulae = Object.values(introspection.cul_functions).filter(d => d.reason == 'definition' && inputs.indexOf(d.name+'_in') == -1).map(d => d.name)

formulae_next = Object.values(introspection_next.cul_functions).filter(d => d.reason == 'definition' && inputs.indexOf(d.name+'_in') == -1).map(d => d.name)

formulae_objs = Object.values(introspection.cul_functions).filter(d => d.reason == 'definition' && inputs.indexOf(d.name+'_in') == -1)

mutable inputs_history = inputs_default

domains

input_domains = {
  // if formula mapped => not something to include
  var o = {}
  mapped.filter(d => !formulae.includes(d)).forEach(i => { // only use mapped
    
    o[i] = domains[i]
  })
  if (mapped.includes("interaction"))
    o.interaction = inputs_history.map((d,i) => i)
  return o
}

input_domains_next = {
  // if formula mapped => not something to include
  var o = {}
  mapped_next.filter(d => !formulae_next.includes(d)).forEach(i => { // only use mapped
    o[i] = domains_next[i]
  })
  if (mapped_next.includes("interaction"))
    o.interaction = inputs_history.map((d,i) => i)
  return o
}


input_combos_projection = cartesianProduct(Object.entries(input_domains_projection).map(([k,v]) => ({[k == 'formulae' ? 'formula' : k]: v})))

input_combos_projection_next = cartesianProduct(Object.entries(input_domains_projection_next).map(([k,v]) => ({[k == 'formulae' ? 'formula' : k]: v})))

```

```{ojs}
// cursor shouldn't include invalid inputs

// this is not designed to replace existing code - e.g. formulae needs to change to formula in specs
// this relies on domains.formula having revelent list of model formulae if formula is mapped

function projection_fn ({mapped, domains, cursor}) {
  
  let input_domains_projection = {}
  Object.entries(cursor).forEach(([k,v]) => {
    input_domains_projection[k] = [v] // inputs defined in cursor => a one-entry array
  })
  
  Object.entries(domains).forEach(([k,v]) => {
    if (mapped.includes(k)) input_domains_projection[k] = v // mapped domain => include that domain
  })

  let input_combos_projection = cartesianProduct(Object.entries(input_domains_projection).map(([k,v]) => ({[k]:v})))

  // inputs...|formula|value
  // ^ whenever a set of formulae mapped

  // inputs...|formulae...
  // ^ whenever >1 formula is mapped
  // =1 case also falls here, its ok

  /*let sets = 0; // I could just look for formula in mapped, since
  mapped.forEach(m => {
    if (m.slice(-3) == '_in') return;
    if (m != 'interaction')
      sets++;
    // not even checking domains keys
  })*/

  if (mapped.includes('formula')) {

    //return input_combos_projection.map(combos => ({...combos, value: /*+*/model[combos.formula](combos)}))

    let o = []

    input_combos_projection.forEach(combos => {
      let ans = 'ERROR'; // or NaN for viz purposes?
      try {
        ans = /*+*/model[combos.formula](combos)
      } catch(e) {
        console.log(e)
      }

      o.push({...combos, value:ans})
    })

    return o;

  } else {
    
    let o = [];

    input_combos_projection.forEach(combos => {
      let oo = {...combos};

      mapped.filter(m => m.slice(-3) != '_in') // 'interaction' case todo
        .forEach(f => {
          oo[f] = /*+*/model[f](oo);
        })

      o.push(oo);

    })

    return o;

  }
}

```

```{ojs}
projection = {
  if (mapped.includes('formulae'))
    return input_combos_projection.map(combos => {
      if (!mapped.includes('interaction')) return combos
      if (combos.interaction == inputs_history.length-1) return combos
      else
        return ({...combos, ...inputs_history[combos.interaction]/*adding too much here e.g. t_interval, restrict to inputs?*/}) // cant set to input histories here !
    }).map(combos => ({...combos, value: /*+*/model[combos.formula](combos)/*.toFixed(2)*/}))
  else {
    // do all mapped formulae at once
    var o = [];
   input_combos_projection.forEach(combo => {
    var oo = (combos => {
          if (!mapped.includes('interaction')) return combos
      if (combos.interaction == inputs_history.length-1) return combos
else
        return ({...combos, ...inputs_history[combos.interaction]/*adding too much here e.g. t_interval, restrict to inputs?*/}) // cant set to input histories here !
    //mapped.filter(d => formulae.includes(d)).for
    })(combo);
      mapped.filter(d => formulae.includes(d)).forEach(formula => {
        var oooo = {}
        Object.keys(oo).forEach(k => {
          if (k.slice(-3) == '_in') oooo[k] = oo[k]
        })
        oo[formula] = /*+*/model[formula](oooo); // interaction going into call and other things on scatters messes up memo when its needed where randomness is used in model
      })
     o.push(oo);
   })

    return o
     
     
     //combos => ({...combos, [formula]}))
    
    //mapped.filter(formula => formulae.includes(formula)).map(formula => input_combos_projection.map(combos => ({...combos, [formula]}))
    // here look for mapped functions and loop/flatten
    // what will be in c-p for functions? nothing?
  }
}

projection_next = {
  if (mapped_next.includes('formulae'))
    return input_combos_projection_next.map(combos => {
      if (!mapped_next.includes('interaction')) return combos
      if (combos.interaction == inputs_history.length-1) return combos
      else
        return ({...combos, ...inputs_history[combos.interaction]/*adding too much here e.g. t_interval, restrict to inputs?*/}) // cant set to input histories here !
    }).map(combos => ({...combos, value: /*+*/model_next[combos.formula](combos)/*.toFixed(2)*/}))
  else {
    // do all mapped formulae at once
    var o = [];
   input_combos_projection_next.forEach(combo => {
    var oo = (combos => {
          if (!mapped_next.includes('interaction')) return combos
      if (combos.interaction == inputs_history.length-1) return combos
else
        return ({...combos, ...inputs_history[combos.interaction]/*adding too much here e.g. t_interval, restrict to inputs?*/}) // cant set to input histories here !
    //mapped.filter(d => formulae.includes(d)).for
    })(combo);
      mapped_next.filter(d => formulae.includes(d)).forEach(formula => {
        var oooo = {}
        Object.keys(oo).forEach(k => {
          if (k.slice(-3) == '_in') oooo[k] = oo[k]
        })
        oo[formula] = /*+*/model_next[formula](oooo); // interaction going into call and other things on scatters messes up memo when its needed where randomness is used in model
      })
     o.push(oo);
   })

    return o
     
     
     //combos => ({...combos, [formula]}))
    
    //mapped.filter(formula => formulae.includes(formula)).map(formula => input_combos_projection.map(combos => ({...combos, [formula]}))
    // here look for mapped functions and loop/flatten
    // what will be in c-p for functions? nothing?
  }
}
```

```{ojs}
inputs = Object.values(introspection.cul_functions).filter(d => d.reason == 'input definition').map(d => d.name).sort()

inputs_next = Object.values(introspection_next.cul_functions).filter(d => d.reason == 'input definition').map(d => d.name).sort()


input_domains_projection = { // think RE blanket using all here ! filter for inputs? See t_interval
  var o = {}
  Object.entries(/*viz_spec.cursor*/ui).filter(([k,v]) => inputs.includes(k)).forEach(([k,v]) => {
    
    o[k] = [v]
  })
  Object.entries(input_domains).forEach(([k,v]) => {
    o[k] = v
  })
  return o
}

input_domains_projection_next = { // think RE blanket using all here ! filter for inputs? See t_interval
  var o = {}
  Object.entries(/*viz_spec.cursor*/ui).filter(([k,v]) => inputs_next.includes(k)).forEach(([k,v]) => {
    
    o[k] = [v]
  })
  Object.entries(input_domains_next).forEach(([k,v]) => {
    o[k] = v
  })
  return o
}
```

```{ojs}
// https://stackoverflow.com/questions/18957972/cartesian-product-of-objects-in-javascript
function cartesianProduct(input, current) {
    if (!input || !input.length) { return []; }
 
    var head = input[0];
    var tail = input.slice(1);
    var output = [];
 
     for (var key in head) {
       for (var i = 0; i < head[key].length; i++) {
             var newCurrent = copy(current);         
             newCurrent[key] = head[key][i];
             if (tail.length) {
                  var productOfTail = 
                          cartesianProduct(tail, newCurrent);
                  output = output.concat(productOfTail);
             } else output.push(newCurrent);
        }
      }    
     return output;
 }

// https://stackoverflow.com/questions/18957972/cartesian-product-of-objects-in-javascript

function copy(obj) {
   var res = {};
   for (var p in obj) res[p] = obj[p];
   return res;
 }
 
```

```{ojs}
json2csv = require("json2csv@5.0.7/dist/json2csv.umd.js")

// thx to https://observablehq.com/@palewire/saving-csv
// Ben Welsh and comments from Christophe Yamahata
function serialize (data) {
 let parser = new json2csv.Parser();
 let csv = parser.parse(data);
 return new Blob([csv], {type: "text/csv"}) 
}


function serializeJSON (data) {
 return new Blob([JSON.stringify(data,null,2)], {type: "text/json"}) 
}
```

```{ojs}
cql = require("compassql")

schema = cql.schema.build([...projection, ...projection, ...projection, ...projection])

encodings = Object.entries(spec.channels).filter(([k,v]) => k != 'detail_only_proj').map(([k,v]) => ({type:v.type??'nominal', channel:k, field:(v.name??v) == 'formulae' ? 'formula' : (v.name??v)/*nominal, type todo*/}))

output = cql.recommend({
  spec: {data: projection,
    mark: spec.mark == 'bar' ? 'line' : spec.mark,
    encodings
  },
  chooseBy: "effectiveness",
}, schema);

vlTree = cql.result.mapLeaves(output.result, function (item) {
  return item//.toSpec();
});

c_spec = vlTree.items[0].toSpec()

c_spec1 = {
  var s = c_spec;
  s.data = {name: 'projection'};
  s.width = /*viz.width ??*/ 500;
  s.height = spec.height;
  s.datasets = {projection:projection/*.filter(d => d.y>-100)*/}
  var r = {}
  // todo independent scales Object.entries(viz_spec.independent_scales).filter(([k,v]) => v == true).forEach(([k,v]) => { r[k] = 'independent' })
  //s.resolve = {scale: r}
  if (spec.mark == "bar") s.mark = "bar"; //{"type": "bar", "tooltip": true};
  var p = s.mark;
  s.mark = {type: p, tooltip:true}
  if (spec.mark == "line") {s.mark.point = true; s.encoding.order={field:spec.channels.detail_only_proj}/*s.encoding.size = {value:20}*/}
  if (spec.mark == "point") {s.encoding.size = {value:100};
  s.mark.strokeWidth = 5};
  
  //s["config"] ={"legend": {"disable": true}}

  //s['encoding']['x']['axis']['labelAngle'] = 0 //.x.axis.labelAngle=0// = {"orient": "top", "labelAngle":0};

  return s
}
```

```{ojs}
vega_interactive = { // credit to Mike Bostock (starting point): https://observablehq.com/@mbostock/hello-vega-embed
  const v = window.vega = await require("vega");
  const vl = window.vl = await require("vega-lite");
  const ve = await require("vega-embed");
  async function vega(spec, options) {
    const div = document.createElement("div");
        div.setAttribute('id','chart-out');
    div.value = (await ve(div, spec, options)).view;
    div.value.addEventListener('mousemove', (event, item) => {
      //console.log(item);
      if (item != undefined && item.datum != undefined && item.datum.formula != undefined) {
              /*DN off viewof formula_select.value = item.datum.formula;
      viewof formula_select.dispatchEvent(new CustomEvent("input"))
*/
      }
    })
    div.value.addEventListener('click', (event, item) => {
      console.log(item.datum);
      /*viewof formula_select.value = item.datum.formula;
      viewof formula_select.dispatchEvent(new CustomEvent("input"));
      viewof month_select.value = item.datum.month_in;
      viewof month_select.dispatchEvent(new CustomEvent("input"));*/
      //if (item.datum.age_0_in != undefined) {viewof inputs.value = {...viewof inputs.value, age_0_in:item.datum.age_0_in}; //cursor.month_in.push(item.datum.month_in);
      //viewof inputs.dispatchEvent(new CustomEvent("input"))};

       //     if (item.datum.age_in != undefined) {viewof inputs.value = {...viewof inputs.value, age_in:item.datum.age_in}; //cursor.month_in.push(item.datum.month_in);
      //viewof inputs.dispatchEvent(new CustomEvent("input"))};

      //viewof inputs.value = { ...viewof inputs.value, ...item.datum };
      //viewof inputs.dispatchEvent(new CustomEvent("input"))

      /*viewof formula_select.value = item.datum.formula;
      viewof formula_select.dispatchEvent(new CustomEvent("input"))
*/
//y.value.insert('selection_age_0_in_store', [{unit: "concat_1_layer_0", fields: [{"field":"age_0_in","channel":"x","type":"E"}]    , values:[10]}])
//y.value.insert('selection_dampener_in_store', [{unit: "concat_2_concat_2_layer_0", fields: [{"field":"dampener_in","channel":"x","type":"E"}] , values:[0.95]}])
  //.run()
      
// NOW TODO addSignalListener stuff... is it trigerred for above??
// until there is a selection api...
//https://github.com/vega/vega-lite/issues/2790#issuecomment-976633121
// https://github.com/vega/vega-lite/issues/1830#issuecomment-926138326
      
    }) // DN
    return div;
  }
  vega.changeset = v.changeset;
  return vega;
}
```


```{ojs}
// reference: https://observablehq.com/@mbostock/saving-svg
serializeSVG = {
  const xmlns = "http://www.w3.org/2000/xmlns/";
  const xlinkns = "http://www.w3.org/1999/xlink";
  const svgns = "http://www.w3.org/2000/svg";
  return function serialize(svg) {
    svg = svg.cloneNode(true);
    const fragment = window.location.href + "#";
    const walker = document.createTreeWalker(svg, NodeFilter.SHOW_ELEMENT);
    while (walker.nextNode()) {
      for (const attr of walker.currentNode.attributes) {
        if (attr.value.includes(fragment)) {
          attr.value = attr.value.replace(fragment, "#");
        }
      }
    }
    svg.setAttributeNS(xmlns, "xmlns", svgns);
    svg.setAttributeNS(xmlns, "xmlns:xlink", xlinkns);
    const serializer = new window.XMLSerializer;
    const string = serializer.serializeToString(svg);
    return new Blob([string], {type: "image/svg+xml"});
  };
}
```

```{ojs}
  import {inputs_default} from "./simple-loan.ojs" // for enable/disable of inputs

import {viewof ui} with {uis, introspection, mutable inputs_history} from "./simple-loan.ojs" // for enable/disable of inputs

  // AA:
  import {introspection as introspection_0, spec_post_process/*, viewof ui*//*, domains*//*, mutable inputs_history*/, viewof field, uis as uis_0, spec as spec_0, mapped as mapped_0 } from "./simple-loan.ojs"




  import { domains as domains_0 } with {viewof ui} from "./simple-loan.ojs"


model_0 = require('../../models/loan-validator/simple-loan.js');
//introspection_0 = await FileAttachment('../../models/loan-validator/simple-loan-nomemo.introspection.json').json({typed:true});

cul_0_0 = await FileAttachment('../../models/loan-validator/simple-loan-nomemo_esm/cul_scope_0.cul.js').text();
esm_0_0 = await FileAttachment('../../models/loan-validator/simple-loan-nomemo_esm/cul_scope_0.mjs').text();


```

```{ojs}
models = [model_0,]
uis1 = [uis_0,]
specs = [spec_0,]
cul_0s = [cul_0_0,]
esm_0s = [esm_0_0,]
introspections = [introspection_0,]
domains1 = [domains_0,]
mappeds = [mapped_0,]



viewof step = Inputs.range([0,1], {label:'spec', value:q.get('spec') ?? 0, step:1})

viewof shadow_step = Inputs.range([0,1], {label:'shadow spec', value:q.get('spec') ?? 0, step:0.5}) // hmmmm

viewof step_next = Inputs.range([0,1], {label:'spec next', value: step ==0 ? 0 : step + 1, step:1})


emojis = [/*"🅾️"*/"⏮️","➡️1️⃣","➡️2️⃣","➡️3️⃣","➡️4️⃣","➡️5️⃣","➡️6️⃣"]//➡️


model = models[step]
uis = uis1[step]
spec = specs[step]
cul_0 = cul_0s[step]
esm_0 = esm_0s[step]
introspection = introspections[step]
domains = domains1[step]
mapped = mappeds[step]



domains_next = domains1[step_next]
mapped_next = mappeds[step_next]

model_next = models[step_next]
introspection_next = introspections[step_next]

// for now assume static spec, minus datasets
//spec_next = spec_now //specs[step_next]

```
 
 

Made with 💓 and calculang