changeset 108:5e6840229a05

rewrite freeStatements rendering to put more planning in layout
author drewp@bigasterisk.com
date Fri, 18 Mar 2022 11:57:38 -0700
parents 042bd3361339
children cbcd82d21356
files package.json pnpm-lock.yaml src/layout/Layout.test.ts src/layout/Layout.ts src/layout/rdf_value.test.ts src/layout/rdf_value.ts src/render/GraphView.ts src/render/StreamedGraph.ts tasks.py
diffstat 9 files changed, 271 insertions(+), 152 deletions(-) [+]
line wrap: on
line diff
--- a/package.json	Sun Mar 13 22:02:30 2022 -0700
+++ b/package.json	Fri Mar 18 11:57:38 2022 -0700
@@ -22,10 +22,9 @@
     "immutable": "^4.0.0",
     "jsonld": "^2.0.2",
     "lit": "^2.1.3",
-    "n3": "^1.13.0",
+    "n3": "git+https://github.com/rdfjs/N3.js.git#088006449c9e8275351db604b3d184071fef31a7",
     "rdf-js": "^4.0.2"
   },
-  "n3 is hacked":"to remove hashCode getter",
   "devDependencies": {
     "@types/jest": "^27.4.0",
     "jest": "^27.5.1",
--- a/pnpm-lock.yaml	Sun Mar 13 22:02:30 2022 -0700
+++ b/pnpm-lock.yaml	Fri Mar 18 11:57:38 2022 -0700
@@ -8,7 +8,7 @@
   jest: ^27.5.1
   jsonld: ^2.0.2
   lit: ^2.1.3
-  n3: ^1.13.0
+  n3: git+https://github.com/rdfjs/N3.js.git#088006449c9e8275351db604b3d184071fef31a7
   rdf-js: ^4.0.2
   stylus: ^0.56.0
   ts-jest: ^27.1.3
@@ -22,7 +22,7 @@
   immutable: 4.0.0
   jsonld: 2.0.2
   lit: 2.1.3
-  n3: 1.13.0
+  n3: github.com/rdfjs/N3.js/088006449c9e8275351db604b3d184071fef31a7
   rdf-js: 4.0.2
 
 devDependencies:
@@ -2556,14 +2556,6 @@
     resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
     dev: true
 
-  /n3/1.13.0:
-    resolution: {integrity: sha512-GMB4ypBfnuf6mmwbtyN6Whc8TfuVDedxc4n+3wsacQH/h0+RjaEobGMhlWrFLDsqVbT94XA6+q9yysMO5SadKA==}
-    engines: {node: '>=8.0'}
-    dependencies:
-      queue-microtask: 1.2.3
-      readable-stream: 3.6.0
-    dev: false
-
   /nanoid/3.2.0:
     resolution: {integrity: sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==}
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -3432,3 +3424,15 @@
       y18n: 5.0.8
       yargs-parser: 20.2.9
     dev: true
+
+  github.com/rdfjs/N3.js/088006449c9e8275351db604b3d184071fef31a7:
+    resolution: {tarball: https://codeload.github.com/rdfjs/N3.js/tar.gz/088006449c9e8275351db604b3d184071fef31a7}
+    name: n3
+    version: 1.13.0
+    engines: {node: '>=8.0'}
+    prepare: true
+    requiresBuild: true
+    dependencies:
+      queue-microtask: 1.2.3
+      readable-stream: 3.6.0
+    dev: false
--- a/src/layout/Layout.test.ts	Sun Mar 13 22:02:30 2022 -0700
+++ b/src/layout/Layout.test.ts	Fri Mar 18 11:57:38 2022 -0700
@@ -47,9 +47,9 @@
     expect(lr).toEqual({
       sections: [
         {
-          statements: [
-            G1(EX("a0"), EX("b0"), EX("c0")),
-            G1(EX("d0"), EX("e0"), EX("f0")),
+          subjRows: [
+            { subj: EX("a0"), predRows: [{ pred: EX("b0"), objs: [EX("c0")] }] },
+            { subj: EX("d0"), predRows: [{ pred: EX("e0"), objs: [EX("f0")] }] },
           ],
         },
       ],
@@ -68,27 +68,32 @@
       const layout = new Layout(vc);
       lr = layout.plan(await typedStatements());
     });
-    it("returns 2 sections", ()=>{
+    it("returns 2 sections", () => {
       expect(lr.sections).toHaveLength(2);
-    })
+    });
     it("puts the right type in the table", async () => {
       const sec0 = lr.sections[0] as AlignedTable;
       expect(sec0.columnHeaders).toEqual([
         { rdfType: EX("T1"), pred: EX("color") },
-        { rdfType: EX("T1"), pred: EX("size") }
-    ])
+        { rdfType: EX("T1"), pred: EX("size") },
+      ]);
       expect(sec0.rows).toEqual([
-          [EX("a"), EX("red"), null],
-          [EX("b"), EX("blue"),null],
-          [EX("c"), null, null],
-          [EX("e"), null, EX('small')],
-        ]);
+        [EX("a"), EX("red"), null],
+        [EX("b"), EX("blue"), null],
+        [EX("c"), null, null],
+        [EX("e"), null, EX("small")],
+      ]);
     });
     it("leaves the rest ungrouped", async () => {
       expect(lr.sections[1]).toEqual({
-        statements: [
-          G1(EX("d"), rdf.type, EX("T2")),
-          G1(EX("d"), EX("size"), EX("big")),
+        subjRows: [
+          {
+            subj: EX("d"),
+            predRows: [
+              { pred: EX("size"), objs: [EX("big")] },
+              { pred: rdf.type, objs: [EX("T2")] },
+            ],
+          },
         ],
       });
     });
--- a/src/layout/Layout.ts	Sun Mar 13 22:02:30 2022 -0700
+++ b/src/layout/Layout.ts	Fri Mar 18 11:57:38 2022 -0700
@@ -3,25 +3,36 @@
 import Immutable from "immutable"; // mostly using this for the builtin equals() testing, since NamedNode(x)!=NamedNode(x)
 import { NamedNode, Quad, Store, Term } from "n3";
 import { rdf } from "./namespaces";
+import { uniqueSortedTerms } from "./rdf_value";
 import { TableDesc, ViewConfig } from "./ViewConfig";
 
 type UriSet = Immutable.Set<NamedNode>;
 export type TypeToSubjs = Immutable.Map<NamedNode, UriSet>;
 
-// https://github.com/rdfjs/N3.js/issues/265
-(NamedNode.prototype as any).hashCode = () => 0;
-
 interface ColumnHeader {
   rdfType: NamedNode;
   pred: NamedNode;
 }
+
 export interface AlignedTable {
   columnHeaders: ColumnHeader[];
   rows: (Term | null)[][]; // each row is 1 wider than columnHeaders since the 1st element is the subject for that row
 }
-interface FreeStatements {
-  statements: Quad[];
+
+export interface PredRow {
+  pred: NamedNode;
+  objs: Term[];
 }
+
+export interface SubjRow {
+  subj: NamedNode;
+  predRows: PredRow[];
+}
+
+export interface FreeStatements {
+  subjRows: SubjRow[];
+}
+
 export interface LayoutResult {
   sections: (AlignedTable | FreeStatements)[];
 }
@@ -100,6 +111,34 @@
   return Immutable.Set(subjectsToGather);
 }
 
+function freeStatmentsSection(stmts: Quad[]): FreeStatements {
+  const subjs: NamedNode[] = [];
+  stmts.forEach((q) => {
+    subjs.push(q.subject as NamedNode);
+  });
+  return {
+    subjRows: uniqueSortedTerms(subjs).map((subj) => {
+      const preds: NamedNode[] = [];
+      let po = Immutable.Map<NamedNode, Term[]>();
+      stmts.forEach((q) => {
+        if (q.subject.equals(subj)) {
+          const p = q.predicate as NamedNode;
+          preds.push(p);
+          po = po.set(p, po.get(p, []));
+          po.get(p)?.push(q.object as Term);
+        }
+      });
+
+      const rows: PredRow[] = [];
+      uniqueSortedTerms(preds).forEach((p) => {
+        rows.push({ pred: p, objs: uniqueSortedTerms(po.get(p, [])) });
+      });
+      return { subj: subj, predRows: rows };
+    }),
+  };
+}
+
+// The description of how this page should look: sections, tables, etc.
 export class Layout {
   constructor(public viewConfig?: ViewConfig) {}
   plan(graph: Store): LayoutResult {
@@ -130,7 +169,7 @@
     if (table) {
       res.sections.push(table.value());
     }
-    res.sections.push({ statements: ungrouped });
+    res.sections.push(freeStatmentsSection(ungrouped));
     return res;
   }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layout/rdf_value.test.ts	Fri Mar 18 11:57:38 2022 -0700
@@ -0,0 +1,49 @@
+import Immutable from "immutable";
+import { DataFactory, NamedNode, Term } from "n3";
+import { EX } from "./namespaces";
+import { uniqueSortedTerms } from "./rdf_value";
+const { namedNode, literal } = DataFactory;
+describe("Immutable.Set", () => {
+  it("contains", () => {
+    const s = Immutable.Set([EX("e1")]);
+    expect(s.contains(EX("e1"))).toBeTruthy();
+    expect(s.contains(EX("e2"))).toBeFalsy();
+  });
+});
+
+const uri1 = namedNode("http://example.com/1");
+const uri2 = namedNode("http://example.com/2");
+const lit1 = literal("lit1");
+const lit2 = literal("lit2");
+const lit3 = literal("lit2", namedNode("#othertype"));
+const lit4 = literal("http://example.com/1"); // sic literal (that looks like a URI)
+
+describe("uniqueSortedTerms", () => {
+  it("takes Term arrays", () => {
+    const actual = uniqueSortedTerms([lit1] as Term[]);
+    expect(actual).toEqual([lit1]);
+  });
+  it("takes NamedNode arrays", () => {
+    const actual = uniqueSortedTerms([uri1] as NamedNode[]);
+    expect(actual).toEqual([uri1]);
+  });
+  it("dedups URIs", () => {
+    expect(uniqueSortedTerms([uri1, uri1, uri2])).toEqual([uri1, uri2]);
+  });
+  it("sorts URIs", () => {
+    expect(uniqueSortedTerms([uri2, uri1])).toEqual([uri1, uri2]);
+  });
+  it("dedups literals", () => {
+    expect(uniqueSortedTerms([lit1, lit2, lit2, lit3, lit3])).toEqual([
+      lit1,
+      lit2,
+      lit3,
+    ]);
+  });
+  it("sorts literals", () => {
+    expect(uniqueSortedTerms([lit3, lit2, lit1])).toEqual([lit1, lit2, lit3]);
+  });
+  it("doesn't confuse literal URI strings", () => {
+    expect(uniqueSortedTerms([uri1, lit4])).toEqual([lit4, uri1]);
+  });
+});
--- a/src/layout/rdf_value.ts	Sun Mar 13 22:02:30 2022 -0700
+++ b/src/layout/rdf_value.ts	Fri Mar 18 11:57:38 2022 -0700
@@ -44,3 +44,16 @@
   }
   return ret;
 }
