diff src/graph_view.ts @ 88:ac7ad087d474

graph view rewrites and fixes for the multi-subject table
author drewp@bigasterisk.com
date Tue, 30 Nov 2021 00:14:37 -0800
parents 067d66a45a51
children 955cde1550c3
line wrap: on
line diff
--- a/src/graph_view.ts	Thu Nov 25 18:52:07 2021 -0800
+++ b/src/graph_view.ts	Tue Nov 30 00:14:37 2021 -0800
@@ -1,87 +1,42 @@
-import { html, TemplateResult } from 'lit';
-import { DataFactory, Literal, Store, NamedNode, Quad, Quad_Object, Term, Util } from 'n3';
-
-import { SuffixLabels } from './suffixLabels';
+import { html, TemplateResult } from "lit";
+import { DataFactory, Literal, NamedNode, Quad, Store, Term, Util } from "n3";
+import { SuffixLabels } from "./suffixLabels";
+import {
+  groupByRdfType,
+  MultiSubjsTypeBlockLayout,
+  predsForSubj,
+  TypeToSubjs,
+} from "./tabulate";
 
 const { namedNode } = DataFactory;
 
-// // import ns from 'n3/src/IRIs';
-// // const { rdf } = ns;
-const rdf = {
-  type: namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")
-};
-
-type TypeToSubjs = Map<NamedNode, Set<NamedNode>>;
-// When there are multiple types, an arbitrary one is used.
-function groupByRdfType(
-  graph: Store
-): { byType: TypeToSubjs; untyped: Set<NamedNode> } {
-  const rdfType = rdf.type;
-  const byType: TypeToSubjs = new Map();
-  const untyped: Set<NamedNode> = new Set(); // subjs
-  const internSubjs = new Map<string, NamedNode>();
-  graph.forEach(
-    q => {
-      if (!Util.isNamedNode(q.subject)) {
-        throw new Error("unsupported " + q.subject.value);
-      }
-      const subj = q.subject as NamedNode;
-
-      let subjType: NamedNode | null = null;
-
-      graph.forObjects(
-        (o: Quad_Object) => {
-          subjType = o as NamedNode;
-        },
-        subj,
-        rdfType,
-        null
-      );
-
-      if (subjType !== null) {
-        // (subj, rdf:type, subjType) in graph
-        if (!byType.has(subjType)) {
-          byType.set(subjType, new Set());
-        }
-        (byType.get(subjType) as Set<NamedNode>).add(subj);
-      } else {
-        // no rdf:type stmt in graph
-        if (!internSubjs.has(subj.value)) {
-          internSubjs.set(subj.value, subj);
-        }
-        const intSubj: NamedNode = internSubjs.get(
-          subj.value as string
-        ) as NamedNode;
-        untyped.add(intSubj);
-      }
-    },
-    null,
-    null,
-    null,
-    null
-  );
-  return { byType: byType, untyped: untyped };
+// https://github.com/rdfjs/N3.js/issues/265
+if ((Literal.prototype as any).hashCode === undefined) {
+  (Literal.prototype as any).hashCode = () => 0;
 }
-
+if ((NamedNode.prototype as any).hashCode === undefined) {
+  (NamedNode.prototype as any).hashCode = () => 0;
+}
 class NodeDisplay {
   labels: SuffixLabels;
   constructor(labels: SuffixLabels) {
     this.labels = labels;
   }
-  getHtml(n: Term | NamedNode): TemplateResult {
+  render(n: Term | NamedNode): TemplateResult {
     if (Util.isLiteral(n)) {
       n = n as Literal;
       let dtPart: any = "";
-      if (n.datatype) {
+      if (
+        n.datatype &&
+        n.datatype.value != "http://www.w3.org/2001/XMLSchema#string" && // boring
+        n.datatype.value !=
+          "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString" //  boring
+      ) {
         dtPart = html`
-          ^^<span class="literalType">
-            ${this.getHtml(n.datatype)}
-          </span>
+          ^^<span class="literalType"> ${this.render(n.datatype)} </span>
         `;
       }
-      return html`
-        <span class="literal">${n.value}${dtPart}</span>
-      `;
+      return html` <span class="literal">${n.value}${dtPart}</span> `;
     }
 
     if (Util.isNamedNode(n)) {
@@ -92,7 +47,7 @@
         ["http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf:"],
         ["http://www.w3.org/2000/01/rdf-schema#", "rdfs:"],
         ["http://purl.org/dc/elements/1.1/", "dc:"],
-        ["http://www.w3.org/2001/XMLSchema#", "xsd:"]
+        ["http://www.w3.org/2001/XMLSchema#", "xsd:"],
       ]) {
         if (uriValue.startsWith(long)) {
           uriValue = short + uriValue.substr(long.length);
@@ -108,14 +63,10 @@
         uriValue = dn;
       }
 
-      return html`
-        <a class="graphUri" href="${n.value}">${uriValue}</a>
-      `;
+      return html` <a class="graphUri" href="${n.value}">${uriValue}</a> `;
     }
 
-    return html`
-      [${n.termType} ${n.value}]
-    `;
+    return html` [${n.termType} ${n.value}] `;
   }
 }
 
@@ -155,42 +106,31 @@
     );
   }
 
