Published
Edited
Apr 10, 2021
Importers
5 stars
Insert cell
Insert cell
/**
* Generates a dot-formatted file for Graphviz to visualize the dataflow
* graph for a Vega View instance.
* Note: does not (yet) support nested scopes / subflows.
* @param {View} view A Vega View instance.
* @param {number} [stamp] Optional pulse timestamp. If provided, dataflow
* nodes and edges that have not been evaluated since the timestamp will
* be deemphasized.
* @returns {string} The generated dot file.
*/
view2dot = function(view, stamp) {
const rt = view._runtime,
ops = rt.nodes,
keys = Object.keys(ops);
const signals = Object.keys(rt.signals).reduce((lut, name) => {
lut[rt.signals[name].id] = name;
return lut;
}, {});

const scales = Object.keys(rt.scales).reduce((lut, name) => {
lut[rt.scales[name].id] = name;
return lut;
}, {});
const data = Object.keys(rt.data).reduce((lut, name) => {
const sets = rt.data[name];
if (sets.input) lut[sets.input.id] = name;
if (sets.output) lut[sets.output.id] = name;
return lut;
}, {});

// build node objects
const nodes = keys.map(key => {
const op = ops[key];
const node = {
id: op.id,
type: op.constructor.name.toLowerCase(),
stamp: op.stamp,
value: op
};
if (markTypes[getType(node.type)]) node.isMark = true;
if (signals[op.id]) node.signal = signals[op.id];
if (scales[op.id]) node.scale = scales[op.id];
if (data[op.id]) node.data = data[op.id];
if (rt.root === op) node.root = true;
return node;
});
const ids = nodes.reduce((lut, node) => {
lut[node.id] = node;
return lut;
}, {});
// build edge objects
const edges = [];
keys.forEach(key => {
const op = ops[key];
if (op._targets) op._targets.forEach(t => {
if (!ids[t.id]) return;
edges.push({
nodes: [op.id, t.id],
param: t.source === op ? 'pulse' : argop(t, op)
});
if (t.source === op && ids[op.id].isMark) {
let node = ids[t.id];
if (getType(node.type) === 'collect') {
// annotate post-datajoin collect operators as mark-processing
node.isMark = true;
}
}
});
});
return `digraph {
rankdir = LR;
node [style=filled];
${nodes.map(node => {
return node.id
+ ' [label="' + nodeLabel(node) + '"]'
+ ' [color="' + nodeColor(node, stamp) + '"]'
+ ' [fillcolor="' + nodeFillColor(node, stamp) + '"]'
+ ' [fontcolor="' + nodeFontColor(node, stamp) + '"]';
})
.join(';\n ')};
${edges.map(e => {
return e.nodes.join(' -> ')
+ ' [label="' + (e.param === 'pulse' ? '' : e.param) + '"]'
+ ' [color="' + edgeColor(e, ids, stamp) + '"]'
+ ' [fontcolor="' + edgeLabelColor(e, ids, stamp) + '"]'
+ ' [weight="' + edgeWeight(e, ids) + '"]';
}).join(';\n ')};
}`;
}
Insert cell
nodeLabel = function(node) {
return node.signal ? node.signal
: node.scale ? node.scale
: node.root ? 'root'
: getType(node.type) === 'collect' && node.data ? node.data
: getType(node.type)
}
Insert cell
nodeFillColor = function(node, stamp) {
return stamp && node.value.stamp < stamp ? '#ffffff'
: node.signal ? '#dddddd'
: node.scale ? '#ccffcc'
: node.data ? '#ffcccc'
: getType(node.type) === 'axisticks' || getType(node.type) === 'legendentries' ? '#ffcccc'
: node.isMark || node.root ? '#ccccff'
: '#ffffff';
}
Insert cell
nodeColor = function(node, stamp) {
return stamp && node.value.stamp < stamp ? '#dddddd' : '#000000';
}
Insert cell
nodeFontColor = function(node, stamp) {
return stamp && node.value.stamp < stamp ? '#cccccc' : '#000000';
}
Insert cell
edgeColor = function(edge, nodes, stamp) {
const n = edge.nodes;
return stamp && nodes[n[0]].value.stamp < stamp ? '#dddddd'
: edge.param !== 'pulse' ? '#aaaaaa'
: '#000000';
}
Insert cell
edgeLabelColor = function(edge, nodes, stamp) {
const n = edge.nodes;
return stamp && nodes[n[0]].value.stamp < stamp ? '#dddddd'
: '#000000';
}
Insert cell
edgeWeight = function(edge, nodes) {
const n = edge.nodes;
return edge.param !== 'pulse' ? 1
: nodes[n[0]].isMark && nodes[n[1]].isMark ? 100
: 2;
}
Insert cell
argop = function(t, s) {
if (t._argops) for (let v of t._argops) {
if (v.op === s) return v.name;
}
return '';
}
Insert cell
getType = function(type) {
const cut = type ? type.indexOf('$') : -1;
return cut < 0 ? type : type.slice(0, cut);
}
Insert cell
Insert cell
Insert cell
scene2dot = function(view) {
const nodes = [];
const edges = [];
const lut = new Map();
function visitor(parent, node, index) {
const n = {
id: nodes.length + 1,
type: node.marktype,
role: node.role,
index: index
};
nodes.push(n);
lut.set(node, n);

if (parent) {
edges.push(lut.get(parent).id + ' -- ' + n.id);
}
}
visitScene(visitor, view.scenegraph().root);

return `graph {
rankdir = TB;
${nodes.map(n => n.id
+ ' [label="' + (n.type ? (n.type + ':' + n.role) : n.index) + '"]'
+ ' [shape="' + (n.type ? 'rect' : 'circle') + '"]'
).join(';\n ')};
${edges.join(';\n ')};
}`;
}
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more