+
+export function uniqueSortedTerms<T extends NamedNode | Term>(terms: Iterable<T>): T[] {
+  const uniques: T[] = [];
+  const seen = new Set();
+  for (let o of terms) {
+    if (!seen.has(o.id)) {
+      seen.add(o.id);
+      uniques.push(o);
+    }
+  }
+  uniques.sort((a,b)=>{return a.id.localeCompare(b.id)});
+  return uniques;
+}
--- a/src/render/GraphView.ts	Sun Mar 13 22:02:30 2022 -0700
+++ b/src/render/GraphView.ts	Fri Mar 18 11:57:38 2022 -0700
@@ -1,12 +1,16 @@
+import Immutable from "immutable";
 import { html, TemplateResult } from "lit";
 import { DataFactory, Literal, NamedNode, Quad, Store, Term } from "n3";
 import { NodeDisplay } from "./NodeDisplay";
 import { SuffixLabels } from "../layout/suffixLabels";
-import { Layout } from "../layout/Layout";
+import { AlignedTable, FreeStatements, Layout, PredRow, SubjRow } from "../layout/Layout";
 import { TableDesc, ViewConfig } from "../layout/ViewConfig";
+import { uniqueSortedTerms } from "../layout/rdf_value";
 
 const { namedNode } = DataFactory;
 
