Journal to Graph

We want to convert the Journal into a Graph with page journal items as nodes.

We send a message to the Frame asking it to send us info about the page surrounding it. mdn

window.addEventListener("message", handler) let message = { action:"sendFrameContext" } window.parent.postMessage(message, "*")

We stop listening then process the data we got.

function handler ({data}) { if (data.action == "frameContext") { window.removeEventListener("message", handler) const {slug, item, page} = data const journal = JSON.stringify(page.journal) show(page, journal) } }

We define a class called Graph that represents a graph data structure. github

export class Graph { constructor(nodes=[], rels=[]) { this.nodes = nodes; this.rels = rels; }

The constructor takes two optional parameters nodes and rels which are arrays of nodes and relationships in the graph respectively.

The addNode method allows a new node to be added to the graph with a given type and properties, and returns the index of the new node in the nodes array.

addNode(type, props={}){ const obj = {type, in:[], out:[], props}; this.nodes.push(obj); return this.nodes.length-1; }

The addRel method allows a new relationship to be added between two nodes specified by their indices, with a given type and properties.

addRel(type, from, to, props={}) { const obj = {type, from, to, props}; this.rels.push(obj); const rid = this.rels.length-1; this.nodes[from].out.push(rid) this.nodes[to].in.push(rid); return rid; }

The tally method returns an object that contains the count of each node and relationship type in the graph.

tally(){ const tally = list => list.reduce((s,e)=>{s[e.type] = s[e.type] ? s[e.type]+1 : 1; return s}, {}); return { nodes:tally(this.nodes), rels:tally(this.rels)}; }

The size method returns the total number of nodes and relationships in the graph.

size(){ return this.nodes.length + this.rels.length; }

The load method is a static method that can be used to create a new Graph instance from a JSON object.

static load(obj) { return new Graph(obj.nodes, obj.rels) }

n(type=null, props={}) { let nids = Object.keys(this.nodes).map(key => +key) if (type) nids = nids.filter(nid => this.nodes[nid].type == type) for (const key in props) nids = nids.filter(nid => this.nodes[nid].props[key] == props[key]) return new Nodes(this, nids) } /** * Converts a graph to a JavaScript Object Notation (JSON) string using JSON.stringify. @param - replacer A function that transforms the results. @param - space Adds indentation, white space, and line break characters to the return- * @returns {string} JSON string containing serialized graph */ stringify(...args) { const obj = { nodes: this.nodes, rels: this.rels } return JSON.stringify(obj, ...args) } }

~

The page we show in the Journal to Graph frame below is the page on the screen which might be a ghost, maybe retrieved from history.

