Public
Edited
Nov 13, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
info = html`Click something`
Insert cell
Insert cell
Insert cell
Insert cell
d3dotOptions = ({ width: width, height: 1000, fit: true, zoom: false })
Insert cell
Insert cell
function parseAssertions(inputAssertions) {
let statements = [];
let prepositionHash = {};
let groupsHash = {};
let hidesHash = {};
let groups = [];

// parse line by line
inputAssertions.split('\n').forEach((line, index) => {
var subject = '';
var object = '';
var preposition = '';
var isGrouping = false;
var isHide = false;

function isLowerCase(str) {
return str == str.toLowerCase() && str != str.toUpperCase();
}

// now walk the words in the line
const words = line.split(' ');
var comment = false;
words.forEach((word, index) => {
if (comment) return;
if (word == '') return;
if (word.match(/\/\/.*/)) {
comment = true;
return;
}
if (word.toUpperCase() == 'GROUPBY') {
isGrouping = true;
} else if (word.toUpperCase() == 'HIDE') {
isHide = true;
} else if (isLowerCase(word)) {
preposition = preposition + (preposition == '' ? '' : ' ') + word;
} else if (subject == '') {
subject = word;
} else if (preposition == '') {
subject = subject + ' ' + word;
} else {
object = object + (object == '' ? '' : ' ') + word;
}
});

// what kind of line is this?
if (isGrouping) {
// group by the preposition
groups.push(preposition);
groupsHash[preposition] = true;
} else if (isHide) {
hidesHash[preposition] = true;
} else if (subject != '' && preposition != '' && object != '') {
// TODO: consider making prepositions a node rather than a label
// as does conceptmaps.io
statements.push({
subject: subject,
preposition: preposition,
object: object
});

// keep track of all prepositions we have successfully parsed
// TODO: do we need this structure anymore?
if (!prepositionHash.hasOwnProperty(preposition)) {
prepositionHash[preposition] = {};
}
if (!prepositionHash[preposition].hasOwnProperty(object)) {
prepositionHash[preposition][object] = [];
}

prepositionHash[preposition][object].push(subject);
}
});

return {
statements: statements,
prepositionHash: prepositionHash,
groupsHash: groupsHash,
hidesHash: hidesHash,
groups: groups
};
}
Insert cell
function buildDot(statements) {
// create an array with nodes based on Subjects and Objects
let dotLines = `
strict digraph {
overlap=false;
splines=true;
pack=true;
start=random;
`;

statements.forEach((statement, index) => {
let subject = statement.subject;
let object = statement.object;
let preposition = statement.preposition;

dotLines += `\"${subject}\" [id=\"${subject}\"]\n`;
dotLines += `\"${object}\" [id=\"${object}\"]\n`;
dotLines += `\"${subject}\" -> \"${preposition + subject}\" [arrowhead=none]\n`;
dotLines += `\"${preposition + subject}\" [id =\"${preposition + subject}\" label="${preposition}" shape=plain]\n`;
dotLines += `\"${preposition + subject}\" -> \"${object}\"\n`;
});

dotLines = dotLines + "}";
return dotLines;
}
Insert cell
async function drawDot(stmnts, some, opts = {}) {
let graph = buildDot(parseAssertions(stmnts).statements);

if (!this) {
var div = html`<div>`;
} else {
div = this;
}

let options = {
useWorker: true,
width: null, // SVG width. Default: use original SVG width
height: null, // SVG height. Default: use original SVG height
scale: 1, // scale factor to scale up the graph with after possible fit to SVG size. Default: no scaling
fit: false, // If true, scale the graph automatically to fit the svg size. Default: don't fit
loadEvents: false,
duration: 250,
zoom: false
};
for (var option of Object.keys(opts)) {
options[option] = opts[option];
}
console.log(options);
let graphviz = d3.graphviz(div, options);

// attempt animation
graphviz.transition(function () {
return d3.transition().duration(options.duration);
});

// TODO: debounce multiple changes in short timeframes
await new Promise(function (resolve, reject) {
graphviz.renderDot(graph, function () {
resolve(this);
});
}).then((graphviz) => {
var node = d3.select(div);
node.selectAll("title").remove();
// fill all ellipses with white to improve pointer enter detection
// TODO: figure out if this is something to do with SVG painted/filled modes
node.selectAll("ellipse").attr("fill", "white");

node.selectAll(".node")
.on("pointerenter", function () {
d3.select(this).select("ellipse").attr("fill", "yellow");
})
.on("click", function () {
d3.select(some).text(this.id);
})
.on("pointerleave", function () {
d3.select(this).select("ellipse").attr("fill", "white");
});

node.selectAll("text").style("pointer-events", "none");
});

const zoom = d3.zoom().scaleExtent([1, 1]);
return div;
}
Insert cell
Insert cell
Insert cell
Insert cell
function drawVis(inputAssertions) {
let container = document.createElement("span");
let params = parseAssertions(inputAssertions);
let visGraph = buildVis(params);
visGraph.container = container;
drawVisInternal(visGraph);
return container;
}
Insert cell
function drawVisInternal(params) {
// create an array with nodes based on Subjects and Objects

let nodes = new vis.DataSet(params.nouns);

// create an array with edges
let edges = new vis.DataSet(params.linkages);

// create a network
let data = {
nodes: nodes,
edges: edges
};
let options = {
height: "1000",
autoResize: true,
edges: {
arrows: "to"
},
layout: {
randomSeed: 0 // keep the layout stable as we edit
}
};

let clusterOptions = {
joinCondition: function(nodeOptions) {
return params.groupsHash.hasOwnProperty(nodeOptions.preposition);
}
};
let network = new vis.Network(params.container, data, options);
network.clustering.cluster(clusterOptions);
}
Insert cell
vis = require('vis@4.21.0-EOL/dist/vis.js')
Insert cell
dot = require("@observablehq/graphviz@0.2")
Insert cell
Insert cell
import {adot} from "@magjac/d3-graphviz"
Insert cell
Insert cell
// based on the original with one slight mod
// original here:
// import {editor} from "@mbostock/comma-separated-tree"