+type UriSet = Immutable.Set<NamedNode>;
+const emptyUriSet = Immutable.Set<NamedNode>();
 // https://github.com/rdfjs/N3.js/issues/265
 if ((Literal.prototype as any).hashCode === undefined) {
   (Literal.prototype as any).hashCode = () => 0;
@@ -15,22 +19,42 @@
   (NamedNode.prototype as any).hashCode = () => 0;
 }
 export class GraphView {
-  url: string;
-  view: View;
-  graph: Store;
-  nodeDisplay: NodeDisplay;
-  constructor(url: string, viewUrl: string, graph: Store) {
-    this.url = url;
-    this.view = new View(viewUrl);
-    this.graph = graph;
+  nodeDisplay!: NodeDisplay;
+  constructor(
+    public dataSourceUrls: string[],
+    public graph: Store,
+    public viewConfig?: ViewConfig
+  ) {}
+
+  async makeTemplate(): Promise<TemplateResult> {
+    const layout = new Layout(this.viewConfig);
+    const lr = layout.plan(this.graph);
 
     const labels = new SuffixLabels();
     this._addLabelsForAllTerms(this.graph, labels);
-    
-    this.view.ready.then(() => {
-      this._addLabelsForAllTerms(this.view.graph, labels);
-    });
+
     this.nodeDisplay = new NodeDisplay(labels);
+    let viewTitle = html` (no view)`;
+    if (this.viewConfig?.url) {
+      viewTitle = html` using view
+        <a href="${this.viewConfig.url}">${this.viewConfig.label()}</a>`;
+    }
+    // const tables = this.view.toplevelTables(typesPresent);
+    return html`
+      <section>
+        <h2>
+          Current graph (<a href="${this.dataSourceUrls[0]}"
+            >${this.dataSourceUrls[0]}</a
+          >)${viewTitle}
+        </h2>
+        <div>
+          <!-- todo: graphs and provenance.
+            These statements are all in the
+            <span data-bind="html: $root.createCurie(graphUri())">...</span> graph.-->
+        </div>
+        ${lr.sections.map(this._renderSection.bind(this))}
+      </section>
+    `;
   }
 
   _addLabelsForAllTerms(graph: Store, labels: SuffixLabels) {
@@ -55,48 +79,54 @@
       null
     );
   }
+  _renderSection(section: AlignedTable | FreeStatements) {
+    if ((section as any).columnHeaders) {
+      return this._renderAlignedTable(section as AlignedTable);
+    } else {
+      return this._renderFreeStatements(section as FreeStatements);
+    }
+  }
 
-  _subjPredObjsBlock(subj: NamedNode) {
-    const columns = predsForSubj(this.graph, subj);
+  _renderAlignedTable(section: AlignedTable): TemplateResult {
+    return html`aligned table section`;
+  }
+
+  _renderFreeStatements(section: FreeStatements): TemplateResult {
+    const subjects: NamedNode[] = [];
+    let subjPreds = Immutable.Map<NamedNode, UriSet>();
+    
+    return html`<div class="spoGrid">
+      grid has rowcount ${section.subjRows.length} 
+      ${section.subjRows.map(this._subjPredObjsBlock.bind(this))}
+    </div>`;
+  }
+
+  _subjPredObjsBlock( row: SubjRow ): TemplateResult {
     return html`
       <div class="subject">
-        ${this.nodeDisplay.render(subj)}
+        ${this.nodeDisplay.render(row.subj)}
         <!-- todo: special section for uri/type-and-icon/label/comment -->
         <div>
-          ${columns.map((p) => {
-            return this._predObjsBlock(subj, p);
-          })}
+          ${row.predRows.map(this._predObjsBlock.bind(this))}
         </div>
       </div>
     `;
   }
 
-  _objCell(obj: Term) {
+  _predObjsBlock(row: PredRow): TemplateResult {
     return html`
-      <div class="object">
-        ${this.nodeDisplay.render(obj)}
-        <!-- indicate what source or graph said this stmt -->
+      <div class="predicate">
+        ${this.nodeDisplay.render(row.pred)}
+        <div>${row.objs.map(this._objCell.bind(this))}</div>
       </div>
     `;
   }
 
-  _predObjsBlock(subj: NamedNode, pred: NamedNode) {
-    const objsSet = new Set<Term>();
-    this.graph.forEach(
-      (q: Quad) => {
-        objsSet.add(q.object);
-      },
-      subj,
-      pred,
-      null,
-      null
-    );
-    const objs = Array.from(objsSet.values());
-    objs.sort();
+  _objCell(obj: Term): TemplateResult {
     return html`
-      <div class="predicate">
-        ${this.nodeDisplay.render(pred)}
-        <div>${objs.map(this._objCell.bind(this))}</div>
+      <div class="object">
+        ${this.nodeDisplay.render(obj)}
+        <!-- indicate what source or graph said this stmt -->
       </div>
     `;
   }
@@ -109,87 +139,59 @@
     return html` <th>${this.nodeDisplay.render(pred)}</th> `;
   }
 
-  _thead(layout: MultiSubjsTypeBlockLayout): TemplateResult {
-    return html`
-      <thead>
-        <tr>
-          <th></th>
-          ${layout.preds.map(this._drawColumnHead.bind(this))}
-        </tr>
-      </thead>
-    `;
-  }
+  // _thead(layout: MultiSubjsTypeBlockLayout): TemplateResult {
+  //   return html`
+  //     <thead>
+  //       <tr>
+  //         <th></th>
+  //         ${layout.preds.map(this._drawColumnHead.bind(this))}
+  //       </tr>
+  //     </thead>
+  //   `;
+  // }
 
-  _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.render(subj)}</td>
-          ${layout.preds.map(this._msbCell(layout, subj))}
-        </tr>
-      `;
-    };
-  }
-
-  _multiSubjsTypeBlock(byType: TypeToSubjs, table: TableDesc) {
-    const layout = new MultiSubjsTypeBlockLayout(this.graph, byType, table);
+  // _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> `;
+  //   };
+  // }
 
-    let typeNames = [html`${this.nodeDisplay.render(table.primary)}`];
-    if (table.joins) {
-      typeNames.push(html` joined with [`);
-      for (let j of table.joins) {
-        typeNames.push(html`${this.nodeDisplay.render(j)}`);
-      }
-      typeNames.push(html`]`);
-    }
+  // _instanceRow(layout: MultiSubjsTypeBlockLayout) {
+  //   return (subj: NamedNode): TemplateResult => {
+  //     return html`
+  //       <tr>
+  //         <td>${this.nodeDisplay.render(subj)}</td>
+  //         ${layout.preds.map(this._msbCell(layout, subj))}
+  //       </tr>
+  //     `;
+  //   };
+  // }
 
-    return html`
-      <div>[icon] Resources of type ${typeNames}</div>
-      <div class="typeBlockScroll">
-        <table class="typeBlock">
-          ${this._thead(layout)} 
-          ${layout.subjs.map(this._instanceRow(layout))}
-        </table>
-      </div>
-    `;
-  }
+  // _multiSubjsTypeBlock(byType: TypeToSubjs, table: TableDesc) {
+  //   const layout = new MultiSubjsTypeBlockLayout(this.graph, byType, table);
 
-  async makeTemplate(): Promise<TemplateResult> {
-    await this.view.ready;
-    const { byType, typesPresent, untypedSubjs } = groupByRdfType(this.graph);
-    let viewTitle = html` (no view)`;
-    if (this.view.url) {
-      viewTitle = html` using view
-        <a href="${this.view.url}">${this.view.label()}</a>`;
-    }
-    const tables = this.view.toplevelTables(typesPresent);
-    return html`
-      <section>
-        <h2>
-          Current graph (<a href="${this.url}">${this.url}</a>)${viewTitle}
-        </h2>
-        <div>
-          <!-- todo: graphs and provenance.
-            These statements are all in the
-            <span data-bind="html: $root.createCurie(graphUri())">...</span> graph.-->
-        </div>
-        ${tables.map((t: TableDesc) => this._multiSubjsTypeBlock(byType, t))}
-        <div class="spoGrid">
-          ${untypedSubjs.map(this._subjPredObjsBlock.bind(this))}
-        </div>
-      </section>
-    `;
-  }
+  //   let typeNames = [html`${this.nodeDisplay.render(table.primary)}`];
+  //   if (table.joins) {
+  //     typeNames.push(html` joined with [`);
+  //     for (let j of table.joins) {
+  //       typeNames.push(html`${this.nodeDisplay.render(j)}`);
+  //     }
+  //     typeNames.push(html`]`);
+  //   }
+
+  //   return html`
+  //     <div>[icon] Resources of type ${typeNames}</div>
+  //     <div class="typeBlockScroll">
+  //       <table class="typeBlock">
+  //         ${this._thead(layout)} ${layout.subjs.map(this._instanceRow(layout))}
+  //       </table>
+  //     </div>
+  //   `;
+  // }
 }
