changeset 118:c2923b20bf5c

support multi labels per column
author drewp@bigasterisk.com
date Sun, 20 Mar 2022 00:54:19 -0700
parents 069c1f70afa5
children 8715633f5213
files src/layout/Layout.test.ts src/layout/Layout.ts src/render/GraphView.ts
diffstat 3 files changed, 151 insertions(+), 46 deletions(-) [+]
line wrap: on
line diff
--- a/src/layout/Layout.test.ts	Sat Mar 19 17:45:01 2022 -0700
+++ b/src/layout/Layout.test.ts	Sun Mar 20 00:54:19 2022 -0700
@@ -84,14 +84,14 @@
       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") },
+      const section0 = lr.sections[0] as AlignedTable;
+      expect(section0.columnHeaders).toEqual([
+        { rdfTypes: [EX("T1")], pred: EX("color") },
+        { rdfTypes: [EX("T1"), EX("T2")], pred: EX("size") },
         // and doesn't include rdf:type as a column header here
       ]);
-      expect(sec0.rowHeaders).toEqual([EX("a"), EX("b"), EX("c"), EX("e")]);
-      expect(sec0.rows).toEqual([
+      expect(section0.rowHeaders).toEqual([EX("a"), EX("b"), EX("c"), EX("e")]);
+      expect(section0.rows).toEqual([
         [[EX("red")], []],
         [[EX("blue")], []],
         [[], []],
@@ -123,26 +123,60 @@
     const layout = new Layout(vc);
     const lr = layout.plan(await typedStatements());
     expect(lr.sections).toHaveLength(2);
-    expect(lr.sections[0]).toEqual({
-      columnHeaders: [
-        { rdfType: EX("T1"), pred: EX("color") },
-        { rdfType: EX("T1"), pred: EX("size") },
-      ],
-      rowHeaders: [EX("a"), EX("b"), EX("c"), EX("e")],
-      rows: [
+    const section0 = lr.sections[0] as AlignedTable;
+    expect(section0.columnHeaders).toEqual([
+      { rdfTypes: [EX("T1")], pred: EX("color") },
+      { rdfTypes: [EX("T1"), EX("T2")], pred: EX("size") },
+    ]);
+    expect(section0.rowHeaders).toEqual([EX("a"), EX("b"), EX("c"), EX("e")]);
+    expect(section0.rows).toEqual([
+      [[EX("red")], []],
+      [[EX("blue")], []],
+      [[], []],
+      [[], [EX("small")]],
+    ]);
+    const section1 = lr.sections[1] as AlignedTable;
+    expect(section1.columnHeaders).toEqual([
+      { rdfTypes: [EX("T1"), EX("T2")], pred: EX("size") },
+    ]);
+    expect(section1.rowHeaders).toEqual([EX("d"), EX("e")]);
+    expect(section1.rows).toEqual([
+      [[EX("big")]], //
+      [[EX("small")]],
+    ]);
+  });
+  describe("joins multiple types into one table", () => {
+    it("can simply merge types", async () => {
+      const vc = new ViewConfig();
+      await vc.readFromGraph(`
+        @prefix ex: <http://example.com/> .
+        @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+        @prefix : <http://example.com/> .
+
+        <> a :View; :table [ :primaryType :T1; :joinType :T2 ] .
+      `);
+      vc.graph.forEach((q) => console.log("vc", q), null, null, null, null);
+      const layout = new Layout(vc);
+      const lr = layout.plan(
+        await n3Graph(`
+        @prefix : <http://example.com/> .
+        :g1 {
+          :a a :T1 ; :color :red .
+          :b a :T2 ; :size :big .
+        }
+      `)
+      );
+      expect(lr.sections).toHaveLength(1);
+      const section0 = lr.sections[0] as AlignedTable;
+      expect(section0.columnHeaders).toEqual([
+        { rdfTypes: [EX("T1")], pred: EX("color") },
+        { rdfTypes: [EX("T2")], pred: EX("size") },
+      ]);
+      expect(section0.rowHeaders).toEqual([EX("a"), EX("b")]);
+      expect(section0.rows).toEqual([
         [[EX("red")], []],
-        [[EX("blue")], []],
-        [[], []],
-        [[], [EX("small")]],
-      ],
-    });
-    expect(lr.sections[1]).toEqual({
-      columnHeaders: [{ rdfType: EX("T2"), pred: EX("size") }],
-      rowHeaders: [EX("d"), EX("e")],
-      rows: [
-        [[EX("big")]], //
-        [[EX("small")]],
-      ],
+        [[], [EX("big")]],
+      ]);
     });
   });
   it.skip("makes a table out of ungrouped triples with the same type", async () => {});
--- a/src/layout/Layout.ts	Sat Mar 19 17:45:01 2022 -0700
+++ b/src/layout/Layout.ts	Sun Mar 20 00:54:19 2022 -0700
@@ -9,7 +9,7 @@
 type UriSet = Immutable.Set<NamedNode>;
 
 interface ColumnHeader {
-  rdfType: NamedNode;
+  rdfTypes: NamedNode[];
   pred: NamedNode;
 }
 
@@ -40,10 +40,18 @@
 class AlignedTableBuilder {
   subjSet: UriSet = Immutable.Set();
   predSet: UriSet = Immutable.Set();
+  subjsSeenWithPred: Immutable.Map<NamedNode, NamedNode[]> = Immutable.Map();
+  typesSeenForSubj: Immutable.Map<NamedNode, NamedNode[]> = Immutable.Map();
+
   cell = new UriPairMap();
-  constructor(
-    public rdfType: NamedNode /* plus join types, sort instructions */
-  ) {}
+  constructor(public primaryType: NamedNode, public joinTypes: NamedNode[]) {}
+
+  showsType(rdfType: NamedNode): boolean {
+    return (
+      this.primaryType.equals(rdfType) ||
+      Immutable.Set<NamedNode>(this.joinTypes).has(rdfType)
+    );
+  }
 
   addQuad(q: Quad) {
     const subj = q.subject as NamedNode;
@@ -51,6 +59,29 @@
     this.subjSet = this.subjSet.add(subj);
     this.predSet = this.predSet.add(pred);
     this.cell.add(subj, pred, q.object);
+
+    if (pred.equals(rdf.type)) {
+      this.trackTypes(subj, q.object as NamedNode);
+    }
+    this.trackSubjs(subj, pred);
+  }
+
+  private trackTypes(subj: NamedNode<string>, rdfType: NamedNode) {
+    let cur = this.typesSeenForSubj.get(subj, undefined);
+    if (cur === undefined) {
+      cur = [];
+      this.typesSeenForSubj = this.typesSeenForSubj.set(subj, cur);
+    }
+    cur.push(rdfType);
+  }
+
+  private trackSubjs(subj: NamedNode, pred: NamedNode<string>) {
+    let cur = this.subjsSeenWithPred.get(pred, undefined);
+    if (cur === undefined) {
+      cur = [];
+      this.subjsSeenWithPred = this.subjsSeenWithPred.set(pred, cur);
+    }
+    cur.push(subj);
   }
 
   _displayedPreds(): NamedNode[] {
@@ -70,9 +101,11 @@
     preds = tagged.map((e) => e.val);
     return preds;
   }
+
   gotStatements(): boolean {
     return !this.subjSet.isEmpty();
   }
+
   value(): AlignedTable {
     const subjs = uniqueSortedTerms(this.subjSet);
     const preds = this._displayedPreds();
@@ -87,8 +120,16 @@
       outputGrid.push(row);
     }
 
-    const headers = preds.map((pred) => {
-      return { rdfType: this.rdfType, pred: pred };
+    const headers: ColumnHeader[] = preds.map((pred) => {
+      const subs = this.subjsSeenWithPred.get(pred, []);
+      const types: NamedNode[] = [];
+      subs.forEach((s) => {
+        this.typesSeenForSubj.get(s, []).forEach((t) => {
+          types.push(t);
+        });
+      });
+
+      return { rdfTypes: uniqueSortedTerms(types), pred: pred };
     });
     return { columnHeaders: headers, rowHeaders: subjs, rows: outputGrid };
   }
@@ -107,9 +148,10 @@
   graph.forEach(
     (q: Quad) => {
       const s = q.subject as NamedNode;
+      const rdfType = q.object as NamedNode;
       tableBuilders.forEach((tb) => {
-        if (tb.rdfType.equals(q.object)) {
-          let cur = out.get(s);
+        if (tb.showsType(rdfType)) {
+          let cur = out.get(s, undefined);
           if (cur === undefined) {
             cur = [];
             out = out.set(s, cur);
@@ -165,6 +207,7 @@
     graph.forEach(
       (q: Quad) => {
         const tables = tablesWantingSubject.get(q.subject as NamedNode);
+
         if (tables && tables.length) {
           tables.forEach((t: AlignedTableBuilder) => t.addQuad(q));
         } else {
@@ -182,7 +225,9 @@
     const ungrouped: Quad[] = [];
 
     const tableBuilders = this.viewConfig
-      ? this.viewConfig.tables.map((t) => new AlignedTableBuilder(t.primary))
+      ? this.viewConfig.tables.map(
+          (t) => new AlignedTableBuilder(t.primary, t.joins)
+        )
       : [];
 
     const tablesWantingSubject = subjectsToTablesMap(graph, tableBuilders);
--- a/src/render/GraphView.ts	Sat Mar 19 17:45:01 2022 -0700
+++ b/src/render/GraphView.ts	Sun Mar 20 00:54:19 2022 -0700
@@ -8,6 +8,7 @@
   PredRow,
   SubjRow,
 } from "../layout/Layout";
+import { uniqueSortedTerms } from "../layout/rdf_value";
 import { SuffixLabels } from "../layout/suffixLabels";
 import { ViewConfig } from "../layout/ViewConfig";
 import { NodeDisplay } from "./NodeDisplay";
@@ -74,6 +75,7 @@
       null
     );
   }
+
   _renderSection(section: AlignedTable | FreeStatements) {
     if ((section as any).columnHeaders) {
       return this._renderAlignedTable(section as AlignedTable);
@@ -83,31 +85,56 @@
   }
 
   _renderAlignedTable(section: AlignedTable): TemplateResult {
-    let anyType: NamedNode = new NamedNode('');
-    const heads = section.columnHeaders.map((ch) => {
-      anyType = ch.rdfType;
-      return html`<th>${this.nodeDisplay.render(ch.pred)}</th>`;
-    });
+    const tableTypes: NamedNode[][] = [];
+    const typeHeads: TemplateResult[] = [];
+    const heads: TemplateResult[] = [];
+    for (let ch of section.columnHeaders) {
+      const colSpan = 1; //todo
+      typeHeads.push(
+        html`<th colspan="${colSpan}">
+          ${ch.rdfTypes.map((n) => this.nodeDisplay.render(n))}
+        </th>`
+      );
+
+      tableTypes.push(ch.rdfTypes);
+      heads.push(html`<th>${this.nodeDisplay.render(ch.pred)}</th>`);
+    }
+
     const cells = [];
 
     for (let rowIndex in section.rows) {
-      const headerCol = html`<th>${this.nodeDisplay.render(section.rowHeaders[rowIndex])}</th>`;
-      const bodyCols = []
+      const headerCol = this.nodeDisplay.render(section.rowHeaders[rowIndex]);
+      const bodyCols = [];
       for (let cellObjs of section.rows[rowIndex]) {
         const display = cellObjs.map(
           (t) => html`<div>${this.nodeDisplay.render(t)}</div>`
         );
         bodyCols.push(html`<td>${display}</td>`);
       }
-      cells.push(html`<tr>${headerCol}${bodyCols}</tr>`);
+      cells.push(
+        html`<tr>
+          <th>${headerCol}</th>
+          ${bodyCols}
+        </tr>`
+      );
     }
+    const tableTypesUnique = uniqueSortedTerms(tableTypes.flat());
+    const typesDisplay = html`${tableTypesUnique.length == 1 ? "type" : "types"}
+    ${tableTypesUnique.map((n) => this.nodeDisplay.render(n))}`;
+
     return html`
-      <div>[icon] Resources of type ${this.nodeDisplay.render(anyType)}</div>
+      <div>[icon] Resources of ${typesDisplay}</div>
       <div class="typeBlockScroll">
         <table class="typeBlock">
           <thead>
-            <th>Subject</th>
-            ${heads}
+            <tr>
+              <th></th>
+              ${typeHeads}
+            </tr>
+            <tr>
+              <th>Subject</th>
+              ${heads}
+            </tr>
           </thead>
           <tbody>
             ${cells}
@@ -163,7 +190,6 @@
     return html` <th>${this.nodeDisplay.render(pred)}</th> `;
   }
 
-
   //   return html`
   //     <div>[icon] Resources of type ${typeNames}</div>
   //     <div class="typeBlockScroll">