- Model Visuals 📊
- Coffee
- coffee demand curve model
coffee demand curve model
example of overriding a formula & modularity (DEV)
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
})
//viewof field_wildcard = Inputs.select(["cost_in","price_in"])
//viewof field
//field_wildcard
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": {
"x": {"field": "formula", "axis": {"orient": "top", "labelAngle": 0}},
"y": {
"field": field,
"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
Code
viewof cul_scope_id = Inputs.radio(_.range(0,Object.keys(introspection.cul_scope_ids_to_resource).length), {label: "cul_scope_id", value: 0 /*maybe nice to default to last one instead?*/})
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?):
missing cups formula!
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
links = Object.entries(introspection.cul_scope_ids_to_resource).filter(([cul_scope_id]) => cul_scope_id != 0).map(([cul_scope_id, resource]) => new URLSearchParams(resource.split('?').pop()).get('cul_scope_id') + ' -> ' + new URLSearchParams(resource).get('cul_parent_scope_id'))
nodes = Object.entries(introspection.cul_scope_ids_to_resource).map(d => (`${d[0]} [${ d[0] == 0 ? 'color="green" style="filled" ' : 'color="yellow" style="filled" '}label="[${d[0]}]: ${qs.length ? d[1] : d[1].split('?')[0]}"]`))
d = `digraph {
rankdir="RL"
node [shape="box"];
${nodes.join('\n')}
${links.join('\n')}
}`
calculang scope id graph:
.
.
.
.
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
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 "./coffee-demand-curve.ojs" // for enable/disable of inputs
import {viewof ui} with {uis, introspection, mutable inputs_history} from "./coffee-demand-curve.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 "./coffee-demand-curve.ojs"
import { domains as domains_0 } with {viewof ui} from "./coffee-demand-curve.ojs"
model_0 = require('../../models/coffee/coffee-demand-curve.js');
//introspection_0 = await FileAttachment('../../models/coffee/coffee-demand-curve-nomemo.introspection.json').json({typed:true});
//cul_0_0 = await FileAttachment('../../models/coffee/coffee-demand-curve-nomemo_esm/cul_scope_0.cul.js').text();
//esm_0_0 = await FileAttachment('../../models/coffee/coffee-demand-curve-nomemo_esm/cul_scope_0.mjs').text();
cul_0_0_fetch = await fetch(`../../models/coffee/coffee-demand-curve-nomemo_esm/cul_scope_${cul_scope_id}.cul.js`)
esm_0_0_fetch = await fetch(`../../models/coffee/coffee-demand-curve-nomemo_esm/cul_scope_${cul_scope_id}.mjs`)
cul_0_0 = cul_0_0_fetch.text()
esm_0_0 = esm_0_0_fetch.text()
done
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]