--- a/src/render/StreamedGraph.ts	Sun Mar 13 22:02:30 2022 -0700
+++ b/src/render/StreamedGraph.ts	Fri Mar 18 11:57:38 2022 -0700
@@ -6,6 +6,7 @@
 import { GraphView } from "./GraphView";
 import { StreamedGraphClient } from "../layout/StreamedGraphClient";
 import { style, addFontToRootPage } from "./style";
+import { ViewConfig } from "../layout/ViewConfig";
 
 // export * from "./graph_queries";
 
@@ -107,8 +108,11 @@
     if (!this.graphViewDirty) return;
 
     if ((this.graph as VersionedGraph).store && this.graph.store) {
+
+      const vc = new ViewConfig()
+
       await this._graphAreaShowGraph(
-        new GraphView(this.url, this.view, this.graph.store)
+        new GraphView([this.url], this.graph.store, vc)
       );
       this.graphViewDirty = false;
     } else {
--- a/tasks.py	Sun Mar 13 22:02:30 2022 -0700
+++ b/tasks.py	Fri Mar 18 11:57:38 2022 -0700
@@ -29,3 +29,7 @@
 @task
 def release(ctx):
     ctx.run(f'pnpm publish --registry https://bigasterisk.com/js', pty=True)
+
+@task
+def dev(ctx):
+    ctx.run('pnpm dev')
\ No newline at end of file