- Model Visuals 📊
- Donut
- donut 3d model - ASCII
donut 3d model - ASCII
    donut 3d model based on imperative models by Andy Sloane - ASCII
  
Author
    
  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'})
pformula-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").
.
.
.
You may ignore notebook workings below this line
debug
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
domainsCode
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 "./donut-3d-ascii.ojs" // for enable/disable of inputs
import {viewof ui} with {uis, introspection, mutable inputs_history} from "./donut-3d-ascii.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 "./donut-3d-ascii.ojs"
  import { domains as domains_0 } with {viewof ui} from "./donut-3d-ascii.ojs"
model_0 = require('../../models/donut/donut.js');
//introspection_0 = await FileAttachment('../../models/donut/donut-nomemo.introspection.json').json({typed:true});
cul_0_0 = await FileAttachment('../../models/donut/donut-nomemo_esm/cul_scope_0.cul.js').text();
esm_0_0 = await FileAttachment('../../models/donut/donut-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]