-  _subjBlock(subj: NamedNode) {
-    const predsSet: Set<NamedNode> = new Set();
-    this.graph.forEach(
-      (q: Quad) => {
-        predsSet.add(q.predicate as NamedNode);
-      },
-      subj,
-      null,
-      null,
-      null
-    );
-    const preds = Array.from(predsSet.values());
-    preds.sort();
+  _subjPredObjsBlock(subj: NamedNode) {
+    const columns = predsForSubj(this.graph, subj);
     return html`
       <div class="subject">
-        ${this.nodeDisplay.getHtml(subj)}
+        ${this.nodeDisplay.render(subj)}
         <!-- todo: special section for uri/type-and-icon/label/comment -->
         <div>
-          ${preds.map(p => {
-            return this._predBlock(subj, p);
+          ${columns.map((p) => {
+            return this._predObjsBlock(subj, p);
           })}
         </div>
       </div>
     `;
   }
 
-  _objBlock(obj: Term) {
+  _objCell(obj: Term) {
     return html`
       <div class="object">
-        ${this.nodeDisplay.getHtml(obj)}
+        ${this.nodeDisplay.render(obj)}
         <!-- indicate what source or graph said this stmt -->
       </div>
     `;
   }
 
-  _predBlock(subj: NamedNode, pred: NamedNode) {
+  _predObjsBlock(subj: NamedNode, pred: NamedNode) {
     const objsSet = new Set<Term>();
     this.graph.forEach(
       (q: Quad) => {
@@ -205,109 +145,69 @@
     objs.sort();
     return html`
       <div class="predicate">
-        ${this.nodeDisplay.getHtml(pred)}
-        <div>
-          ${objs.map(this._objBlock.bind(this))}
-        </div>
+        ${this.nodeDisplay.render(pred)}
+        <div>${objs.map(this._objCell.bind(this))}</div>
       </div>
     `;
   }
 
-  byTypeBlock(byType: TypeToSubjs, typeUri: NamedNode) {
-    const subjSet = byType.get(typeUri);
-    const subjs: Array<NamedNode> = subjSet ? Array.from(subjSet) : [];
-    subjs.sort();
-
-    const graphCells = new Map<string, Set<Term>>(); // [subj, pred] : objs
-    const makeCellKey = (subj: NamedNode, pred: NamedNode) =>
-      subj.value + "|||" + pred.value;
-    const preds = new Set<NamedNode>();
+  _drawObj(obj: Term): TemplateResult {
+    return html` <div>${this.nodeDisplay.render(obj)}</div> `;
+  }
 
-    subjs.forEach((subj: NamedNode) => {
-      this.graph.forEach(
-        (q: Quad) => {
-          if (!Util.isNamedNode(q.predicate)) {
-            throw new Error();
-          }
-          preds.add(q.predicate as NamedNode);
-          const cellKey = makeCellKey(subj, q.predicate as NamedNode);
-          if (!graphCells.has(cellKey)) {
-            graphCells.set(cellKey, new Set<Term>());
-          }
-          graphCells.get(cellKey)!.add(q.object);
-        },
-        subj,
-        null,
-        null,
-        null
-      );
-    });
-    const predsList = Array.from(preds);
-    predsList.splice(predsList.indexOf(rdf.type), 1);
-    // also pull out label, which should be used on 1st column
-    predsList.sort();
+  _drawColumnHead(pred: NamedNode): TemplateResult {
+    return html` <th>${this.nodeDisplay.render(pred)}</th> `;
+  }
 
-    const thead = () => {
-      const predColumnHead = (pred: NamedNode) => {
-        return html`
-          <th>${this.nodeDisplay.getHtml(pred)}</th>
-        `;
-      };
-      return html`
-        <thead>
-          <tr>
-            <th></th>
-            ${predsList.map(predColumnHead)}
-          </tr>
-        </thead>
-      `;
-    };
+  _thead(layout: MultiSubjsTypeBlockLayout): TemplateResult {
+    return html`
+      <thead>
+        <tr>
+          <th></th>
+          ${layout.preds.map(this._drawColumnHead.bind(this))}
+        </tr>
+      </thead>
+    `;
+  }
 
-    const instanceRow = (subj: NamedNode) => {
-      const cell = (pred: NamedNode) => {
-        const objs = graphCells.get(subj + "|||" + pred);
-        if (!objs) {
-          return html`
-            <td></td>
-          `;
-        }
-        const objsList = Array.from(objs);
-        objsList.sort();
-        const draw = (obj: Term) => {
-          return html`
-            <div>${this.nodeDisplay.getHtml(obj)}</div>
-          `;
-        };
-        return html`
-          <td>${objsList.map(draw)}</td>
-        `;
-      };
+  _msbCell(layout: MultiSubjsTypeBlockLayout, subj: NamedNode) {
+    return (pred: NamedNode): TemplateResult => {
+      const objs = layout.graphCells.get(layout.makeCellKey(subj, pred));
+      if (!objs || !objs.size) {
+        return html` <td></td> `;
+      }
+      const objsList = Array.from(objs);
+      objsList.sort();
+      return html` <td>${objsList.map(this._drawObj.bind(this))}</td> `;
+    };
+  }
 
+  _instanceRow(layout: MultiSubjsTypeBlockLayout) {
+    return (subj: NamedNode): TemplateResult => {
       return html`
         <tr>
-          <td>${this.nodeDisplay.getHtml(subj)}</td>
-          ${predsList.map(cell)}
+          <td>${this.nodeDisplay.render(subj)}</td>
+          ${layout.preds.map(this._msbCell(layout, subj))}
         </tr>
       `;
     };
+  }
+
+  _multiSubjsTypeBlock(byType: TypeToSubjs, typeUri: NamedNode) {
+    const layout = new MultiSubjsTypeBlockLayout(this.graph, byType, typeUri);
 
     return html`
-      <div>[icon] ${this.nodeDisplay.getHtml(typeUri)} resources</div>
+      <div>[icon] ${this.nodeDisplay.render(typeUri)} type resources</div>
       <div class="typeBlockScroll">
         <table class="typeBlock">
-          ${thead()} ${subjs.map(instanceRow)}
+          ${this._thead(layout)} ${layout.subjs.map(this._instanceRow(layout))}
         </table>
       </div>
     `;
   }
 
   makeTemplate(): TemplateResult {
-    const { byType, untyped } = groupByRdfType(this.graph);
-    const typedSubjs = Array.from(byType.keys());
-    typedSubjs.sort();
-
-    const untypedSubjs = Array.from(untyped.values());
-    untypedSubjs.sort();
+    const { byType, typedSubjs, untypedSubjs } = groupByRdfType(this.graph);
 
     return html`
       <section>
@@ -317,9 +217,11 @@
             These statements are all in the
             <span data-bind="html: $root.createCurie(graphUri())">...</span> graph.-->
         </div>
-        ${typedSubjs.map((t: NamedNode) => this.byTypeBlock(byType, t))}
+        ${typedSubjs.map((t: NamedNode) =>
+          this._multiSubjsTypeBlock(byType, t)
+        )}
         <div class="spoGrid">
-          ${untypedSubjs.map(this._subjBlock.bind(this))}
+          ${untypedSubjs.map(this._subjPredObjsBlock.bind(this))}
         </div>
       </section>
     `;