function editor(value, callback = () => {}) {
const textarea = document.createElement("textarea");
textarea.value = value;
textarea.style.display = "block";
textarea.style.boxSizing = "border-box";
textarea.style.width = "calc(100% + 28px)";
textarea.style.font = "var(--monospace-font, var(--mono_fonts))";
textarea.style.minHeight = "60px";
textarea.style.border = "none";
textarea.style.padding = "4px 10px";
textarea.style.margin = "0 -14px";
textarea.style.background = "rgb(247,247,249)";
textarea.style.tabSize = 2;
textarea.style.resize = "none";
textarea.onkeypress = (event) => {
console.log("got input");
if (
event.key !== "Enter" ||
event.shiftKey ||
event.altKey ||
event.metaKey ||
event.ctrlKey
)
return;

// if you want to be told when enter is pressed, pass a callback
callback(textarea);

let i = textarea.selectionStart;
let j = textarea.selectionEnd;
let v = textarea.value;
if (i === j) {
let k = 0;
while (i > 0 && v[--i - 1] !== "\n");
while (i < j && v[i] === " ") ++i, ++k;
textarea.value =
v.substring(0, j) + "\n" + new Array(k + 1).join(" ") + v.substring(j);
textarea.selectionStart = textarea.selectionEnd = j + k + 1;
textarea.dispatchEvent(new CustomEvent("input"));
event.preventDefault();
}
};
textarea.oninput = () => {
textarea.style.height = `${textarea.value.match(/^/gm).length * 21 + 8}px`;
};
textarea.oninput();
return textarea;
}
Insert cell
Insert cell
// Concept is intended for use as a Markdown heading
// invoke it in a markdown cell
function Concept(concept) {
mutable concepts += concept + "\n";
return md`## ${concept}`;
}
Insert cell
d3 = require("d3@7", "d3-graphviz@2")
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