function show (page, journal) { const graph = new Graph(); //TODO Graph.load() const actions = page.journal.map(item => [ item.type, item.id ]) let html = `<h1>Journal to Graph</h1> <pre>${graph.stringify()}</pre> <pre> <== //TODO Graph.load(journal) </pre> <pre>${actions.join("\n")}</pre> <pre>${journal}</pre>` output.innerHTML = html }

Note: We plan to use the load method to create a new Graph instance from a Journal JSON object.

Frame: [⇒ Static Import Snippet, importjs.html]

//wiki.ralfbarkow.ch/assets/pages/js-snippet-template/importjs.html HEIGHT 200

~

We define a uniq function that returns a new array with all duplicate values removed.

const uniq = (value, index, self) => self.indexOf(value) === index

We define a class called Nodes that is used to represent a collection of nodes in a graph.

export class Nodes { constructor (graph, nids) { console.log('Nodes',{graph:graph.size(),type,nids}) this.graph = graph this.nids = nids }

The constructor takes two arguments: "graph" (an object representing the entire graph) and "nids" (an array of node IDs that belong to the current collection of nodes).

The class has several methods:

"i(type=null, props={})" – This method returns a new collection of relationship objects (represented by another class called "Rels") that originate from any of the nodes in the current collection. The method takes two optional arguments: "type" (a string representing a relationship type) and "props" (an object representing relationship properties). If provided, the method filters the resulting relationship objects based on these arguments.

i(type=null, props={}) { console.log('Nodes.i',{type,props}) let rids = this.nids.map(nid => this.graph.nodes[nid].in).flat().filter(uniq) if (type) rids = rids.filter(rid => this.graph.rels[rid].type == type) for (const key in props) rids = rids.filter(rid => this.graph.rels[rid].props[key] == props[key]) return new Rels(this.graph, rids) }

"o(type=null, props={})" – This method is similar to the "i" method, but instead returns a new collection of relationship objects that terminate at any of the nodes in the current collection.

o(type=null, props={}) { console.log('Nodes.o',{type,props}) let rids = this.nids.map(nid => this.graph.nodes[nid].out).flat().filter(uniq) if (type) rids = rids.filter(rid => this.graph.rels[rid].type == type) for (const key in props) rids = rids.filter(rid => this.graph.rels[rid].props[key] == props[key]) return new Rels(this.graph, rids) }

"props(key='name')" – This method returns an array of unique values for a given property key across all nodes in the current collection. By default, the "name" property is returned.

props(key='name') { console.log('Nodes.p',{key}) return this.nids.map(nid => this.graph.nodes[nid].props[key]).filter(uniq).sort() }

"types()" – This method returns an array of unique node types across all nodes in the current collection.

types() { return this.nids.map(nid => this.graph.nodes[nid].type).filter(uniq).sort() }

"tally()" – This method returns an object representing a tally of node types in the current collection.

tally(){ const tally = list => list.reduce((s,e)=>{s[e.type] = s[e.type] ? s[e.type]+1 : 1; return s}, {}); return { nodes:tally(this.nids.map(nid => this.graph.nodes[nid]))}; }

"size()" – This method returns the number of nodes in the current collection.

size(){ return this.nids.length }

"filter(f)" – This method returns a new collection of nodes that satisfy a given filter function "f". The filter function takes two arguments: "type" (a string representing a node type) and "props" (an object representing node properties).

filter(f) { const nodes = this.graph.nodes const nids = this.nids.filter(nid => { const node = nodes[nid] return f(node.type,node.props) }) return new Nodes(this.graph,nids) }

"map(f)" – This method returns an array of results obtained by applying a given function "f" to each node in the current collection. The function takes a single argument, a node object.

map(f) { const nodes = this.graph.nodes const result = this.nids.map(nid => { const node = nodes[nid] return f(node) }) return result } }

~

We define a class called Rels. The class takes two arguments graph and rids in its constructor method. The graph argument represents a graph data structure and rids represents an array of relationship ids in the graph.

export class Rels { constructor (graph, rids) { // console.log('Rels',{graph:graph.size(),type,rids}) this.graph = graph this.rids = rids }

The class has several methods defined in it, which are:

f(type=null, props={}): This method takes two optional arguments, type and props. It filters the nodes connected from the relationships in rids and returns the filtered nodes as an instance of the Nodes class. If type is provided, it further filters the nodes based on their type. If props is provided, it filters the nodes based on the values of their properties.

f(type=null, props={}) { // console.log('Rels.f',{type,props}) let nids = this.rids.map(rid => this.graph.rels[rid].from).filter(uniq) if (type) nids = nids.filter(nid => this.graph.nodes[nid].type == type) for (const key in props) nids = nids.filter(nid => this.graph.nodes[nid].props[key] == props[key]) return new Nodes(this.graph, nids) }

t(type=null, props={}): This method is similar to f(), but instead filters the nodes connected to the relationships in rids.

t(type=null, props={}) { // console.log('Rels.t',{type,props}) let nids = this.rids.map(rid => this.graph.rels[rid].to).filter(uniq) if (type) nids = nids.filter(nid => this.graph.nodes[nid].type == type) for (const key in props) nids = nids.filter(nid => this.graph.nodes[nid].props[key] == props[key]) return new Nodes(this.graph, nids) }

props(key='name'): This method returns an array of unique values of a given property (key) for all relationships in rids.

props(key='name') { // console.log('Rels.p',{key}) return this.rids.map(rid => this.graph.rels[rid].props[key]).filter(uniq).sort() }

types(): This method returns an array of unique relationship types for all relationships in rids.

types() { return this.rids.map(rid => this.graph.rels[rid].type).filter(uniq).sort() }

tally(): This method returns an object containing a tally of the relationship types in rids.

tally(){ const tally = list => list.reduce((s,e)=>{s[e.type] = s[e.type] ? s[e.type]+1 : 1; return s}, {}); return { rels:tally(this.rids.map(nid => this.graph.rels[nid]))}; }

size(): This method returns the number of relationships in rids.

size(){ return this.rids.length }

filter(f): This method takes a function f as an argument and returns a new instance of Rels containing only the relationships in rids for which f returns true.

filter(f) { const rels = this.graph.rels const rids = this.rids.filter(rid => { const rel = rels[rid] return f(rel.type,rel.props) }) return new Rels(this.graph,rids) }

map(f): This method takes a function f as an argument and returns an array of values obtained by applying f to each relationship in rids.

map(f) { const rels = this.graph.rels const result = this.rids.map(rid => { const rel = rels[rid] return f(rel) }) return result }

}