changeset 106:2468f2227d22

make src/layout/ and src/render/ separation
author drewp@bigasterisk.com
date Sun, 13 Mar 2022 22:00:30 -0700
parents 4bb8c7775c83
children 042bd3361339
files src/Layout.test.ts src/Layout.ts src/NodeDisplay.ts src/README src/ViewConfig.test.ts src/ViewConfig.ts src/fetchAndParse.ts src/graph_queries.ts src/graph_view.ts src/index.ts src/json_ld_quads.test.ts src/json_ld_quads.ts src/layout/Layout.test.ts src/layout/Layout.ts src/layout/ViewConfig.test.ts src/layout/ViewConfig.ts src/layout/fetchAndParse.ts src/layout/graph_queries.ts src/layout/json_ld_quads.test.ts src/layout/json_ld_quads.ts src/layout/namespaces.ts src/layout/rdf_value.ts src/layout/streamed_graph_client.ts src/layout/suffixLabels.test.ts src/layout/suffixLabels.ts src/namespaces.ts src/rdf_value.ts src/render/NodeDisplay.ts src/render/element.ts src/render/graph_view.ts src/render/style.ts src/streamed_graph_client.ts src/style.ts src/suffixLabels.test.ts src/suffixLabels.ts
diffstat 35 files changed, 1599 insertions(+), 1599 deletions(-) [+]
line wrap: on
line diff
--- a/src/Layout.test.ts	Sun Mar 13 21:57:52 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,163 +0,0 @@
-import { Quad, Store, Term } from "n3";
-import { n3Graph } from "./fetchAndParse";
-import { AlignedTable, Layout, LayoutResult } from "./Layout";
-import { EX, rdf } from "./namespaces";
-import { ViewConfig } from "./ViewConfig";
-
-const twoStatements = async (): Promise<Store> => {
-  return n3Graph(`
-  @prefix : <http://example.com/> .
-  :g1 {
-    :a0 :b0 :c0 .
-    :d0 :e0 :f0 .
-  }
-  `);
-};
-
-const typedStatements = async (): Promise<Store> => {
-  return n3Graph(`
-  @prefix : <http://example.com/> .
-  :g1 {
-    :a a :T1 ; :color :red .
-    :b a :T1 ; :color :blue .
-    :c a :T1 .
-    :d a :T2 ; :size :big .
-    :e a :T1,:T2; :size :small
-  }
-  `);
-};
-function G1(s: Term, p: Term, o: Term): Quad {
-  return new Quad(s, p, o, EX("g1"));
-}
-
-describe("Layout", () => {
-  it("accepts a ViewConfig", async () => {
-    const vc = new ViewConfig();
-    await vc.readFromGraph(`
-      @prefix ex: <http://example.com/> .
-      @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
-
-      <> a ex:View; rdfs:label "repos" .`);
-    const layout = new Layout(vc);
-    const lr = layout.plan(await twoStatements());
-  });
-  it("defaults to putting all triples in the ungrouped list", async () => {
-    const layout = new Layout();
-    const lr = layout.plan(await twoStatements());
-    expect(lr).toEqual({
-      sections: [
-        {
-          statements: [
-            G1(EX("a0"), EX("b0"), EX("c0")),
-            G1(EX("d0"), EX("e0"), EX("f0")),
-          ],
-        },
-      ],
-    });
-  });
-  describe("makes a table as requested by ViewConfig", () => {
-    let lr: LayoutResult;
-
-    beforeAll(async () => {
-      const vc = new ViewConfig();
-      await vc.readFromGraph(`
-        @prefix : <http://example.com/> .
-        @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
-  
-        <> a :View; :table [ :primaryType :T1 ] .`);
-      const layout = new Layout(vc);
-      lr = layout.plan(await typedStatements());
-    });
-    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") }
-    ])
-      expect(sec0.rows).toEqual([
-          [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")),
-        ],
-      });
-    });
-  });
-  it("makes a table out of ungrouped triples with the same type", async () => {});
-});
-
-// describe("equality", () => {
-//   test("investigation of https://github.com/rdfjs/N3.js/issues/265", () => {
-//     const x = namedNode("x");
-//     const x2 = namedNode("x");
-//     // (NamedNode.prototype as any).hashCode = () => 0;
-//     // expect((x as any).hashCode()).toEqual((x2 as any).hashCode())
-//     expect(x === x2).toBeFalsy();
-//     expect(x == x2).toBeFalsy();
-//     expect(x.equals(x2)).toBeTruthy();
-//     let imap = Immutable.Map();
-//     imap = imap.set(x, 11);
-//     imap = imap.set(x, 22);
-//     imap = imap.set(x2, 33);
-//     expect(imap.has(x)).toBeTruthy();
-//     expect(imap.has(x2)).toBeTruthy();
-//     expect(imap.size).toEqual(1);
-//   });
-// });
-
-// describe("groupByRdfType", () => {
-//   test("finds multiple graphs", () => {});
-//   test("works", async () => {
-//     const store = new Store();
-
-//     const parser = new Parser();
-//     await new Promise((res, rej) => {
-//       parser.parse(
-//         `PREFIX : <urn:>
-//         :rs1 a :Foo; :pred1 "obj1" .
-//         :rs2 a :Foo; :pred1 "obj2" .
-//         :rs3 a :Bar .
-//         :rs4 :pred1 "obj4" .
-// `,
-//         (error, quad: Quad, prefixes: Prefixes) => {
-//           if (quad) {
-//             store.addQuad(quad);
-//           } else {
-//             res(undefined);
-//           }
-//         }
-//       );
-//     });
-//     const grouped = groupByRdfType(store);
-//     expect(Array.from(grouped.byType.keys())).toHaveLength(2);
-//     expect(grouped.byType.get(namedNode("urn:Foo"))).toEqual(
-//       Immutable.Set([namedNode("urn:rs1"), namedNode("urn:rs2")])
-//     );
-//     expect(grouped.byType.get(namedNode("urn:Bar"))).toEqual(
-//       Immutable.Set([namedNode("urn:rs3")])
-//     );
-//     expect(grouped.untypedSubjs).toEqual([namedNode("urn:rs4")]);
-//   });
-
-//   describe("MultiSubjsTypeBlockLayout", () => {
-//     test("gathers subjs", () => {
-
-//     });
-//     test("gathers preds", () => {
-
-//     });
-//     test("cells reports filled cells", () => {
-
-//     });
-//   });
-// });
--- a/src/Layout.ts	Sun Mar 13 21:57:52 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,200 +0,0 @@
-// Organize graph data into tables (column orders, etc) for the view layer.
-
-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 { 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 LayoutResult {
-  sections: (AlignedTable | FreeStatements)[];
-}
-
-class AlignedTableBuilder {
-  columnPreds = Immutable.List<NamedNode>();
-  subjRowIndices = Immutable.Map<NamedNode, number>();
-  rows: (Term | null)[][] = [];
-  constructor(
-    public rdfType: NamedNode /* plus join types, sort instructions */
-  ) {}
-
-  addQuad(q: Quad) {
-    const pred = q.predicate as NamedNode;
-    const omittedColumn = pred.equals(rdf.type);
-    if (!this.columnPreds.contains(pred) && !omittedColumn) {
-      this.columnPreds = this.columnPreds.push(pred); // this is putting cols in random order
-      this.rows.forEach((r) => r.push(null));
-    }
-
-    const predIndex = omittedColumn ? null : this.columnPreds.indexOf(pred);
-    let rowIndex = this.subjRowIndices.get(q.subject as NamedNode);
-    if (rowIndex === undefined) {
-      const newRow = new Array(1 + this.columnPreds.size).fill(null);
-      newRow[0] = q.subject;
-      this.rows.push(newRow);
-      rowIndex = this.rows.length - 1;
-      this.subjRowIndices = this.subjRowIndices.set(
-        q.subject as NamedNode,
-        rowIndex
-      );
-    }
-    if (predIndex !== null) {
-      this.rows[rowIndex][1 + predIndex] = q.object;
-    }
-  }
-
-  value(): AlignedTable {
-    this.rows.sort((a, b) => {
-      const uriA = (a[0] as NamedNode).value,
-        uriB = (b[0] as NamedNode).value;
-      return uriA.localeCompare(uriB);
-    });
-    const headers = this.columnPreds
-      .map((pred) => {
-        return { rdfType: this.rdfType, pred: pred };
-      })
-      .toArray();
-    return { columnHeaders: headers, rows: this.rows };
-  }
-}
-
-function findTypesNeededForTables(viewConfig?: ViewConfig): UriSet {
-  const typesToGather: NamedNode[] = [];
-  if (viewConfig) {
-    viewConfig.tables.forEach((t: TableDesc) => {
-      typesToGather.push(t.primary);
-    });
-  }
-  return Immutable.Set(typesToGather);
-}
-
-function findSubjectsWithTypes(graph: Store, typesToGatherSet: UriSet): UriSet {
-  const subjectsToGather: NamedNode[] = [];
-  graph.forEach(
-    (q: Quad) => {
-      if (typesToGatherSet.contains(q.object as NamedNode)) {
-        subjectsToGather.push(q.subject as NamedNode);
-      }
-    },
-    null,
-    rdf.type,
-    null,
-    null
-  );
-  return Immutable.Set(subjectsToGather);
-}
-
-export class Layout {
-  constructor(public viewConfig?: ViewConfig) {}
-  plan(graph: Store): LayoutResult {
-    const typesToGatherSet = findTypesNeededForTables(this.viewConfig);
-
-    const subjectsToGatherSet = findSubjectsWithTypes(graph, typesToGatherSet);
-    const ungrouped: Quad[] = [];
-    const vc = this.viewConfig;
-    const table =
-      vc && vc.tables.length > 0
-        ? new AlignedTableBuilder(vc.tables[0].primary)
-        : null;
-
-    graph.forEach(
-      (q: Quad) => {
-        if (!subjectsToGatherSet.contains(q.subject as NamedNode) || !table) {
-          ungrouped.push(q);
-        } else {
-          table.addQuad(q);
-        }
-      },
-      null,
-      null,
-      null,
-      null
-    );
-    const res: LayoutResult = { sections: [] };
-    if (table) {
-      res.sections.push(table.value());
-    }
-    res.sections.push({ statements: ungrouped });
-    return res;
-  }
-}
-
-// interface ISP {
-//   subj: NamedNode;
-//   pred: NamedNode;
-// }
-// const SP = Immutable.Record<ISP>({
-//   subj: new NamedNode(""),
-//   pred: new NamedNode(""),
-// });
-
-// // One table of rows with a common rdf:type.
-// export class MultiSubjsTypeBlockLayout {
-//   subjs: NamedNode[];
-//   preds: NamedNode[];
-//   graphCells: Immutable.Map<ISP, Immutable.Set<Term>>;
-//   constructor(graph: Store, byType: TypeToSubjs, table: TableDesc) {
-//     const subjSet = byType.get(table.primary);
-//     this.subjs = subjSet ? Array.from(subjSet) : [];
-//     this.subjs.sort();
-
-//     let preds = Immutable.Set<NamedNode>();
-
-//     this.graphCells = Immutable.Map<ISP, Immutable.Set<Term>>().withMutations(
-//       (mutGraphCells) => {
-//         this.subjs.forEach((subj: NamedNode) => {
-//           graph.forEach(
-//             (q: Quad) => {
-//               if (!Util.isNamedNode(q.predicate)) {
-//                 throw new Error();
-//               }
-
-//               const pred = q.predicate as NamedNode;
-//               if (pred.equals(rdf.type)) {
-//                 // the whole block is labeled with the type
-//                 return;
-//               }
-//               preds = preds.add(pred);
-//               const cellKey = this.makeCellKey(subj, pred);
-//               mutGraphCells.set(
-//                 cellKey,
-//                 mutGraphCells.get(cellKey, Immutable.Set<Term>()).add(q.object)
-//               );
-//             },
-//             subj,
-//             null,
-//             null,
-//             null
-//           );
-//         });
-//       }
-//     );
-//     this.preds = Array.from(preds);
-//     this.preds.splice(this.preds.indexOf(rdf.type), 1);
-//     // also pull out label, which should be used on 1st column
-//     this.preds.sort();
-//   }
-
-//   makeCellKey(subj: NamedNode, pred: NamedNode): ISP {
-//     return SP({
-//       subj: subj,
-//       pred: pred,
-//     });
-//   }
-// }
--- a/src/NodeDisplay.ts	Sun Mar 13 21:57:52 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-import { html, TemplateResult } from "lit";
-import { Literal, NamedNode, Term, Util } from "n3";
-import { SuffixLabels } from "./suffixLabels";
-
-export class NodeDisplay {
-  labels: SuffixLabels;
-  constructor(labels: SuffixLabels) {
-    this.labels = labels;
-  }
-  render(n: Term | NamedNode): TemplateResult {
-    if (Util.isLiteral(n)) {
-      n = n as Literal;
-      let dtPart: any = "";
-      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.render(n.datatype)} </span>
-        `;
-      }
-      return html` <span class="literal">${n.value}${dtPart}</span> `;
-    }
-
-    if (Util.isNamedNode(n)) {
-      n = n as NamedNode;
-      let shortened = false;
-      let uriValue: string = n.value;
-      for (let [long, short] of [
-        ["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:"],
-      ]) {
-        if (uriValue.startsWith(long)) {
-          uriValue = short + uriValue.substr(long.length);
-          shortened = true;
-          break;
-        }
-      }
-      if (!shortened) {
-        let dn: string | undefined = this.labels.getLabelForNode(uriValue);
-        if (dn === undefined) {
-          throw new Error(`dn=${dn}`);
-        }
-        uriValue = dn;
-      }
-
-      return html` <a class="graphUri" href="${n.value}">${uriValue}</a> `;
-    }
-
-    return html` [${n.termType} ${n.value}] `;
-  }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/README	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,2 @@
+layout/ -> everything that doesn't involve html
+render/ -> everything that does involve html
\ No newline at end of file
--- a/src/ViewConfig.test.ts	Sun Mar 13 21:57:52 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-import { Util } from "n3";
-import { ViewConfig } from "./ViewConfig";
-
-describe("ViewModel", () => {
-  it("gets a table description", async () => {
-    const vc = new ViewConfig();
-
-    await vc.readFromGraph(`
-        @prefix ex: <http://example.com/> .
-        @prefix demo: <http://example.com/demo/> .
-        @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
-        @prefix : <http://bigasterisk.com/netRoutes/ns#> .
-
-        <> a ex:View ; rdfs:label "repos" .
-        <> ex:table demo:table1 .
-        demo:table1 
-        ex:primaryType :FilteredNic;
-        ex:joinType :Traffic .
-        `);
-    const NET = Util.prefix("http://bigasterisk.com/netRoutes/ns#");
-
-    expect(vc.tables[0].primary).toEqual(NET("FilteredNic"));
-    expect(vc.tables[0].joins).toEqual([NET("Traffic")]);
-  });
-});
--- a/src/ViewConfig.ts	Sun Mar 13 21:57:52 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-// Load requested view (rdf data) and provide access to it
-import { DataFactory, NamedNode, Store } from "n3";
-import { fetchAndParse, n3Graph } from "./fetchAndParse";
-import { EX, RDF } from "./namespaces";
-import { labelOrTail, uriValue } from "./rdf_value";
-const Uri = DataFactory.namedNode;
-
-function firstElem<E>(seq: Iterable<E>): E {
-  for (let e of seq) {
-    return e;
-  }
-  throw new Error("no elems");
-}
-
-export interface TableDesc {
-  uri: NamedNode;
-  primary: NamedNode;
-  joins: NamedNode[];
-}
-
-export class ViewConfig {
-  graph: Store;
-  viewRoot!: NamedNode;
-  url?: string;
-  tables: TableDesc[] = [];
-
-  constructor() {
-    this.graph = new Store();
-  }
-
-  async readFromUrl(url: string | "") {
-    if (!url) {
-      return;
-    }
-    await fetchAndParse(url, this.graph);
-
-    this._read();
-  }
-
-  async readFromGraph(n3: string) {
-    this.graph = await n3Graph(n3);
-    this._read();
-  }
-
-  _read() {
-    this.viewRoot = firstElem(
-      this.graph.getSubjects(RDF("type"), EX("View"), null)
-    ) as NamedNode;
-    for (let table of this.graph.getObjects(this.viewRoot, EX("table"), null)) {
-      const tableType = uriValue(this.graph, table, EX("primaryType"));
-      const joins: NamedNode[] = [];
-      for (let joinType of this.graph.getObjects(table, EX("joinType"), null)) {
-        joins.push(joinType as NamedNode);
-      }
-      joins.sort();
-      this.tables.push({
-        uri: table as NamedNode,
-        primary: tableType,
-        joins: joins,
-      });
-    }
-    this.tables.sort();
-  }
-
-  label(): string {
-    return this.url !== undefined
-      ? labelOrTail(this.graph, Uri(this.url))
-      : "unnamed";
-  }
-}
--- a/src/fetchAndParse.ts	Sun Mar 13 21:57:52 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-import { Store, Parser, Quad, Prefixes } from "n3";
-
-export async function fetchAndParse(
-  url: string,
-  store?: Store
-): Promise<Store> {
-  const res = await fetch(url);
-  const body = await res.text();
-  return n3Graph(body, store);
-}
-
-export async function n3Graph(n3: string, store?: Store): Promise<Store> {
-  if (store === undefined) {
-    store = new Store();
-  }
-
-  const parser = new Parser({ format: "TriG" });
-  await new Promise((res, rej) => {
-    parser.parse(n3, (error, quad: Quad, prefixes: Prefixes) => {
-      if (error) rej(error);
-      if (quad) {
-        store!.addQuad(quad);
-      } else {
-        res(undefined);
-      }
-    });
-  });
-
-  return store;
-}
--- a/src/graph_queries.ts	Sun Mar 13 21:57:52 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,101 +0,0 @@
-// import { DataFactory, Literal, N3Store, NamedNode, Util } from "n3";
-// const { literal, namedNode } = DataFactory;
-
-// // i think this one is a worse subset of graphLiteral, below
-// export function getStringValue(
-//   store: N3Store | undefined,
-//   subj: NamedNode,
-//   pred: NamedNode,
-//   defaultValue: string = ""
-// ): string {
-//   if (store === undefined) {
-//     // this is so you can use the function before you have a graph
-//     return "...";
-//   }
-//   const objs = store.getObjects(subj, pred, null);
-//   if (objs.length == 0) {
-//     return defaultValue;
-//   }
-//   return objs[0].value;
-// }
-
-// // workaround for uris that don't have good labels in the graph
-// export function labelFromUri(
-//   uri: NamedNode,
-//   prefix: string,
-//   tailsToLabels: { [key: string]: string },
-//   defaultLabel: string
-// ) {
-//   let label = defaultLabel === undefined ? uri.value : defaultLabel;
-//   Object.entries(tailsToLabels).forEach(([tail, useLabel]) => {
-//     if (uri.equals(namedNode(prefix + tail))) {
-//       label = useLabel as string;
-//     }
-//   });
-//   return label;
-// }
-
-// export function graphLiteral(
-//   store: N3Store,
-//   subj: NamedNode,
-//   pred: string,
-//   notFoundResult?: string
-// ): Literal {
-//   const keep: Array<Literal> = [];
-//   store.forEach(
-//     q => {
-//       if (!Util.isLiteral(q.object)) {
-//         throw new Error("non literal found");
-//       }
-//       let seen = false;
-//       for (let other of keep) {
-//         // why are we getting multiple matches for the same literal? seems like a bug
-//         if (other.equals(q.object)) {
-//           seen = true;
-//         }
-//       }
-//       if (!seen) {
-//         keep.push(q.object as Literal);
-//       }
-//     },
-//     subj,
-//     namedNode(pred),
-//     null,
-//     null
-//   );
-//   if (keep.length == 0) {
-//     return literal(notFoundResult || "(missing)");
-//   }
-//   if (keep.length == 1) {
-//     return keep[0];
-//   }
-//   console.log(`${subj.value} ${pred} had ${keep.length} objects:`, keep);
-//   return keep[0];
-// }
-
-// export function graphUriValue(
-//   store: N3Store,
-//   subj: NamedNode,
-//   pred: string
-// ): NamedNode | undefined {
-//   const keep: Array<NamedNode> = [];
-//   store.forEach(
-//     q => {
-//       if (!Util.isNamedNode(q.object)) {
-//         throw new Error("non uri found");
-//       }
-//       keep.push(q.object as NamedNode);
-//     },
-//     subj,
-//     namedNode(pred),
-//     null,
-//     null
-//   );
-//   if (keep.length == 0) {
-//     return undefined;
-//   }
-//   if (keep.length == 1) {
-//     return keep[0];
-//   }
-//   throw new Error("found multiple matches for pred");
-// }
--- a/src/graph_view.ts	Sun Mar 13 21:57:52 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,201 +0,0 @@
-import { html, TemplateResult } from "lit";
-import { DataFactory, Literal, NamedNode, Quad, Store, Term } from "n3";
-import { NodeDisplay } from "./NodeDisplay";
-import { SuffixLabels } from "./suffixLabels";
-import {
-  groupByRdfType,
-  MultiSubjsTypeBlockLayout,
-  predsForSubj,
-  TypeToSubjs,
-} from "./tabulate";
-import { TableDesc, View } from "./view_loader";
-
-const { namedNode } = DataFactory;
-
-// 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;
-}
-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;
-
-    const labels = new SuffixLabels();
-    this._addLabelsForAllTerms(this.graph, labels);
-    
-    if (this.view.graph) {
-      this._addLabelsForAllTerms(this.view.graph, labels);
-    }
-    this.nodeDisplay = new NodeDisplay(labels);
-  }
-
-  _addLabelsForAllTerms(graph: Store, labels: SuffixLabels) {
-    console.log("_addLabelsForAllTerms");
-
-    graph.forEach(
-      (q: Quad) => {
-        if (q.subject.termType === "NamedNode") {
-          labels.planDisplayForNode(q.subject);
-        }
-        if (q.predicate.termType === "NamedNode") {
-          labels.planDisplayForNode(q.predicate);
-        }
-        if (q.object.termType === "NamedNode") {
-          labels.planDisplayForNode(q.object);
-        }
-        if (q.object.termType === "Literal" && q.object.datatype) {
-          labels.planDisplayForNode(q.object.datatype);
-        }
-      },
-      null,
-      null,
-      null,
-      null
-    );
-  }
-
-  _subjPredObjsBlock(subj: NamedNode) {
-    const columns = predsForSubj(this.graph, subj);
-    return html`
-      <div class="subject">
-        ${this.nodeDisplay.render(subj)}
-        <!-- todo: special section for uri/type-and-icon/label/comment -->
-        <div>
-          ${columns.map((p) => {
-            return this._predObjsBlock(subj, p);
-          })}
-        </div>
-      </div>
-    `;
-  }
-
-  _objCell(obj: Term) {
-    return html`
-      <div class="object">
-        ${this.nodeDisplay.render(obj)}
-        <!-- indicate what source or graph said this stmt -->
-      </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();
-    return html`
-      <div class="predicate">
-        ${this.nodeDisplay.render(pred)}
-        <div>${objs.map(this._objCell.bind(this))}</div>
-      </div>
-    `;
-  }
-
-  _drawObj(obj: Term): TemplateResult {
-    return html` <div>${this.nodeDisplay.render(obj)}</div> `;
-  }
-
-  _drawColumnHead(pred: NamedNode): TemplateResult {
-    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>
-    `;
-  }
-
-  _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);
-
-    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>
-    `;
-  }
-
-  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>
-    `;
-  }
-}
--- a/src/index.ts	Sun Mar 13 21:57:52 2022 -0700
+++ b/src/index.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -1,150 +1,3 @@
-import { LitElement, html, render, TemplateResult } from "lit";
-import { customElement, property } from "lit/decorators.js";
-
-import { Store } from "n3";
-
-import { GraphView } from "./graph_view";
-import { StreamedGraphClient } from "./streamed_graph_client";
-import { style, addFontToRootPage } from "./style";
-
-// export * from "./graph_queries";
-
-export interface VersionedGraph {
-  version: number;
-  store: Store;
-}
-
-@customElement("streamed-graph")
-export class StreamedGraph extends LitElement {
-  @property({ type: String })
-  url: string = "";
-  @property({ type: String })
-  view: string = "";
-  @property({ type: Object })
-  graph!: VersionedGraph;
-
-  @property({ type: Boolean })
-  expanded: boolean = false;
-
-  @property({ type: String })
-  status: string = "";
-
-  sg!: StreamedGraphClient;
-  graphViewDirty = true;
-
-  static styles = style;
-
-  render() {
-    const expandAction = this.expanded ? "-" : "+";
-    return html`
-      <div id="ui">
-        <span class="expander"
-          ><button @click="${this.toggleExpand}">${expandAction}</button></span
-        >
-        StreamedGraph <a href="${this.url}">[source]</a>: ${this.status}
-      </div>
-      <div id="graphView"></div>
-    `;
-  }
-
-  connectedCallback() {
-    super.connectedCallback();
-    addFontToRootPage();
-    const emptyStore = new Store();
-    this.graph = { version: -1, store: emptyStore };
-
-    this._onUrl(this.url); // todo: watch for changes and rebuild
-    if (this.expanded) {
-      this.redrawGraph();
-    }
-  }
-
-  toggleExpand() {
-    this.expanded = !this.expanded;
-    if (this.expanded) {
-      this.redrawGraph();
-    } else {
-      this.graphViewDirty = false;
-      this._graphAreaClose();
-    }
-  }
+import { StreamedGraph } from "./render/element";
 
-  redrawGraph() {
-    this.graphViewDirty = true;
-    const rl: ()=>Promise<void> = this._redrawLater.bind(this)
-    requestAnimationFrame(rl);
-  }
-
-  _onUrl(url: string) {
-    if (this.sg) {
-      this.sg.close();
-    }
-    this.sg = new StreamedGraphClient(
-      url,
-      this.onGraphChanged.bind(this),
-      (st) => {
-        this.status = st;
-      },
-      [], //window.NS,
-      []
-    );
-  }
-
-  onGraphChanged() {
-    this.graph = {
-      version: this.graph.version + 1,
-      store: this.sg.store,
-    };
-    if (this.expanded) {
-      this.redrawGraph();
-    }
-    this.dispatchEvent(
-      new CustomEvent("graph-changed", { detail: { graph: this.graph } })
-    );
-  }
-
-  async _redrawLater() {
-    if (!this.graphViewDirty) return;
-
-    if ((this.graph as VersionedGraph).store && this.graph.store) {
-      await this._graphAreaShowGraph(
-        new GraphView(this.url, this.view, this.graph.store)
-      );
-      this.graphViewDirty = false;
-    } else {
-      this._graphAreaShowPending();
-    }
-  }
-
-  _graphAreaClose() {
-    this._setGraphArea(html``);
-  }
-
-  _graphAreaShowPending() {
-    this._setGraphArea(html` <span>waiting for data...</span> `);
-  }
-
-  async _graphAreaShowGraph(graphView: GraphView) {
-    this._setGraphArea(await graphView.makeTemplate());
-  }
-
-  _setGraphArea(tmpl: TemplateResult) {
-    const el = this.shadowRoot?.getElementById("graphView");
-    if (!el) {
-      return;
-    }
-    render(tmpl, el);
-  }
-}
-
-declare global {
-  interface HTMLElementTagNameMap {
-    "streamed-graph": StreamedGraph;
-  }
-}
-
-// // allow child nodes to combine a few graphs and statics
-// //<streamed-graph id="timebankGraph"  graph="{{graph}}" expanded="true">
-// //  <member-graph url="graph/timebank/events"></member-graph>
-// //  <member-graph url="/some/static.n3"></member-graph>
-// //</streamed-graph>
+export {StreamedGraph}
--- a/src/json_ld_quads.test.ts	Sun Mar 13 21:57:52 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-import { eachJsonLdQuad } from "./json_ld_quads";
-import { Literal, DataFactory } from "n3";
-const { literal } = DataFactory;
-
-describe("eachJsonLdQuad", () => {
-  test("finds multiple graphs", () => {});
-  test("returns quads", async () => {
-    let results: Array<any> = [];
-    await eachJsonLdQuad(
-      [
-        {
-          "@id": "http://example.com/g1",
-          "@graph": [
-            {
-              "@id": "http://example.com/s1",
-              "http://example.com/p1": [{ "@value": "lit1" }]
-            }
-          ]
-        }
-      ],
-      (res: any) => results.push(res)
-    );
-    expect(results).toHaveLength(1);
-    expect(results[0].subject.value).toEqual("http://example.com/s1");
-    expect(results[0].predicate.value).toEqual("http://example.com/p1");
-    expect((results[0].object as Literal).equals(literal("lit1"))).toBeTruthy();
-    expect(results[0].graph.value).toEqual("http://example.com/g1");
-  });
-});
--- a/src/json_ld_quads.ts	Sun Mar 13 21:57:52 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,67 +0,0 @@
-import * as jsonld from "jsonld";
-import { JsonLd, JsonLdArray } from "jsonld/jsonld-spec";
-import { Quad, NamedNode, DataFactory } from "n3";
-const { literal, quad, namedNode } = DataFactory;
-
-// const { rdf } = ns;
-const rdf = { type: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" };
-
-function parseObjNode(obj: any) {
-  if (obj["@id"]) {
-    return namedNode(obj["@id"]);
-  } else {
-    if (obj["@value"] === undefined) {
-      throw new Error("no @id or @value");
-    }
-    return literal(obj["@value"], obj["@language"] || obj["@type"]);
-  }
-}
-
-function parsePred(
-  onQuad: (q: Quad) => void,
-  graphNode: NamedNode,
-  subjNode: NamedNode,
-  predKey: string,
-  subjGroup: any
-) {
-  let predNode: NamedNode;
-  if (predKey === "@type") {
-    subjGroup["@type"].forEach((aType: string) => {
-      onQuad(quad(subjNode, namedNode(rdf.type), namedNode(aType), graphNode));
-    });
-    return;
-  }
-  predNode = namedNode(predKey);
-  subjGroup[predKey].forEach(function(obj: any) {
-    const objNode = parseObjNode(obj);
-    onQuad(quad(subjNode, predNode, objNode, graphNode));
-  });
-}
-function parseSubj(
-  onQuad: (q: Quad) => void,
-  graphNode: NamedNode,
-  subjGroup: { [predOrId: string]: any }
-) {
-  const subjNode = namedNode(subjGroup["@id"]);
-  for (let predKey in subjGroup) {
-    if (predKey === "@id") {
-      continue;
-    }
-    parsePred(onQuad, graphNode, subjNode, predKey, subjGroup);
-  }
-}
-function parseGraph(onQuad: (q: Quad) => void, g: JsonLd) {
-  var graph = (g as { "@id": string })["@id"];
-  var graphNode = namedNode(graph);
-  (g as { "@graph": JsonLdArray })["@graph"].forEach(subj => {
-    parseSubj(onQuad, graphNode, subj);
-  });
-}
-
-export async function eachJsonLdQuad(
-  jsonLdObj: object,
-  onQuad: (q: Quad) => void
-) {
-  const expanded = await jsonld.expand(jsonLdObj);
-  (expanded as JsonLdArray).forEach((g: JsonLd) => parseGraph(onQuad, g));
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layout/Layout.test.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,163 @@
+import { Quad, Store, Term } from "n3";
+import { n3Graph } from "./fetchAndParse";
+import { AlignedTable, Layout, LayoutResult } from "./Layout";
+import { EX, rdf } from "./namespaces";
+import { ViewConfig } from "./ViewConfig";
+
+const twoStatements = async (): Promise<Store> => {
+  return n3Graph(`
+  @prefix : <http://example.com/> .
+  :g1 {
+    :a0 :b0 :c0 .
+    :d0 :e0 :f0 .
+  }
+  `);
+};
+
+const typedStatements = async (): Promise<Store> => {
+  return n3Graph(`
+  @prefix : <http://example.com/> .
+  :g1 {
+    :a a :T1 ; :color :red .
+    :b a :T1 ; :color :blue .
+    :c a :T1 .
+    :d a :T2 ; :size :big .
+    :e a :T1,:T2; :size :small
+  }
+  `);
+};
+function G1(s: Term, p: Term, o: Term): Quad {
+  return new Quad(s, p, o, EX("g1"));
+}
+
+describe("Layout", () => {
+  it("accepts a ViewConfig", async () => {
+    const vc = new ViewConfig();
+    await vc.readFromGraph(`
+      @prefix ex: <http://example.com/> .
+      @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+
+      <> a ex:View; rdfs:label "repos" .`);
+    const layout = new Layout(vc);
+    const lr = layout.plan(await twoStatements());
+  });
+  it("defaults to putting all triples in the ungrouped list", async () => {
+    const layout = new Layout();
+    const lr = layout.plan(await twoStatements());
+    expect(lr).toEqual({
+      sections: [
+        {
+          statements: [
+            G1(EX("a0"), EX("b0"), EX("c0")),
+            G1(EX("d0"), EX("e0"), EX("f0")),
+          ],
+        },
+      ],
+    });
+  });
+  describe("makes a table as requested by ViewConfig", () => {
+    let lr: LayoutResult;
+
+    beforeAll(async () => {
+      const vc = new ViewConfig();
+      await vc.readFromGraph(`
+        @prefix : <http://example.com/> .
+        @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+  
+        <> a :View; :table [ :primaryType :T1 ] .`);
+      const layout = new Layout(vc);
+      lr = layout.plan(await typedStatements());
+    });
+    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") }
+    ])
+      expect(sec0.rows).toEqual([
+          [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")),
+        ],
+      });
+    });
+  });
+  it("makes a table out of ungrouped triples with the same type", async () => {});
+});
+
+// describe("equality", () => {
+//   test("investigation of https://github.com/rdfjs/N3.js/issues/265", () => {
+//     const x = namedNode("x");
+//     const x2 = namedNode("x");
+//     // (NamedNode.prototype as any).hashCode = () => 0;
+//     // expect((x as any).hashCode()).toEqual((x2 as any).hashCode())
+//     expect(x === x2).toBeFalsy();
+//     expect(x == x2).toBeFalsy();
+//     expect(x.equals(x2)).toBeTruthy();
+//     let imap = Immutable.Map();
+//     imap = imap.set(x, 11);
+//     imap = imap.set(x, 22);
+//     imap = imap.set(x2, 33);
+//     expect(imap.has(x)).toBeTruthy();
+//     expect(imap.has(x2)).toBeTruthy();
+//     expect(imap.size).toEqual(1);
+//   });
+// });
+
+// describe("groupByRdfType", () => {
+//   test("finds multiple graphs", () => {});
+//   test("works", async () => {
+//     const store = new Store();
+
+//     const parser = new Parser();
+//     await new Promise((res, rej) => {
+//       parser.parse(
+//         `PREFIX : <urn:>
+//         :rs1 a :Foo; :pred1 "obj1" .
+//         :rs2 a :Foo; :pred1 "obj2" .
+//         :rs3 a :Bar .
+//         :rs4 :pred1 "obj4" .
+// `,
+//         (error, quad: Quad, prefixes: Prefixes) => {
+//           if (quad) {
+//             store.addQuad(quad);
+//           } else {
+//             res(undefined);
+//           }
+//         }
+//       );
+//     });
+//     const grouped = groupByRdfType(store);
+//     expect(Array.from(grouped.byType.keys())).toHaveLength(2);
+//     expect(grouped.byType.get(namedNode("urn:Foo"))).toEqual(
+//       Immutable.Set([namedNode("urn:rs1"), namedNode("urn:rs2")])
+//     );
+//     expect(grouped.byType.get(namedNode("urn:Bar"))).toEqual(
+//       Immutable.Set([namedNode("urn:rs3")])
+//     );
+//     expect(grouped.untypedSubjs).toEqual([namedNode("urn:rs4")]);
+//   });
+
+//   describe("MultiSubjsTypeBlockLayout", () => {
+//     test("gathers subjs", () => {
+
+//     });
+//     test("gathers preds", () => {
+
+//     });
+//     test("cells reports filled cells", () => {
+
+//     });
+//   });
+// });
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layout/Layout.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,200 @@
+// Organize graph data into tables (column orders, etc) for the view layer.
+
+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 { 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 LayoutResult {
+  sections: (AlignedTable | FreeStatements)[];
+}
+
+class AlignedTableBuilder {
+  columnPreds = Immutable.List<NamedNode>();
+  subjRowIndices = Immutable.Map<NamedNode, number>();
+  rows: (Term | null)[][] = [];
+  constructor(
+    public rdfType: NamedNode /* plus join types, sort instructions */
+  ) {}
+
+  addQuad(q: Quad) {
+    const pred = q.predicate as NamedNode;
+    const omittedColumn = pred.equals(rdf.type);
+    if (!this.columnPreds.contains(pred) && !omittedColumn) {
+      this.columnPreds = this.columnPreds.push(pred); // this is putting cols in random order
+      this.rows.forEach((r) => r.push(null));
+    }
+
+    const predIndex = omittedColumn ? null : this.columnPreds.indexOf(pred);
+    let rowIndex = this.subjRowIndices.get(q.subject as NamedNode);
+    if (rowIndex === undefined) {
+      const newRow = new Array(1 + this.columnPreds.size).fill(null);
+      newRow[0] = q.subject;
+      this.rows.push(newRow);
+      rowIndex = this.rows.length - 1;
+      this.subjRowIndices = this.subjRowIndices.set(
+        q.subject as NamedNode,
+        rowIndex
+      );
+    }
+    if (predIndex !== null) {
+      this.rows[rowIndex][1 + predIndex] = q.object;
+    }
+  }
+
+  value(): AlignedTable {
+    this.rows.sort((a, b) => {
+      const uriA = (a[0] as NamedNode).value,
+        uriB = (b[0] as NamedNode).value;
+      return uriA.localeCompare(uriB);
+    });
+    const headers = this.columnPreds
+      .map((pred) => {
+        return { rdfType: this.rdfType, pred: pred };
+      })
+      .toArray();
+    return { columnHeaders: headers, rows: this.rows };
+  }
+}
+
+function findTypesNeededForTables(viewConfig?: ViewConfig): UriSet {
+  const typesToGather: NamedNode[] = [];
+  if (viewConfig) {
+    viewConfig.tables.forEach((t: TableDesc) => {
+      typesToGather.push(t.primary);
+    });
+  }
+  return Immutable.Set(typesToGather);
+}
+
+function findSubjectsWithTypes(graph: Store, typesToGatherSet: UriSet): UriSet {
+  const subjectsToGather: NamedNode[] = [];
+  graph.forEach(
+    (q: Quad) => {
+      if (typesToGatherSet.contains(q.object as NamedNode)) {
+        subjectsToGather.push(q.subject as NamedNode);
+      }
+    },
+    null,
+    rdf.type,
+    null,
+    null
+  );
+  return Immutable.Set(subjectsToGather);
+}
+
+export class Layout {
+  constructor(public viewConfig?: ViewConfig) {}
+  plan(graph: Store): LayoutResult {
+    const typesToGatherSet = findTypesNeededForTables(this.viewConfig);
+
+    const subjectsToGatherSet = findSubjectsWithTypes(graph, typesToGatherSet);
+    const ungrouped: Quad[] = [];
+    const vc = this.viewConfig;
+    const table =
+      vc && vc.tables.length > 0
+        ? new AlignedTableBuilder(vc.tables[0].primary)
+        : null;
+
+    graph.forEach(
+      (q: Quad) => {
+        if (!subjectsToGatherSet.contains(q.subject as NamedNode) || !table) {
+          ungrouped.push(q);
+        } else {
+          table.addQuad(q);
+        }
+      },
+      null,
+      null,
+      null,
+      null
+    );
+    const res: LayoutResult = { sections: [] };
+    if (table) {
+      res.sections.push(table.value());
+    }
+    res.sections.push({ statements: ungrouped });
+    return res;
+  }
+}
+
+// interface ISP {
+//   subj: NamedNode;
+//   pred: NamedNode;
+// }
+// const SP = Immutable.Record<ISP>({
+//   subj: new NamedNode(""),
+//   pred: new NamedNode(""),
+// });
+
+// // One table of rows with a common rdf:type.
+// export class MultiSubjsTypeBlockLayout {
+//   subjs: NamedNode[];
+//   preds: NamedNode[];
+//   graphCells: Immutable.Map<ISP, Immutable.Set<Term>>;
+//   constructor(graph: Store, byType: TypeToSubjs, table: TableDesc) {
+//     const subjSet = byType.get(table.primary);
+//     this.subjs = subjSet ? Array.from(subjSet) : [];
+//     this.subjs.sort();
+
+//     let preds = Immutable.Set<NamedNode>();
+
+//     this.graphCells = Immutable.Map<ISP, Immutable.Set<Term>>().withMutations(
+//       (mutGraphCells) => {
+//         this.subjs.forEach((subj: NamedNode) => {
+//           graph.forEach(
+//             (q: Quad) => {
+//               if (!Util.isNamedNode(q.predicate)) {
+//                 throw new Error();
+//               }
+
+//               const pred = q.predicate as NamedNode;
+//               if (pred.equals(rdf.type)) {
+//                 // the whole block is labeled with the type
+//                 return;
+//               }
+//               preds = preds.add(pred);
+//               const cellKey = this.makeCellKey(subj, pred);
+//               mutGraphCells.set(
+//                 cellKey,
+//                 mutGraphCells.get(cellKey, Immutable.Set<Term>()).add(q.object)
+//               );
+//             },
+//             subj,
+//             null,
+//             null,
+//             null
+//           );
+//         });
+//       }
+//     );
+//     this.preds = Array.from(preds);
+//     this.preds.splice(this.preds.indexOf(rdf.type), 1);
+//     // also pull out label, which should be used on 1st column
+//     this.preds.sort();
+//   }
+
+//   makeCellKey(subj: NamedNode, pred: NamedNode): ISP {
+//     return SP({
+//       subj: subj,
+//       pred: pred,
+//     });
+//   }
+// }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layout/ViewConfig.test.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,25 @@
+import { Util } from "n3";
+import { ViewConfig } from "./ViewConfig";
+
+describe("ViewModel", () => {
+  it("gets a table description", async () => {
+    const vc = new ViewConfig();
+
+    await vc.readFromGraph(`
+        @prefix ex: <http://example.com/> .
+        @prefix demo: <http://example.com/demo/> .
+        @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+        @prefix : <http://bigasterisk.com/netRoutes/ns#> .
+
+        <> a ex:View ; rdfs:label "repos" .
+        <> ex:table demo:table1 .
+        demo:table1 
+        ex:primaryType :FilteredNic;
+        ex:joinType :Traffic .
+        `);
+    const NET = Util.prefix("http://bigasterisk.com/netRoutes/ns#");
+
+    expect(vc.tables[0].primary).toEqual(NET("FilteredNic"));
+    expect(vc.tables[0].joins).toEqual([NET("Traffic")]);
+  });
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layout/ViewConfig.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,70 @@
+// Load requested view (rdf data) and provide access to it
+import { DataFactory, NamedNode, Store } from "n3";
+import { fetchAndParse, n3Graph } from "./fetchAndParse";
+import { EX, RDF } from "./namespaces";
+import { labelOrTail, uriValue } from "./rdf_value";
+const Uri = DataFactory.namedNode;
+
+function firstElem<E>(seq: Iterable<E>): E {
+  for (let e of seq) {
+    return e;
+  }
+  throw new Error("no elems");
+}
+
+export interface TableDesc {
+  uri: NamedNode;
+  primary: NamedNode;
+  joins: NamedNode[];
+}
+
+export class ViewConfig {
+  graph: Store;
+  viewRoot!: NamedNode;
+  url?: string;
+  tables: TableDesc[] = [];
+
+  constructor() {
+    this.graph = new Store();
+  }
+
+  async readFromUrl(url: string | "") {
+    if (!url) {
+      return;
+    }
+    await fetchAndParse(url, this.graph);
+
+    this._read();
+  }
+
+  async readFromGraph(n3: string) {
+    this.graph = await n3Graph(n3);
+    this._read();
+  }
+
+  _read() {
+    this.viewRoot = firstElem(
+      this.graph.getSubjects(RDF("type"), EX("View"), null)
+    ) as NamedNode;
+    for (let table of this.graph.getObjects(this.viewRoot, EX("table"), null)) {
+      const tableType = uriValue(this.graph, table, EX("primaryType"));
+      const joins: NamedNode[] = [];
+      for (let joinType of this.graph.getObjects(table, EX("joinType"), null)) {
+        joins.push(joinType as NamedNode);
+      }
+      joins.sort();
+      this.tables.push({
+        uri: table as NamedNode,
+        primary: tableType,
+        joins: joins,
+      });
+    }
+    this.tables.sort();
+  }
+
+  label(): string {
+    return this.url !== undefined
+      ? labelOrTail(this.graph, Uri(this.url))
+      : "unnamed";
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layout/fetchAndParse.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,30 @@
+import { Store, Parser, Quad, Prefixes } from "n3";
+
+export async function fetchAndParse(
+  url: string,
+  store?: Store
+): Promise<Store> {
+  const res = await fetch(url);
+  const body = await res.text();
+  return n3Graph(body, store);
+}
+
+export async function n3Graph(n3: string, store?: Store): Promise<Store> {
+  if (store === undefined) {
+    store = new Store();
+  }
+
+  const parser = new Parser({ format: "TriG" });
+  await new Promise((res, rej) => {
+    parser.parse(n3, (error, quad: Quad, prefixes: Prefixes) => {
+      if (error) rej(error);
+      if (quad) {
+        store!.addQuad(quad);
+      } else {
+        res(undefined);
+      }
+    });
+  });
+
+  return store;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layout/graph_queries.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,101 @@
+// import { DataFactory, Literal, N3Store, NamedNode, Util } from "n3";
+// const { literal, namedNode } = DataFactory;
+
+// // i think this one is a worse subset of graphLiteral, below
+// export function getStringValue(
+//   store: N3Store | undefined,
+//   subj: NamedNode,
+//   pred: NamedNode,
+//   defaultValue: string = ""
+// ): string {
+//   if (store === undefined) {
+//     // this is so you can use the function before you have a graph
+//     return "...";
+//   }
+//   const objs = store.getObjects(subj, pred, null);
+//   if (objs.length == 0) {
+//     return defaultValue;
+//   }
+//   return objs[0].value;
+// }
+
+// // workaround for uris that don't have good labels in the graph
+// export function labelFromUri(
+//   uri: NamedNode,
+//   prefix: string,
+//   tailsToLabels: { [key: string]: string },
+//   defaultLabel: string
+// ) {
+//   let label = defaultLabel === undefined ? uri.value : defaultLabel;
+//   Object.entries(tailsToLabels).forEach(([tail, useLabel]) => {
+//     if (uri.equals(namedNode(prefix + tail))) {
+//       label = useLabel as string;
+//     }
+//   });
+//   return label;
+// }
+
+// export function graphLiteral(
+//   store: N3Store,
+//   subj: NamedNode,
+//   pred: string,
+//   notFoundResult?: string
+// ): Literal {
+//   const keep: Array<Literal> = [];
+//   store.forEach(
+//     q => {
+//       if (!Util.isLiteral(q.object)) {
+//         throw new Error("non literal found");
+//       }
+//       let seen = false;
+//       for (let other of keep) {
+//         // why are we getting multiple matches for the same literal? seems like a bug
+//         if (other.equals(q.object)) {
+//           seen = true;
+//         }
+//       }
+//       if (!seen) {
+//         keep.push(q.object as Literal);
+//       }
+//     },
+//     subj,
+//     namedNode(pred),
+//     null,
+//     null
+//   );
+//   if (keep.length == 0) {
+//     return literal(notFoundResult || "(missing)");
+//   }
+//   if (keep.length == 1) {
+//     return keep[0];
+//   }
+//   console.log(`${subj.value} ${pred} had ${keep.length} objects:`, keep);
+//   return keep[0];
+// }
+
+// export function graphUriValue(
+//   store: N3Store,
+//   subj: NamedNode,
+//   pred: string
+// ): NamedNode | undefined {
+//   const keep: Array<NamedNode> = [];
+//   store.forEach(
+//     q => {
+//       if (!Util.isNamedNode(q.object)) {
+//         throw new Error("non uri found");
+//       }
+//       keep.push(q.object as NamedNode);
+//     },
+//     subj,
+//     namedNode(pred),
+//     null,
+//     null
+//   );
+//   if (keep.length == 0) {
+//     return undefined;
+//   }
+//   if (keep.length == 1) {
+//     return keep[0];
+//   }
+//   throw new Error("found multiple matches for pred");
+// }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layout/json_ld_quads.test.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,29 @@
+import { eachJsonLdQuad } from "./json_ld_quads";
+import { Literal, DataFactory } from "n3";
+const { literal } = DataFactory;
+
+describe("eachJsonLdQuad", () => {
+  test("finds multiple graphs", () => {});
+  test("returns quads", async () => {
+    let results: Array<any> = [];
+    await eachJsonLdQuad(
+      [
+        {
+          "@id": "http://example.com/g1",
+          "@graph": [
+            {
+              "@id": "http://example.com/s1",
+              "http://example.com/p1": [{ "@value": "lit1" }]
+            }
+          ]
+        }
+      ],
+      (res: any) => results.push(res)
+    );
+    expect(results).toHaveLength(1);
+    expect(results[0].subject.value).toEqual("http://example.com/s1");
+    expect(results[0].predicate.value).toEqual("http://example.com/p1");
+    expect((results[0].object as Literal).equals(literal("lit1"))).toBeTruthy();
+    expect(results[0].graph.value).toEqual("http://example.com/g1");
+  });
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layout/json_ld_quads.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,67 @@
+import * as jsonld from "jsonld";
+import { JsonLd, JsonLdArray } from "jsonld/jsonld-spec";
+import { Quad, NamedNode, DataFactory } from "n3";
+const { literal, quad, namedNode } = DataFactory;
+
+// const { rdf } = ns;
+const rdf = { type: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" };
+
+function parseObjNode(obj: any) {
+  if (obj["@id"]) {
+    return namedNode(obj["@id"]);
+  } else {
+    if (obj["@value"] === undefined) {
+      throw new Error("no @id or @value");
+    }
+    return literal(obj["@value"], obj["@language"] || obj["@type"]);
+  }
+}
+
+function parsePred(
+  onQuad: (q: Quad) => void,
+  graphNode: NamedNode,
+  subjNode: NamedNode,
+  predKey: string,
+  subjGroup: any
+) {
+  let predNode: NamedNode;
+  if (predKey === "@type") {
+    subjGroup["@type"].forEach((aType: string) => {
+      onQuad(quad(subjNode, namedNode(rdf.type), namedNode(aType), graphNode));
+    });
+    return;
+  }
+  predNode = namedNode(predKey);
+  subjGroup[predKey].forEach(function(obj: any) {
+    const objNode = parseObjNode(obj);
+    onQuad(quad(subjNode, predNode, objNode, graphNode));
+  });
+}
+function parseSubj(
+  onQuad: (q: Quad) => void,
+  graphNode: NamedNode,
+  subjGroup: { [predOrId: string]: any }
+) {
+  const subjNode = namedNode(subjGroup["@id"]);
+  for (let predKey in subjGroup) {
+    if (predKey === "@id") {
+      continue;
+    }
+    parsePred(onQuad, graphNode, subjNode, predKey, subjGroup);
+  }
+}
+function parseGraph(onQuad: (q: Quad) => void, g: JsonLd) {
+  var graph = (g as { "@id": string })["@id"];
+  var graphNode = namedNode(graph);
+  (g as { "@graph": JsonLdArray })["@graph"].forEach(subj => {
+    parseSubj(onQuad, graphNode, subj);
+  });
+}
+
+export async function eachJsonLdQuad(
+  jsonLdObj: object,
+  onQuad: (q: Quad) => void
+) {
+  const expanded = await jsonld.expand(jsonLdObj);
+  (expanded as JsonLdArray).forEach((g: JsonLd) => parseGraph(onQuad, g));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layout/namespaces.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,11 @@
+import { DataFactory, Util } from "n3";
+const { namedNode } = DataFactory;
+
+export const RDFS = Util.prefix("http://www.w3.org/2000/01/rdf-schema#");
+export const RDF = Util.prefix("http://www.w3.org/1999/02/22-rdf-syntax-ns#");
+export const EX = Util.prefix("http://example.com/");
+
+export const rdf = {
+    type: namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
+  };
+  
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layout/rdf_value.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,46 @@
+import { Store, Term, NamedNode } from "n3";
+import { RDFS } from "./namespaces";
+
+function _singleValue(g: Store, s: Term, p: Term): Term {
+  const quads = g.getQuads(s, p, null, null);
+  const objs = new Set(quads.map((q) => q.object));
+  if (objs.size == 0) {
+    throw new Error("no value for " + s.value + " " + p.value);
+  } else if (objs.size == 1) {
+    const obj = objs.values().next().value;
+    return obj as Term;
+  } else {
+    throw new Error("too many different values: " + JSON.stringify(quads));
+  }
+}
+
+export function stringValue(g: Store, s: Term, p: Term): string {
+  const ret = _singleValue(g, s, p);
+  if (ret.termType != "Literal") {
+    throw new Error(`ret=${ret}`);
+  }
+  return ret.value as string;
+}
+
+export function uriValue(g: Store, s: Term, p: Term): NamedNode {
+  const ret = _singleValue(g, s, p);
+  if (ret.termType != "NamedNode") {
+    throw new Error(`ret=${ret}`);
+  }
+
+  return ret;
+}
+
+export function labelOrTail(g: Store, uri: NamedNode): string {
+  let ret: string;
+  try {
+    ret = stringValue(g, uri, RDFS("label"));
+  } catch (e) {
+    const words = uri.value.split("/");
+    ret = words[words.length - 1];
+  }
+  if (!ret) {
+    ret = uri.value;
+  }
+  return ret;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layout/streamed_graph_client.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,142 @@
+import { eachJsonLdQuad } from "./json_ld_quads";
+import { Store } from "n3";
+
+export class StreamedGraphClient {
+  // holds a n3 Store, which is synced to a server-side
+  // store that sends patches over SSE
+
+  onStatus: (msg: string) => void = function (m) {};
+  onGraphChanged: () => void = function () {};
+  store: Store;
+  _deletedCount: number = 0;
+  events!: EventSource;
+  constructor(
+    eventsUrl: string,
+    onGraphChanged: () => void,
+    onStatus: (status: string) => void,
+    prefixes: Array<Record<string, string>>,
+    staticGraphUrls: Array<string>
+  ) {
+    console.log("new StreamedGraph", eventsUrl);
+    this.onStatus = onStatus;
+    this.onGraphChanged = onGraphChanged;
+    this.onStatus("startup...");
+
+    this.store = new Store();
+
+    // Object.keys(prefixes).forEach((prefix) => {
+    //     this.store.setPrefix(prefix, prefixes[prefix]);
+    // });
+
+    this.connect(eventsUrl);
+    this.reconnectOnWake();
+
+    //     staticGraphUrls.forEach((url) => {
+    //         fetch(url).then((response) => response.text())
+    //             .then((body) => {
+    //                 // parse with n3, add to output
+    //             });
+    //     });
+  }
+
+  _vacuum() {
+    // workaround for the growing _ids map
+    this.store = new Store(this.store.getQuads(null, null, null, null));
+  }
+
+  reconnectOnWake() {
+    // it's not this, which fires on every mouse-in on a browser window, and doesn't seem to work for screen-turned-back-on
+    //window.addEventListener('focus', function() { this.connect(eventsUrl); }.bind(this));
+  }
+
+  connect(eventsUrl: string) {
+    // need to exit here if this obj has been replaced
+
+    this.onStatus("start connect...");
+    this.close();
+    if (this.events && this.events.readyState != EventSource.CLOSED) {
+      this.onStatus("zombie");
+      throw new Error("zombie eventsource");
+    }
+
+    this.events = new EventSource(eventsUrl, { withCredentials: true });
+
+    this.events.addEventListener("error", (ev) => {
+      // todo: this is piling up tons of retries and eventually multiple connections
+      this.testEventUrl(eventsUrl);
+      this.onStatus("connection lost- retrying");
+      setTimeout(() => {
+        requestAnimationFrame(() => {
+          this.connect(eventsUrl);
+        });
+      }, 3000);
+    });
+
+    this.events.addEventListener("fullGraph", async (ev) => {
+      this.onStatus("sync- full graph update");
+      await this.replaceFullGraph((ev as MessageEvent).data);
+      this.onStatus(`synced ${this.store.size}`);
+      this.onGraphChanged();
+    });
+
+    this.events.addEventListener("patch", async (ev) => {
+      this.onStatus("sync- updating");
+      await this.patchGraph((ev as MessageEvent).data);
+      window.setTimeout(() => {
+        this.onStatus(`synced ${this.store.size}`);
+      }, 60);
+      this.onGraphChanged();
+    });
+    this.onStatus("connecting...");
+  }
+
+  // these need some locks
+  async replaceFullGraph(jsonLdText: string) {
+    this.store = new Store();
+    await eachJsonLdQuad(
+      JSON.parse(jsonLdText),
+      this.store.addQuad.bind(this.store)
+    );
+  }
+
+  async patchGraph(patchJson: string) {
+    var patch = JSON.parse(patchJson).patch;
+
+    await eachJsonLdQuad(patch.deletes, (quad) => {
+      this.store.removeQuad(quad);
+      this._deletedCount++;
+    });
+    await eachJsonLdQuad(patch.adds, this.store.addQuad.bind(this.store));
+
+    if (this._deletedCount > 100) {
+      this._vacuum();
+      this._deletedCount = 0;
+    }
+  }
+
+  close() {
+    if (this.events) {
+      this.events.close();
+    }
+  }
+
+  async testEventUrl(eventsUrl: string): Promise<void> {
+    return new Promise<void>((resolve, reject) => {
+      this.onStatus("testing connection");
+      fetch(eventsUrl, {
+        method: "HEAD",
+        credentials: "include",
+      })
+        .then((value) => {
+          if (value.status == 403) {
+            reject();
+            return;
+          }
+          resolve();
+        })
+        .catch((err) => {
+          reject();
+        });
+    });
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layout/suffixLabels.test.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,70 @@
+import { SuffixLabels } from './suffixLabels';
+
+describe('_tailSegments', () => {
+  it("returns right amount", () => {
+    expect(SuffixLabels._tailSegments('http://foo/a/bb', 0)).toEqual('');
+    expect(SuffixLabels._tailSegments('http://foo/a/bb', 1)).toEqual('bb');
+    expect(SuffixLabels._tailSegments('http://foo/a/bb', 2)).toEqual('a/bb');
+    expect(SuffixLabels._tailSegments('http://foo/a/bb', 3)).toEqual('foo/a/bb');
+    expect(SuffixLabels._tailSegments('http://foo/a/bb', 4)).toEqual('/foo/a/bb');
+    expect(SuffixLabels._tailSegments('http://foo/a/bb', 5)).toEqual('http://foo/a/bb');
+  });
+  it("_tailSegments ok with trailing slash", () => {
+    expect(SuffixLabels._tailSegments('http://foo/', 0)).toEqual('');
+    expect(SuffixLabels._tailSegments('http://foo/', 1)).toEqual('');
+    expect(SuffixLabels._tailSegments('http://foo/', 2)).toEqual('foo/');
+  });
+});
+
+describe("suffixLabels", () => {
+  const fakeNode = (uri: string) => { return { nominalValue: uri } };
+
+  it("returns whole url segments", () => {
+    const suf = new SuffixLabels();
+    suf._planDisplayForUri('http://a/b/c/dd');
+    suf._planDisplayForUri('http://a/b/c/ee');
+
+    expect(suf.getLabelForNode('http://a/b/c/dd')).toEqual('dd');
+    expect(suf.getLabelForNode('http://a/b/c/ee')).toEqual('ee');
+  });
+
+  it("doesn't treat a repeated uri as a name clash", () => {
+    const suf = new SuffixLabels();
+    suf._planDisplayForUri('http://a/b/c');
+    suf._planDisplayForUri('http://a/b/c');
+
+    expect(suf.getLabelForNode('http://a/b/c')).toEqual('c');
+  });
+
+  it("moves to two segments when needed", () => {
+    const suf = new SuffixLabels();
+    suf._planDisplayForUri('http://a/b/c/d');
+    suf._planDisplayForUri('http://a/b/f/d');
+
+    expect(suf.getLabelForNode('http://a/b/c/d')).toEqual('c/d');
+    expect(suf.getLabelForNode('http://a/b/f/d')).toEqual('f/d');
+  });
+
+  it("is ok with clashes at different segment positions", () => {
+    const suf = new SuffixLabels();
+    suf._planDisplayForUri('http://z/z/z/a/b/c');
+    suf._planDisplayForUri('http://a/b/c');
+
+    expect(suf.getLabelForNode('http://z/z/z/a/b/c')).toEqual('z/a/b/c');
+    expect(suf.getLabelForNode('http://a/b/c')).toEqual('/a/b/c');
+  });
+
+  it("uses appropriately long suffixes per uri", () => {
+    const suf = new SuffixLabels();
+    suf._planDisplayForUri('http://a/b/c/d/e');
+    suf._planDisplayForUri('http://a/b/f/d/e');
+    suf._planDisplayForUri('http://a/b/c/g');
+    suf._planDisplayForUri('http://a/z');
+
+    expect(suf.getLabelForNode('http://a/b/c/d/e')).toEqual('c/d/e');
+    expect(suf.getLabelForNode('http://a/b/f/d/e')).toEqual('f/d/e');
+    expect(suf.getLabelForNode('http://a/b/c/g')).toEqual('g');
+    expect(suf.getLabelForNode('http://a/z')).toEqual('z');
+  });
+
+});
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layout/suffixLabels.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,81 @@
+import { Term } from "n3";
+
+type SuffixesNode = { usedBy?: string; children: Map<string, SuffixesNode> };
+type DisplayNode = { label?: string; link?: string };
+
+export class SuffixLabels {
+  displayNodes: Map<string, DisplayNode>;
+  usedSuffixes: SuffixesNode;
+  constructor() {
+    this.displayNodes = new Map();
+    this.usedSuffixes = { usedBy: undefined, children: new Map() };
+  }
+
+  planDisplayForNode(node: Term) {
+    const uri = node.value;
+    this._planDisplayForUri(uri);
+  }
+
+  _planDisplayForUri(uri: string) {
+    if (this.displayNodes.has(uri)) {
+      return;
+    }
+
+    const segments = uri.split("/");
+    let curs = this.usedSuffixes;
+    let label: string | undefined = undefined;
+
+    for (let i = segments.length - 1; i >= 0; i--) {
+      const seg = segments[i];
+      if (curs.usedBy && curs.usedBy != uri) {
+        this._prependClashingUri(curs);
+      }
+
+      if (!curs.children.has(seg)) {
+        const child: SuffixesNode = { usedBy: undefined, children: new Map() };
+        curs.children.set(seg, child);
+
+        if (label === undefined) {
+          label = SuffixLabels._tailSegments(uri, segments.length - i);
+          child.usedBy = uri;
+        }
+      }
+      curs = curs.children.get(seg)!;
+    }
+    this.displayNodes.set(uri, { label: label });
+  }
+
+  _prependClashingUri(curs: SuffixesNode) {
+    // Claim: When a clash is discovered, only 1 uri needs to
+    // change its length, and there will be only one child node to
+    // follow, and the clashing uri can be changed to prepend that
+    // one child (since we'll see it again if that one wasn't
+    // enough).
+    const clashNode: DisplayNode = this.displayNodes.get(curs.usedBy!)!;
+    const nextLeftSeg = curs.children.entries().next().value;
+    if (nextLeftSeg[1].usedBy) {
+      throw new Error("unexpected");
+    }
+
+    clashNode.label = nextLeftSeg[0] + "/" + clashNode.label;
+    nextLeftSeg[1].usedBy = curs.usedBy;
+    curs.usedBy = undefined;
+  }
+
+  // a substring to show for this uri
+  getLabelForNode(node: string) {
+    const dn = this.displayNodes.get(node);
+    if (dn === undefined) {
+      throw new Error(`you never called planDisplayForNode on ${node}`);
+    }
+    return dn.label;
+  }
+
+  static _tailSegments(uri: string, n: number) {
+    let i = uri.length;
+    for (let rep = 0; rep < n; rep++) {
+      i = uri.lastIndexOf("/", i - 1);
+    }
+    return uri.substr(i + 1);
+  }
+}
--- a/src/namespaces.ts	Sun Mar 13 21:57:52 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-import { DataFactory, Util } from "n3";
-const { namedNode } = DataFactory;
-
-export const RDFS = Util.prefix("http://www.w3.org/2000/01/rdf-schema#");
-export const RDF = Util.prefix("http://www.w3.org/1999/02/22-rdf-syntax-ns#");
-export const EX = Util.prefix("http://example.com/");
-
-export const rdf = {
-    type: namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
-  };
-  
\ No newline at end of file
--- a/src/rdf_value.ts	Sun Mar 13 21:57:52 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-import { Store, Term, NamedNode } from "n3";
-import { RDFS } from "./namespaces";
-
-function _singleValue(g: Store, s: Term, p: Term): Term {
-  const quads = g.getQuads(s, p, null, null);
-  const objs = new Set(quads.map((q) => q.object));
-  if (objs.size == 0) {
-    throw new Error("no value for " + s.value + " " + p.value);
-  } else if (objs.size == 1) {
-    const obj = objs.values().next().value;
-    return obj as Term;
-  } else {
-    throw new Error("too many different values: " + JSON.stringify(quads));
-  }
-}
-
-export function stringValue(g: Store, s: Term, p: Term): string {
-  const ret = _singleValue(g, s, p);
-  if (ret.termType != "Literal") {
-    throw new Error(`ret=${ret}`);
-  }
-  return ret.value as string;
-}
-
-export function uriValue(g: Store, s: Term, p: Term): NamedNode {
-  const ret = _singleValue(g, s, p);
-  if (ret.termType != "NamedNode") {
-    throw new Error(`ret=${ret}`);
-  }
-
-  return ret;
-}
-
-export function labelOrTail(g: Store, uri: NamedNode): string {
-  let ret: string;
-  try {
-    ret = stringValue(g, uri, RDFS("label"));
-  } catch (e) {
-    const words = uri.value.split("/");
-    ret = words[words.length - 1];
-  }
-  if (!ret) {
-    ret = uri.value;
-  }
-  return ret;
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/render/NodeDisplay.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,55 @@
+import { html, TemplateResult } from "lit";
+import { Literal, NamedNode, Term, Util } from "n3";
+import { SuffixLabels } from "../layout/suffixLabels";
+
+export class NodeDisplay {
+  labels: SuffixLabels;
+  constructor(labels: SuffixLabels) {
+    this.labels = labels;
+  }
+  render(n: Term | NamedNode): TemplateResult {
+    if (Util.isLiteral(n)) {
+      n = n as Literal;
+      let dtPart: any = "";
+      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.render(n.datatype)} </span>
+        `;
+      }
+      return html` <span class="literal">${n.value}${dtPart}</span> `;
+    }
+
+    if (Util.isNamedNode(n)) {
+      n = n as NamedNode;
+      let shortened = false;
+      let uriValue: string = n.value;
+      for (let [long, short] of [
+        ["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:"],
+      ]) {
+        if (uriValue.startsWith(long)) {
+          uriValue = short + uriValue.substr(long.length);
+          shortened = true;
+          break;
+        }
+      }
+      if (!shortened) {
+        let dn: string | undefined = this.labels.getLabelForNode(uriValue);
+        if (dn === undefined) {
+          throw new Error(`dn=${dn}`);
+        }
+        uriValue = dn;
+      }
+
+      return html` <a class="graphUri" href="${n.value}">${uriValue}</a> `;
+    }
+
+    return html` [${n.termType} ${n.value}] `;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/render/element.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,150 @@
+import { LitElement, html, render, TemplateResult } from "lit";
+import { customElement, property } from "lit/decorators.js";
+
+import { Store } from "n3";
+
+import { GraphView } from "./graph_view";
+import { StreamedGraphClient } from "../layout/streamed_graph_client";
+import { style, addFontToRootPage } from "./style";
+
+// export * from "./graph_queries";
+
+export interface VersionedGraph {
+  version: number;
+  store: Store;
+}
+
+@customElement("streamed-graph")
+export class StreamedGraph extends LitElement {
+  @property({ type: String })
+  url: string = "";
+  @property({ type: String })
+  view: string = "";
+  @property({ type: Object })
+  graph!: VersionedGraph;
+
+  @property({ type: Boolean })
+  expanded: boolean = false;
+
+  @property({ type: String })
+  status: string = "";
+
+  sg!: StreamedGraphClient;
+  graphViewDirty = true;
+
+  static styles = style;
+
+  render() {
+    const expandAction = this.expanded ? "-" : "+";
+    return html`
+      <div id="ui">
+        <span class="expander"
+          ><button @click="${this.toggleExpand}">${expandAction}</button></span
+        >
+        StreamedGraph <a href="${this.url}">[source]</a>: ${this.status}
+      </div>
+      <div id="graphView"></div>
+    `;
+  }
+
+  connectedCallback() {
+    super.connectedCallback();
+    addFontToRootPage();
+    const emptyStore = new Store();
+    this.graph = { version: -1, store: emptyStore };
+
+    this._onUrl(this.url); // todo: watch for changes and rebuild
+    if (this.expanded) {
+      this.redrawGraph();
+    }
+  }
+
+  toggleExpand() {
+    this.expanded = !this.expanded;
+    if (this.expanded) {
+      this.redrawGraph();
+    } else {
+      this.graphViewDirty = false;
+      this._graphAreaClose();
+    }
+  }
+
+  redrawGraph() {
+    this.graphViewDirty = true;
+    const rl: ()=>Promise<void> = this._redrawLater.bind(this)
+    requestAnimationFrame(rl);
+  }
+
+  _onUrl(url: string) {
+    if (this.sg) {
+      this.sg.close();
+    }
+    this.sg = new StreamedGraphClient(
+      url,
+      this.onGraphChanged.bind(this),
+      (st) => {
+        this.status = st;
+      },
+      [], //window.NS,
+      []
+    );
+  }
+
+  onGraphChanged() {
+    this.graph = {
+      version: this.graph.version + 1,
+      store: this.sg.store,
+    };
+    if (this.expanded) {
+      this.redrawGraph();
+    }
+    this.dispatchEvent(
+      new CustomEvent("graph-changed", { detail: { graph: this.graph } })
+    );
+  }
+
+  async _redrawLater() {
+    if (!this.graphViewDirty) return;
+
+    if ((this.graph as VersionedGraph).store && this.graph.store) {
+      await this._graphAreaShowGraph(
+        new GraphView(this.url, this.view, this.graph.store)
+      );
+      this.graphViewDirty = false;
+    } else {
+      this._graphAreaShowPending();
+    }
+  }
+
+  _graphAreaClose() {
+    this._setGraphArea(html``);
+  }
+
+  _graphAreaShowPending() {
+    this._setGraphArea(html` <span>waiting for data...</span> `);
+  }
+
+  async _graphAreaShowGraph(graphView: GraphView) {
+    this._setGraphArea(await graphView.makeTemplate());
+  }
+
+  _setGraphArea(tmpl: TemplateResult) {
+    const el = this.shadowRoot?.getElementById("graphView");
+    if (!el) {
+      return;
+    }
+    render(tmpl, el);
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    "streamed-graph": StreamedGraph;
+  }
+}
+
+// // allow child nodes to combine a few graphs and statics
+// //<streamed-graph id="timebankGraph"  graph="{{graph}}" expanded="true">
+// //  <member-graph url="graph/timebank/events"></member-graph>
+// //  <member-graph url="/some/static.n3"></member-graph>
+// //</streamed-graph>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/render/graph_view.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,196 @@
+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 { TableDesc, ViewConfig } from "../layout/ViewConfig";
+
+const { namedNode } = DataFactory;
+
+// 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;
+}
+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;
+
+    const labels = new SuffixLabels();
+    this._addLabelsForAllTerms(this.graph, labels);
+    
+    if (this.view.graph) {
+      this._addLabelsForAllTerms(this.view.graph, labels);
+    }
+    this.nodeDisplay = new NodeDisplay(labels);
+  }
+
+  _addLabelsForAllTerms(graph: Store, labels: SuffixLabels) {
+    console.log("_addLabelsForAllTerms");
+
+    graph.forEach(
+      (q: Quad) => {
+        if (q.subject.termType === "NamedNode") {
+          labels.planDisplayForNode(q.subject);
+        }
+        if (q.predicate.termType === "NamedNode") {
+          labels.planDisplayForNode(q.predicate);
+        }
+        if (q.object.termType === "NamedNode") {
+          labels.planDisplayForNode(q.object);
+        }
+        if (q.object.termType === "Literal" && q.object.datatype) {
+          labels.planDisplayForNode(q.object.datatype);
+        }
+      },
+      null,
+      null,
+      null,
+      null
+    );
+  }
+
+  _subjPredObjsBlock(subj: NamedNode) {
+    const columns = predsForSubj(this.graph, subj);
+    return html`
+      <div class="subject">
+        ${this.nodeDisplay.render(subj)}
+        <!-- todo: special section for uri/type-and-icon/label/comment -->
+        <div>
+          ${columns.map((p) => {
+            return this._predObjsBlock(subj, p);
+          })}
+        </div>
+      </div>
+    `;
+  }
+
+  _objCell(obj: Term) {
+    return html`
+      <div class="object">
+        ${this.nodeDisplay.render(obj)}
+        <!-- indicate what source or graph said this stmt -->
+      </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();
+    return html`
+      <div class="predicate">
+        ${this.nodeDisplay.render(pred)}
+        <div>${objs.map(this._objCell.bind(this))}</div>
+      </div>
+    `;
+  }
+
+  _drawObj(obj: Term): TemplateResult {
+    return html` <div>${this.nodeDisplay.render(obj)}</div> `;
+  }
+
+  _drawColumnHead(pred: NamedNode): TemplateResult {
+    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>
+    `;
+  }
+
+  _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);
+
+    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>
+    `;
+  }
+
+  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>
+    `;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/render/style.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,159 @@
+import { css } from 'lit';
+
+export function addFontToRootPage() {
+  const content = `
+  @font-face {
+    font-family: 'Allerta';
+    font-style: normal;
+    font-weight: 400;
+    font-display: swap;
+    src: url(https://fonts.gstatic.com/s/allerta/v11/TwMO-IAHRlkbx940YnYXTQ.ttf) format('truetype');
+  }`;
+
+  if (!document.getElementById('allerta-style')) {
+    var head = document.head || document.getElementsByTagName('head')[0],
+      style = document.createElement('style');
+    style.id = 'allerta-style';
+    style.type = 'text/css';
+    style.innerText = content;
+    head.appendChild(style);
+  }
+}
+
+
+export const style = css`
+:host {
+  display: flex;
+  flex-direction: column;
+  padding: 2px 0;
+}
+#ui {
+  display: inline-block;
+  width: 30em;
+}
+#ui button {
+  width: 2em;
+}
+#ui .expander {
+  display: inline-block;
+  padding: 3px;
+}
+#graphView section {
+  padding: 4px;
+}
+#graphView .spoGrid {
+  display: flex;
+  flex-direction: column;
+}
+#graphView .subject,
+#graphView .predicate {
+  display: flex;
+  align-items: baseline;
+}
+#graphView .predicate,
+#graphView .object {
+  margin-left: 5px;
+}
+#graphView .literal {
+  display: inline-block;
+  margin: 3px;
+  padding: 4px;
+}
+#graphView .literalType {
+  vertical-align: super;
+  font-size: 80%;
+}
+#graphView .resource {
+  display: inline-block;
+  margin: 2px;
+  padding: 1px 6px;
+}
+#graphView .predicate > a::before {
+  padding-right: 2px;
+  content: '━';
+  font-weight: bolder;
+  font-size: 125%;
+}
+#graphView .predicate > a::after {
+  content: '🠪';
+}
+#graphView table.typeBlock {
+  border-collapse: collapse;
+}
+#graphView table.typeBlock td {
+  white-space: nowrap;
+}
+#graphView table.typeBlock td .literal {
+  padding-top: 1px;
+  padding-bottom: 1px;
+}
+#graphView .typeBlockScroll {
+  overflow-x: auto;
+  max-width: 100%;
+}
+a {
+  color: #b1b1fd;
+  text-shadow: 1px 1px 0px rgba(4,0,255,0.58);
+  text-decoration-color: rgba(0,0,119,0.078);
+}
+body.rdfBrowsePage {
+  background: #000;
+  color: #fff;
+  font-size: 12px;
+}
+#ui {
+  border: 1px solid #808080;
+  background: #000;
+  color: #fff;
+  font-family: 'Allerta', sans-serif;
+}
+#graphView {
+  background: #000;
+  color: #fff;
+  font-family: 'Allerta', sans-serif;
+}
+#graphView section {
+  border: 1px solid #808080;
+}
+#graphView .subject {
+  border-top: 1px solid #2f2f2f;
+}
+#graphView .literal {
+  border: 1px solid #808080;
+  border-radius: 9px;
+  font-size: 115%;
+  font-family: monospace;
+}
+#graphView .subject > .node {
+  border: 2px solid #448d44;
+}
+#graphView .resource {
+  border-radius: 6px;
+  background: #add8e6;
+}
+#graphView .predicate > a {
+  color: #e49dfb;
+}
+#graphView .comment {
+  color: #008000;
+}
+#graphView table.typeBlock th {
+  border: 2px #333 outset;
+  background: #1f1f1f;
+}
+#graphView table.typeBlock td {
+  border: 2px #4a4a4a outset;
+  background: #2b2b2b;
+}
+/*
+for my pages serving rdf data, not necessarily part of browse/
+*/
+.served-resources {
+  margin-top: 4em;
+  padding-top: 1em;
+  border-top: 1px solid #808080;
+}
+.served-resources a {
+  padding-right: 2em;
+}
+  `;
\ No newline at end of file
--- a/src/streamed_graph_client.ts	Sun Mar 13 21:57:52 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,142 +0,0 @@
-import { eachJsonLdQuad } from "./json_ld_quads";
-import { Store } from "n3";
-
-export class StreamedGraphClient {
-  // holds a n3 Store, which is synced to a server-side
-  // store that sends patches over SSE
-
-  onStatus: (msg: string) => void = function (m) {};
-  onGraphChanged: () => void = function () {};
-  store: Store;
-  _deletedCount: number = 0;
-  events!: EventSource;
-  constructor(
-    eventsUrl: string,
-    onGraphChanged: () => void,
-    onStatus: (status: string) => void,
-    prefixes: Array<Record<string, string>>,
-    staticGraphUrls: Array<string>
-  ) {
-    console.log("new StreamedGraph", eventsUrl);
-    this.onStatus = onStatus;
-    this.onGraphChanged = onGraphChanged;
-    this.onStatus("startup...");
-
-    this.store = new Store();
-
-    // Object.keys(prefixes).forEach((prefix) => {
-    //     this.store.setPrefix(prefix, prefixes[prefix]);
-    // });
-
-    this.connect(eventsUrl);
-    this.reconnectOnWake();
-
-    //     staticGraphUrls.forEach((url) => {
-    //         fetch(url).then((response) => response.text())
-    //             .then((body) => {
-    //                 // parse with n3, add to output
-    //             });
-    //     });
-  }
-
-  _vacuum() {
-    // workaround for the growing _ids map
-    this.store = new Store(this.store.getQuads(null, null, null, null));
-  }
-
-  reconnectOnWake() {
-    // it's not this, which fires on every mouse-in on a browser window, and doesn't seem to work for screen-turned-back-on
-    //window.addEventListener('focus', function() { this.connect(eventsUrl); }.bind(this));
-  }
-
-  connect(eventsUrl: string) {
-    // need to exit here if this obj has been replaced
-
-    this.onStatus("start connect...");
-    this.close();
-    if (this.events && this.events.readyState != EventSource.CLOSED) {
-      this.onStatus("zombie");
-      throw new Error("zombie eventsource");
-    }
-
-    this.events = new EventSource(eventsUrl, { withCredentials: true });
-
-    this.events.addEventListener("error", (ev) => {
-      // todo: this is piling up tons of retries and eventually multiple connections
-      this.testEventUrl(eventsUrl);
-      this.onStatus("connection lost- retrying");
-      setTimeout(() => {
-        requestAnimationFrame(() => {
-          this.connect(eventsUrl);
-        });
-      }, 3000);
-    });
-
-    this.events.addEventListener("fullGraph", async (ev) => {
-      this.onStatus("sync- full graph update");
-      await this.replaceFullGraph((ev as MessageEvent).data);
-      this.onStatus(`synced ${this.store.size}`);
-      this.onGraphChanged();
-    });
-
-    this.events.addEventListener("patch", async (ev) => {
-      this.onStatus("sync- updating");
-      await this.patchGraph((ev as MessageEvent).data);
-      window.setTimeout(() => {
-        this.onStatus(`synced ${this.store.size}`);
-      }, 60);
-      this.onGraphChanged();
-    });
-    this.onStatus("connecting...");
-  }
-
-  // these need some locks
-  async replaceFullGraph(jsonLdText: string) {
-    this.store = new Store();
-    await eachJsonLdQuad(
-      JSON.parse(jsonLdText),
-      this.store.addQuad.bind(this.store)
-    );
-  }
-
-  async patchGraph(patchJson: string) {
-    var patch = JSON.parse(patchJson).patch;
-
-    await eachJsonLdQuad(patch.deletes, (quad) => {
-      this.store.removeQuad(quad);
-      this._deletedCount++;
-    });
-    await eachJsonLdQuad(patch.adds, this.store.addQuad.bind(this.store));
-
-    if (this._deletedCount > 100) {
-      this._vacuum();
-      this._deletedCount = 0;
-    }
-  }
-
-  close() {
-    if (this.events) {
-      this.events.close();
-    }
-  }
-
-  async testEventUrl(eventsUrl: string): Promise<void> {
-    return new Promise<void>((resolve, reject) => {
-      this.onStatus("testing connection");
-      fetch(eventsUrl, {
-        method: "HEAD",
-        credentials: "include",
-      })
-        .then((value) => {
-          if (value.status == 403) {
-            reject();
-            return;
-          }
-          resolve();
-        })
-        .catch((err) => {
-          reject();
-        });
-    });
-  }
-}
--- a/src/style.ts	Sun Mar 13 21:57:52 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,159 +0,0 @@
-import { css } from 'lit';
-
-export function addFontToRootPage() {
-  const content = `
-  @font-face {
-    font-family: 'Allerta';
-    font-style: normal;
-    font-weight: 400;
-    font-display: swap;
-    src: url(https://fonts.gstatic.com/s/allerta/v11/TwMO-IAHRlkbx940YnYXTQ.ttf) format('truetype');
-  }`;
-
-  if (!document.getElementById('allerta-style')) {
-    var head = document.head || document.getElementsByTagName('head')[0],
-      style = document.createElement('style');
-    style.id = 'allerta-style';
-    style.type = 'text/css';
-    style.innerText = content;
-    head.appendChild(style);
-  }
-}
-
-
-export const style = css`
-:host {
-  display: flex;
-  flex-direction: column;
-  padding: 2px 0;
-}
-#ui {
-  display: inline-block;
-  width: 30em;
-}
-#ui button {
-  width: 2em;
-}
-#ui .expander {
-  display: inline-block;
-  padding: 3px;
-}
-#graphView section {
-  padding: 4px;
-}
-#graphView .spoGrid {
-  display: flex;
-  flex-direction: column;
-}
-#graphView .subject,
-#graphView .predicate {
-  display: flex;
-  align-items: baseline;
-}
-#graphView .predicate,
-#graphView .object {
-  margin-left: 5px;
-}
-#graphView .literal {
-  display: inline-block;
-  margin: 3px;
-  padding: 4px;
-}
-#graphView .literalType {
-  vertical-align: super;
-  font-size: 80%;
-}
-#graphView .resource {
-  display: inline-block;
-  margin: 2px;
-  padding: 1px 6px;
-}
-#graphView .predicate > a::before {
-  padding-right: 2px;
-  content: '━';
-  font-weight: bolder;
-  font-size: 125%;
-}
-#graphView .predicate > a::after {
-  content: '🠪';
-}
-#graphView table.typeBlock {
-  border-collapse: collapse;
-}
-#graphView table.typeBlock td {
-  white-space: nowrap;
-}
-#graphView table.typeBlock td .literal {
-  padding-top: 1px;
-  padding-bottom: 1px;
-}
-#graphView .typeBlockScroll {
-  overflow-x: auto;
-  max-width: 100%;
-}
-a {
-  color: #b1b1fd;
-  text-shadow: 1px 1px 0px rgba(4,0,255,0.58);
-  text-decoration-color: rgba(0,0,119,0.078);
-}
-body.rdfBrowsePage {
-  background: #000;
-  color: #fff;
-  font-size: 12px;
-}
-#ui {
-  border: 1px solid #808080;
-  background: #000;
-  color: #fff;
-  font-family: 'Allerta', sans-serif;
-}
-#graphView {
-  background: #000;
-  color: #fff;
-  font-family: 'Allerta', sans-serif;
-}
-#graphView section {
-  border: 1px solid #808080;
-}
-#graphView .subject {
-  border-top: 1px solid #2f2f2f;
-}
-#graphView .literal {
-  border: 1px solid #808080;
-  border-radius: 9px;
-  font-size: 115%;
-  font-family: monospace;
-}
-#graphView .subject > .node {
-  border: 2px solid #448d44;
-}
-#graphView .resource {
-  border-radius: 6px;
-  background: #add8e6;
-}
-#graphView .predicate > a {
-  color: #e49dfb;
-}
-#graphView .comment {
-  color: #008000;
-}
-#graphView table.typeBlock th {
-  border: 2px #333 outset;
-  background: #1f1f1f;
-}
-#graphView table.typeBlock td {
-  border: 2px #4a4a4a outset;
-  background: #2b2b2b;
-}
-/*
-for my pages serving rdf data, not necessarily part of browse/
-*/
-.served-resources {
-  margin-top: 4em;
-  padding-top: 1em;
-  border-top: 1px solid #808080;
-}
-.served-resources a {
-  padding-right: 2em;
-}
-  `;
\ No newline at end of file
--- a/src/suffixLabels.test.ts	Sun Mar 13 21:57:52 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-import { SuffixLabels } from './suffixLabels';
-
-describe('_tailSegments', () => {
-  it("returns right amount", () => {
-    expect(SuffixLabels._tailSegments('http://foo/a/bb', 0)).toEqual('');
-    expect(SuffixLabels._tailSegments('http://foo/a/bb', 1)).toEqual('bb');
-    expect(SuffixLabels._tailSegments('http://foo/a/bb', 2)).toEqual('a/bb');
-    expect(SuffixLabels._tailSegments('http://foo/a/bb', 3)).toEqual('foo/a/bb');
-    expect(SuffixLabels._tailSegments('http://foo/a/bb', 4)).toEqual('/foo/a/bb');
-    expect(SuffixLabels._tailSegments('http://foo/a/bb', 5)).toEqual('http://foo/a/bb');
-  });
-  it("_tailSegments ok with trailing slash", () => {
-    expect(SuffixLabels._tailSegments('http://foo/', 0)).toEqual('');
-    expect(SuffixLabels._tailSegments('http://foo/', 1)).toEqual('');
-    expect(SuffixLabels._tailSegments('http://foo/', 2)).toEqual('foo/');
-  });
-});
-
-describe("suffixLabels", () => {
-  const fakeNode = (uri: string) => { return { nominalValue: uri } };
-
-  it("returns whole url segments", () => {
-    const suf = new SuffixLabels();
-    suf._planDisplayForUri('http://a/b/c/dd');
-    suf._planDisplayForUri('http://a/b/c/ee');
-
-    expect(suf.getLabelForNode('http://a/b/c/dd')).toEqual('dd');
-    expect(suf.getLabelForNode('http://a/b/c/ee')).toEqual('ee');
-  });
-
-  it("doesn't treat a repeated uri as a name clash", () => {
-    const suf = new SuffixLabels();
-    suf._planDisplayForUri('http://a/b/c');
-    suf._planDisplayForUri('http://a/b/c');
-
-    expect(suf.getLabelForNode('http://a/b/c')).toEqual('c');
-  });
-
-  it("moves to two segments when needed", () => {
-    const suf = new SuffixLabels();
-    suf._planDisplayForUri('http://a/b/c/d');
-    suf._planDisplayForUri('http://a/b/f/d');
-
-    expect(suf.getLabelForNode('http://a/b/c/d')).toEqual('c/d');
-    expect(suf.getLabelForNode('http://a/b/f/d')).toEqual('f/d');
-  });
-
-  it("is ok with clashes at different segment positions", () => {
-    const suf = new SuffixLabels();
-    suf._planDisplayForUri('http://z/z/z/a/b/c');
-    suf._planDisplayForUri('http://a/b/c');
-
-    expect(suf.getLabelForNode('http://z/z/z/a/b/c')).toEqual('z/a/b/c');
-    expect(suf.getLabelForNode('http://a/b/c')).toEqual('/a/b/c');
-  });
-
-  it("uses appropriately long suffixes per uri", () => {
-    const suf = new SuffixLabels();
-    suf._planDisplayForUri('http://a/b/c/d/e');
-    suf._planDisplayForUri('http://a/b/f/d/e');
-    suf._planDisplayForUri('http://a/b/c/g');
-    suf._planDisplayForUri('http://a/z');
-
-    expect(suf.getLabelForNode('http://a/b/c/d/e')).toEqual('c/d/e');
-    expect(suf.getLabelForNode('http://a/b/f/d/e')).toEqual('f/d/e');
-    expect(suf.getLabelForNode('http://a/b/c/g')).toEqual('g');
-    expect(suf.getLabelForNode('http://a/z')).toEqual('z');
-  });
-
-});
\ No newline at end of file
--- a/src/suffixLabels.ts	Sun Mar 13 21:57:52 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,81 +0,0 @@
-import { Term } from "n3";
-
-type SuffixesNode = { usedBy?: string; children: Map<string, SuffixesNode> };
-type DisplayNode = { label?: string; link?: string };
-
-export class SuffixLabels {
-  displayNodes: Map<string, DisplayNode>;
-  usedSuffixes: SuffixesNode;
-  constructor() {
-    this.displayNodes = new Map();
-    this.usedSuffixes = { usedBy: undefined, children: new Map() };
-  }
-
-  planDisplayForNode(node: Term) {
-    const uri = node.value;
-    this._planDisplayForUri(uri);
-  }
-
-  _planDisplayForUri(uri: string) {
-    if (this.displayNodes.has(uri)) {
-      return;
-    }
-
-    const segments = uri.split("/");
-    let curs = this.usedSuffixes;
-    let label: string | undefined = undefined;
-
-    for (let i = segments.length - 1; i >= 0; i--) {
-      const seg = segments[i];
-      if (curs.usedBy && curs.usedBy != uri) {
-        this._prependClashingUri(curs);
-      }
-
-      if (!curs.children.has(seg)) {
-        const child: SuffixesNode = { usedBy: undefined, children: new Map() };
-        curs.children.set(seg, child);
-
-        if (label === undefined) {
-          label = SuffixLabels._tailSegments(uri, segments.length - i);
-          child.usedBy = uri;
-        }
-      }
-      curs = curs.children.get(seg)!;
-    }
-    this.displayNodes.set(uri, { label: label });
-  }
-
-  _prependClashingUri(curs: SuffixesNode) {
-    // Claim: When a clash is discovered, only 1 uri needs to
-    // change its length, and there will be only one child node to
-    // follow, and the clashing uri can be changed to prepend that
-    // one child (since we'll see it again if that one wasn't
-    // enough).
-    const clashNode: DisplayNode = this.displayNodes.get(curs.usedBy!)!;
-    const nextLeftSeg = curs.children.entries().next().value;
-    if (nextLeftSeg[1].usedBy) {
-      throw new Error("unexpected");
-    }
-
-    clashNode.label = nextLeftSeg[0] + "/" + clashNode.label;
-    nextLeftSeg[1].usedBy = curs.usedBy;
-    curs.usedBy = undefined;
-  }
-
-  // a substring to show for this uri
-  getLabelForNode(node: string) {
-    const dn = this.displayNodes.get(node);
-    if (dn === undefined) {
-      throw new Error(`you never called planDisplayForNode on ${node}`);
-    }
-    return dn.label;
-  }
-
-  static _tailSegments(uri: string, n: number) {
-    let i = uri.length;
-    for (let rep = 0; rep < n; rep++) {
-      i = uri.lastIndexOf("/", i - 1);
-    }
-    return uri.substr(i + 1);
-  }
-}