changeset 128:5a1a79f54779

big rewrite
author drewp@bigasterisk.com
date Fri, 05 May 2023 21:26:36 -0700
parents d2580faef057
children f47661b9ed34
files src/ConfiguredSources.ts src/MultiStore.ts src/Patch.ts src/README src/SourceGraph.ts src/elements/graph-view/GraphView.ts src/elements/graph-view/NodeDisplay.ts src/elements/streamed-graph/SgSource.ts src/elements/streamed-graph/SgView.ts src/elements/streamed-graph/StreamedGraph.ts src/index.ts src/layout/Layout.ts src/layout/StreamedGraphClient.ts src/layout/ViewConfig.ts src/layout/json_ld_quads.ts src/layout/suffixLabels.ts src/render/GraphView.ts src/render/NodeDisplay.ts src/render/StreamedGraph.ts src/render/style.ts src/style.ts
diffstat 21 files changed, 1186 insertions(+), 635 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ConfiguredSources.ts	Fri May 05 21:26:36 2023 -0700
@@ -0,0 +1,64 @@
+import { NamedNode } from "n3";
+import { SubEvent } from "sub-events";
+import { MultiStore } from "./MultiStore";
+import { SourceGraph } from "./SourceGraph";
+import { ViewConfig } from "./layout/ViewConfig";
+
+// Connect <streamed-graph>, <sg-source>, <sg-view>, MultiStore, and ViewConfig.
+// Makes the (single) MultiStore and the (updated as needed) ViewConfig.
+
+// This is poorly named since it deals in both the <sg-source> elements that you
+// "configured" plus the set of SourceGraph objs that are actually connected to remote graphs.
+
+// sic private- this is just for documenting the interface more clearly
+interface IConfiguredSources {
+  // outputs
+  graph: MultiStore; // const- only the graph contents will change
+  viewConfig: ViewConfig;
+
+  // methods
+  newSourceGraph: (s: SourceGraph) => void;
+  lostSourceGraph: (s: SourceGraph) => void;
+  viewUriChanged: (v: NamedNode) => void;
+
+  // events
+  viewConfigChanged: SubEvent<ViewConfig>;
+}
+
+export class ConfiguredSources implements IConfiguredSources {
+  graph: MultiStore;
+  viewConfig: ViewConfig;
+
+  viewConfigChanged: SubEvent<ViewConfig> = new SubEvent();
+
+  private viewUri: NamedNode = new NamedNode("empty-view-config");
+
+  constructor() {
+    this.graph = new MultiStore();
+    this.graph.graphChanged.subscribe(() => this.viewConfigMaybeChanged());
+    this.viewConfig = new ViewConfig(this.graph, this.viewUri);
+  }
+
+  private viewConfigMaybeChanged() {
+    this.viewConfig = new ViewConfig(this.graph, this.viewUri);
+    this.viewConfigChanged.emit(this.viewConfig);
+  }
+
+  newSourceGraph(s: SourceGraph) {
+    this.graph.newStore(s);
+    this.viewConfigMaybeChanged();
+  }
+  
+  lostSourceGraph(s: SourceGraph) {
+    throw new Error("notimplemented");
+    this.viewConfigMaybeChanged();
+  }
+  
+  viewUriChanged(v: NamedNode) {
+    if (v && v == this.viewUri) {
+      return;
+    }
+    this.viewUri = v;
+    this.viewConfigMaybeChanged();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/MultiStore.ts	Fri May 05 21:26:36 2023 -0700
@@ -0,0 +1,218 @@
+import { EventEmitter } from "events";
+import {
+  BlankNode,
+  OTerm,
+  Quad,
+  QuadPredicate,
+  Store,
+  Term,
+  extractListOptions,
+} from "n3";
+import * as RDF from "rdf-js";
+import { SubEvent } from "sub-events";
+import { Patch } from "./Patch";
+import { SourceGraph } from "./SourceGraph";
+
+// queries over multiple Store objects
+export class MultiStore implements Store {
+  // emitted when there's been a net change to the graph data
+  graphChanged: SubEvent<Patch> = new SubEvent();
+
+  // sources of statements
+  private stores: Store[] = [];
+  private tempCombinedGraph: Store;
+
+  constructor() {
+    this.tempCombinedGraph = new Store();
+  }
+
+  newStore(s: SourceGraph) {
+    this.stores.push(s.store);
+    const p = new Patch(); // todo
+    s.sourceGraphChanged.subscribe((p) => {
+      console.log("patchlife1: ");
+      this.sourceGraphDataChanged(p); // todo
+    });
+  }
+
+  lostStore(s: Store) {
+    throw new Error("notimplemented");
+  }
+
+  sourceGraphDataChanged(p: Patch) {
+    console.log("patchlife2: multistore saw a graph change", p);
+
+    this.tempCombinedGraph = new Store();
+    for (let st of this.stores) {
+      for (let q of st.getQuads(null, null, null, null)) {
+        this.tempCombinedGraph.addQuad(q);
+      }
+    }
+    console.log("patchlife3: tempgraph is rebuilt");
+    this.graphChanged.emit(p);
+  }
+
+  //
+  // Store interface follows:
+  //
+  forEach(qfn: (qfn: Quad) => void, s: OTerm, p: OTerm, o: OTerm, g: OTerm) {
+    this.tempCombinedGraph.forEach(qfn, s, p, o, g);
+  }
+  countQuads(s: OTerm, p: OTerm, o: OTerm, g: OTerm): number {
+    return this.tempCombinedGraph.countQuads(s, p, o, g);
+    // const seen: Set<Quad> = new Set();
+    // let count = 0;
+    // for (let src of this.sources.currentSourceGraphs) {
+    //   for (let q of src.store.getQuads(s, p, o, g)) {
+    //     if (!seen.has(q)) {
+    //       count++;
+    //       seen.add(q);
+    //     }
+    //   }
+    // }
+    // return count;
+  }
+
+  get size(): number {
+    return this.countQuads(null, null, null, null);
+  }
+  has(quad: Quad): boolean {
+    throw new Error("notimplemented");
+  }
+  getQuads(
+    subject: OTerm,
+    predicate: OTerm,
+    object: OTerm | OTerm[],
+    graph: OTerm
+  ): Quad[] {
+    return this.tempCombinedGraph.getQuads(subject, predicate, object, graph);
+  }
+  match(
+    subject?: Term | null,
+    predicate?: Term | null,
+    object?: Term | null,
+    graph?: Term | null
+  ): RDF.Stream<Quad> & RDF.DatasetCore<Quad, Quad> {
+    throw new Error("notimplemented");
+  }
+
+  every(
+    callback: QuadPredicate<Quad>,
+    subject: OTerm,
+    predicate: OTerm,
+    object: OTerm,
+    graph: OTerm
+  ): boolean {
+    throw new Error("notimplemented");
+  }
+  some(
+    callback: QuadPredicate<Quad>,
+    subject: OTerm,
+    predicate: OTerm,
+    object: OTerm,
+    graph: OTerm
+  ): boolean {
+    throw new Error("notimplemented");
+  }
+  getSubjects(
+    predicate: OTerm,
+    object: OTerm,
+    graph: OTerm
+  ): Array<Quad["subject"]> {
+    throw new Error("notimplemented");
+  }
+  forSubjects(
+    callback: (result: Quad["subject"]) => void,
+    predicate: OTerm,
+    object: OTerm,
+    graph: OTerm
+  ): void {
+    throw new Error("notimplemented");
+  }
+  getPredicates(
+    subject: OTerm,
+    object: OTerm,
+    graph: OTerm
+  ): Array<Quad["predicate"]> {
+    throw new Error("notimplemented");
+    return [];
+  }
+  forPredicates(
+    callback: (result: Quad["predicate"]) => void,
+    subject: OTerm,
+    object: OTerm,
+    graph: OTerm
+  ): void {
+    throw new Error("notimplemented");
+  }
+  getObjects(
+    subject: OTerm,
+    predicate: OTerm,
+    graph: OTerm
+  ): Array<Quad["object"]> {
+    return this.tempCombinedGraph.getObjects(subject, predicate, graph);
+  }
+  forObjects(
+    callback: (result: Quad["object"]) => void,
+    subject: OTerm,
+    predicate: OTerm,
+    graph: OTerm
+  ): void {
+    throw new Error("notimplemented");
+  }
+  getGraphs(
+    subject: OTerm,
+    predicate: OTerm,
+    object: OTerm
+  ): Array<Quad["graph"]> {
+    throw new Error("notimplemented");
+  }
+  forGraphs(
+    callback: (result: Quad["graph"]) => void,
+    subject: OTerm,
+    predicate: OTerm,
+    object: OTerm
+  ): void {
+    throw new Error("notimplemented");
+  }
+  extractLists(options?: extractListOptions): Record<string, RDF.Term[]> {
+    throw new Error("notimplemented");
+  }
+  [Symbol.iterator](): Iterator<Quad> {
+    throw new Error("notimplemented");
+  }
+
+  add(): this {
+    throw new Error("MultiStore is readonly");
+  }
+  addQuad() {
+    throw new Error("notimplemented");
+  }
+  addQuads(): void {
+    throw new Error("MultiStore is readonly");
+  }
+  delete(): this {
+    throw new Error("MultiStore is readonly");
+  }
+  import(): EventEmitter {
+    throw new Error("MultiStore is readonly");
+  }
+  removeQuad(): void {
+    throw new Error("MultiStore is readonly");
+  }
+  removeQuads(): void {
+    throw new Error("MultiStore is readonly");
+  }
+  remove(): EventEmitter {
+    throw new Error("MultiStore is readonly");
+  }
+  removeMatches(): EventEmitter {
+    throw new Error("MultiStore is readonly");
+  }
+  deleteGraph(): EventEmitter {
+    throw new Error("MultiStore is readonly");
+  }
+  createBlankNode(): BlankNode {
+    throw new Error("MultiStore is readonly");
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Patch.ts	Fri May 05 21:26:36 2023 -0700
@@ -0,0 +1,27 @@
+import { Quad, Store } from "n3";
+import { Stream } from "rdf-js";
+
+export class Patch {
+  delQuads: Quad[] = [];
+  addQuads: Quad[] = [];
+  toString(): string {
+    return `Patch -${this.delQuads.length} +${this.addQuads.length}`;
+  }
+  constructor() { }
+
+  // fill `addQuads` with this stream
+  public async streamImport(quadStream: Stream): Promise<void> {
+    return new Promise((resolve, reject) => {
+      quadStream.on("data", (quad) => {
+        this.addQuads.push(quad);
+      });
+      quadStream.on("error", reject);
+      quadStream.on("end", resolve);
+    });
+  }
+
+  public applyToStore(s: Store) {
+    s.removeQuads(this.delQuads);
+    s.addQuads(this.addQuads);
+  }
+}
--- a/src/README	Fri May 05 21:23:44 2023 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-layout/ -> everything that doesn't involve html
-render/ -> everything that does involve html
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/SourceGraph.ts	Fri May 05 21:26:36 2023 -0700
@@ -0,0 +1,91 @@
+import { JsonLdParser } from "jsonld-streaming-parser";
+import { Parser, Store } from "n3";
+import { SubEvent } from "sub-events";
+import { Patch } from "./Patch";
+
+// Possibly-changing graph for one source. Maintains its own dataset.
+// Read-only.
+//
+// Rename to RemoteGraph? RemoteStore? SyncedStore? PatchableGraph?
+export class SourceGraph {
+  store: Store; // const; do not rebuild
+  isCurrent: boolean = false;
+  sourceGraphChanged: SubEvent<Patch> = new SubEvent();
+  constructor(public url: string /* immutable */) {
+    this.store = new Store();
+  }
+
+  dispose() {}
+
+  // Call this first, after you've subscribed to `sourceGraphChanged`. This call may
+  // synchronously fire change events.
+  //
+  async reloadRdf() {
+    const resp = await fetch(this.url);
+    const ctype = resp.headers.get("content-type");
+    if (ctype?.startsWith("text/event-stream")) {
+      await this.reloadEventStream();
+    } else {
+      await this.reloadSimpleFile(resp);
+    }
+  }
+
+  private async reloadEventStream(): Promise<void> {
+    return new Promise((res, rej) => {
+      //  todo deal with reconnects
+      const ev = new EventSource(this.url);
+      let firstPatch = true;
+      // clear store here?
+
+      // maybe the event messages should be 'add' and 'del',
+      // for less parsing and clearer order of ops.
+      ev.addEventListener("patch", async (ev) => {
+        const patchMsg = JSON.parse(ev.data);
+
+        const p = new Patch();
+
+        const parser = new JsonLdParser();
+        parser.write(JSON.stringify(patchMsg.patch.adds));
+        parser.end();
+
+        await p.streamImport(parser);
+        this.isCurrent = true;
+
+        p.applyToStore(this.store);
+        console.log("patchlife0: eventsream store changed");
+        this.sourceGraphChanged.emit(p);
+        if (firstPatch) {
+          firstPatch = false;
+          res();
+        }
+      });
+    });
+  }
+
+  private async reloadSimpleFile(resp: Response) {
+    const bodyText = await resp.text();
+    const parser = new Parser({ format: "application/trig" });
+    await new Promise<void>((res, rej) => {
+      parser.parse(bodyText, (error, quad, prefixes) => {
+        if (error) {
+          console.log("parse ~ error:", error);
+          rej(error);
+          return;
+        }
+        if (quad) {
+          this.store.addQuad(quad); // prob want to do this as one or a small number of patches
+        } else {
+          res();
+        }
+      });
+    });
+    this.isCurrent = true;
+    // this may have to fire per addQuad call for correctness, or else we batch the adds where no readers can see them in advance.
+    console.log("patchlife0: simple file store changed");
+    this.sourceGraphChanged.emit(new Patch());
+  }
+
+  quadCount(): number {
+    return this.store.countQuads(null, null, null, null);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/elements/graph-view/GraphView.ts	Fri May 05 21:26:36 2023 -0700
@@ -0,0 +1,242 @@
+import Immutable from "immutable";
+import { LitElement, PropertyValues, TemplateResult, html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { NamedNode, Quad, Store, Term } from "n3";
+import { MultiStore } from "../../MultiStore";
+import { Patch } from "../../Patch";
+import {
+  AlignedTable,
+  FreeStatements,
+  Layout,
+  PredRow,
+  SubjRow,
+} from "../../layout/Layout";
+import { ViewConfig } from "../../layout/ViewConfig";
+import { uniqueSortedTerms } from "../../layout/rdf_value";
+import { SuffixLabels } from "../../layout/suffixLabels";
+import { graphViewStyle, pageStyle } from "../../style";
+import { NodeDisplay } from "./NodeDisplay";
+type UriSet = Immutable.Set<NamedNode>;
+
+@customElement("graph-view")
+export class GraphView extends LitElement {
+  @property() graph: MultiStore | null = null;
+  @property() viewEl: HTMLElement | null = null;
+
+  viewConfig: ViewConfig | null = null;
+
+  nodeDisplay: NodeDisplay | null = null;
+  constructor() {
+    super();
+  }
+
+  static styles = [pageStyle, graphViewStyle];
+  update(changedProperties: PropertyValues) {
+    if (changedProperties.has("graph") && this.graph) {
+      // const viewUri = new NamedNode(this.viewEl?.getAttribute('uri'))
+      const viewUri = new NamedNode(new URL("#view", document.baseURI).href);
+      this.viewConfig = new ViewConfig(this.graph, viewUri);
+
+      // "when viewconfig is updated..."
+      setTimeout(()=>this.requestUpdate(), 1000)
+
+      this.graph.graphChanged.subscribe(this.onChange?.bind(this));
+    }
+    super.update(changedProperties);
+  }
+
+  onChange(p: Patch) {
+    this.requestUpdate();
+  }
+
+  render() {
+    if (!this.graph) {
+      return;
+    }
+    return this.makeTemplate(this.graph);
+  }
+
+  makeTemplate(graph: MultiStore): TemplateResult {
+    if (!this.viewConfig) {
+      throw new Error();
+    }
+    const layout = new Layout(this.viewConfig);
+    const lr = layout.plan(graph);
+
+    const labels = new SuffixLabels();
+    this._addLabelsForAllTerms(graph, labels);
+    labels.planDisplayForNode(this.viewConfig.viewRoot); // todo shoudltn be needed
+    this.nodeDisplay = new NodeDisplay(labels);
+    let viewTitle = html` (no view)`;
+    viewTitle = html` using view
+      <a href="${this.viewConfig.viewRoot.value}"
+        >{this.nodeDisplay.render(this.viewConfig.viewRoot)}</a
+      >`;
+    return html`
+      <section>
+        <h2>View: ${viewTitle}</h2>
+        <div>
+          <!-- todo: graphs and provenance.
+            These statements are all in the
+            <span data-bind="html: $root.createCurie(graphUri())">...</span> graph.-->
+        </div>
+        ${lr.sections.map(this._renderSection.bind(this))}
+      </section>
+    `;
+  }
+
+  _addLabelsForAllTerms(graph: Store, labels: SuffixLabels) {
+    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
+    );
+  }
+
+  _renderSection(section: AlignedTable | FreeStatements) {
+    if ((section as any).columnHeaders) {
+      return this._renderAlignedTable(section as AlignedTable);
+    } else {
+      return this._renderFreeStatements(section as FreeStatements);
+    }
+  }
+
+  _renderAlignedTable(section: AlignedTable): TemplateResult {
+    const nodeDisplay = this.nodeDisplay;
+    if (!nodeDisplay) throw new Error();
+    const tableTypes: NamedNode[][] = [];
+    const typeHeads: TemplateResult[] = [];
+    const heads: TemplateResult[] = [];
+    for (let ch of section.columnHeaders) {
+      const colSpan = 1; //todo
+      typeHeads.push(
+        html`<th colspan="${colSpan}">
+          ${ch.rdfTypes.map((n) => nodeDisplay.render(n))}
+        </th>`
+      );
+
+      tableTypes.push(ch.rdfTypes);
+      heads.push(html`<th>${nodeDisplay.render(ch.pred)}</th>`);
+    }
+
+    const cells = [];
+
+    for (let rowIndex in section.rows) {
+      const headerCol = nodeDisplay.render(section.rowHeaders[rowIndex]);
+      const bodyCols = [];
+      for (let cellObjs of section.rows[rowIndex]) {
+        const display = cellObjs.map(
+          (t) => html`<div>${nodeDisplay.render(t)}</div>`
+        );
+        bodyCols.push(html`<td>${display}</td>`);
+      }
+      cells.push(
+        html`<tr>
+          <th>${headerCol}</th>
+          ${bodyCols}
+        </tr>`
+      );
+    }
+    const tableTypesUnique = uniqueSortedTerms(tableTypes.flat());
+    const typesDisplay = html`${tableTypesUnique.length == 1 ? "type" : "types"}
+    ${tableTypesUnique.map((n) => nodeDisplay.render(n))}`;
+
+    return html`
+      <div>[icon] Resources of ${typesDisplay}</div>
+      <div class="typeBlockScroll">
+        <table class="typeBlock">
+          <thead>
+            <tr>
+              <th></th>
+              ${typeHeads}
+            </tr>
+            <tr>
+              <th>Subject</th>
+              ${heads}
+            </tr>
+          </thead>
+          <tbody>
+            ${cells}
+          </tbody>
+        </table>
+      </div>
+    `;
+  }
+
+  _renderFreeStatements(section: FreeStatements): TemplateResult {
+    const subjects: NamedNode[] = [];
+    let subjPreds = Immutable.Map<NamedNode, UriSet>();
+
+    return html`<div class="spoGrid">
+      grid has rowcount ${section.subjRows.length}
+      ${section.subjRows.map(this._subjPredObjsBlock.bind(this))}
+    </div>`;
+  }
+
+  _subjPredObjsBlock(row: SubjRow): TemplateResult {
+    return html`
+      <div class="subject">
+        ${this.nodeDisplay?.render(row.subj)}
+        <!-- todo: special section for uri/type-and-icon/label/comment -->
+        <div>${row.predRows.map(this._predObjsBlock.bind(this))}</div>
+      </div>
+    `;
+  }
+
+  _predObjsBlock(row: PredRow): TemplateResult {
+    return html`
+      <div class="predicate">
+        ${this.nodeDisplay?.render(row.pred)}
+        <div>${row.objs.map(this._objCell.bind(this))}</div>
+      </div>
+    `;
+  }
+
+  _objCell(obj: Term): TemplateResult {
+    return html`
+      <div class="object">
+        ${this.nodeDisplay?.render(obj)}
+        <!-- indicate what source or graph said this stmt -->
+      </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> `;
+  }
+
+  //   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>
+  //   `;
+  // }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    "graph-view": GraphView;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/elements/graph-view/NodeDisplay.ts	Fri May 05 21:26:36 2023 -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/elements/streamed-graph/SgSource.ts	Fri May 05 21:26:36 2023 -0700
@@ -0,0 +1,74 @@
+import { LitElement } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { SubEvent } from "sub-events";
+import { SourceGraph } from "../../SourceGraph";
+
+export interface SgSourceStatus {
+  url: string;
+  isCurrent: boolean;
+  quadCount: number;
+}
+
+/*
+Invisible elem used for configuring a <streamed-graph>.
+URL can be to a simple files or SSE patch stream. Wait for
+newSourceGraphAvailable events to access the rdf data.
+
+This obj doesn't care about `sourceGraphChanged` events. It just
+builds and replaces SourceGraph objs. See `ConfiguredSources`.
+*/
+@customElement("sg-source")
+export class SgSource extends LitElement {
+  @property() url: string = ""; // mutable
+
+  @property() sourceGraph: SourceGraph | null = null; // rebuilt when url changes
+
+  removingSourceGraph: SubEvent<SourceGraph> = new SubEvent();
+  newSourceGraph: SubEvent<SourceGraph> = new SubEvent();
+
+  async connectedCallback() {
+    // incorrect callback- this should rerun when the attribute changes too
+    super.connectedCallback();
+    await this.onUrlChange();
+  }
+
+  private async onUrlChange() {
+    this.removeExistingSourceGraph(this.sourceGraph);
+    if (this.url) {
+      this.sourceGraph = new SourceGraph(this.url);
+      
+      this.newSourceGraph.emit(this.sourceGraph);
+      await this.sourceGraph.reloadRdf();
+    } else {
+      this.sourceGraph = null;
+    }
+  }
+
+  private removeExistingSourceGraph(s: SourceGraph | null) {
+    if (!s) return;
+    this.removingSourceGraph.emit(s);
+    s.dispose();
+  }
+
+  isCurrent(): boolean {
+    return this.sourceGraph ? this.sourceGraph.isCurrent : false;
+  }
+
+  quadCount(): number {
+    return this.sourceGraph ? this.sourceGraph.quadCount() : 0;
+  }
+
+  status(): SgSourceStatus {
+    return {
+      url: this.url || "<empty>",
+      isCurrent: this.isCurrent(),
+      quadCount: this.quadCount(),
+    };
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    "sg-source": SgSource;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/elements/streamed-graph/SgView.ts	Fri May 05 21:26:36 2023 -0700
@@ -0,0 +1,25 @@
+import { LitElement, PropertyValueMap } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { NamedNode } from "n3";
+import { SubEvent } from "sub-events";
+
+/*
+Invisible elem used for configuring a <streamed-graph>.
+*/
+@customElement("sg-view")
+export class SgView extends LitElement {
+  @property() uri: string = "";
+  viewUriChanged: SubEvent<NamedNode> = new SubEvent();
+  update(changes: PropertyValueMap<this>) {
+    super.update(changes);
+    if (changes.has("uri")) {
+      this.viewUriChanged.emit(new NamedNode(this.uri));
+    }
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    "sg-view": SgView;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/elements/streamed-graph/StreamedGraph.ts	Fri May 05 21:26:36 2023 -0700
@@ -0,0 +1,116 @@
+import { LitElement, PropertyValueMap, TemplateResult, html } from "lit";
+import { customElement, property, state } from "lit/decorators.js";
+import { NamedNode } from "n3";
+import { ConfiguredSources } from "../../ConfiguredSources";
+import { MultiStore } from "../../MultiStore";
+import { SourceGraph } from "../../SourceGraph";
+import { ViewConfig } from "../../layout/ViewConfig";
+import { addFontToRootPage, streamedGraphStyle } from "../../style";
+import { SgSource, SgSourceStatus } from "./SgSource";
+import { SgView } from "./SgView";
+export { GraphView } from "../graph-view/GraphView";
+export { SgView } from "./SgView"; // workaround for vite mystery
+
+// Top object, and the visible/expandable element you put on your page.
+// Routes sg-source and sg-view changes to ConfiguredSources.
+@customElement("streamed-graph")
+export class StreamedGraph extends LitElement {
+  sources: ConfiguredSources;
+
+  // consumers read this
+  graph: MultiStore; // exists from ctor time, mutated in place
+
+  @property() viewConfig: ViewConfig; // immutable, rebuilt when page or graph changes (in relevant ways)
+  @state() statusSummary: TemplateResult[] = [html`...`];
+
+  // @state() viewDirty: number = 0;
+  static styles = [streamedGraphStyle];
+  constructor() {
+    super();
+    this.sources = new ConfiguredSources();
+    this.graph = this.sources.graph;
+
+    this.sources.graph.graphChanged.subscribe((p) => {
+      this.updateStatusSummary();
+    });
+
+    this.sources.viewConfigChanged.subscribe((vc) => {
+      this.viewConfig = vc;
+      // this.viewDirty++;
+    });
+    this.viewConfig = this.sources.viewConfig;
+  }
+
+  firstUpdated(ch: PropertyValueMap<this>): void {
+    super.firstUpdated(ch);
+    this.scanChildNodesOnce();
+    addFontToRootPage();
+  }
+
+  scanChildNodesOnce() {
+    for (let el of this.children || []) {
+      if (el.tagName == "SG-SOURCE") {
+        this.onSgSourceChild(el as SgSource);
+      } else if (el.tagName == "SG-VIEW") {
+        this.onSgViewChild(el as SgView);
+      } else {
+        throw new Error(`streamed-graph has unknown child ${el.tagName}`);
+      }
+    }
+    this.updateStatusSummary();
+  }
+
+  private onSgViewChild(el: SgView) {
+    el.viewUriChanged.subscribe((u: NamedNode) => {
+      this.sources.viewUriChanged(u);
+    });
+
+    const viewUri = new NamedNode(el.uri || "no-view");
+    this.sources.viewUriChanged(viewUri);
+  }
+
+  private onSgSourceChild(el: SgSource) {
+    el.newSourceGraph.subscribe((s: SourceGraph) => {
+      this.sources.newSourceGraph(s);
+    });
+
+    const st = el.sourceGraph;
+    if (st) {
+      this.sources.newSourceGraph(st);
+    }
+  }
+
+  updateStatusSummary() {
+    const displayStatus = (st: SgSourceStatus): TemplateResult => {
+      const shortName = st.url.replace(/.*[\/#]([^\.]*).*/, "$1");
+      return html`[${shortName}
+        <span class="isCurrent isCurrent-${st.isCurrent}">
+          ${st.isCurrent ? "✓" : "…"}
+        </span>
+        ${st.quadCount}] `;
+    };
+
+    this.statusSummary = Array.from(this.children)
+      .filter((ch) => ch.tagName == "SG-SOURCE")
+      .map((el: Element) => (el as SgSource).status())
+      .map(displayStatus);
+  }
+
+  render() {
+    return html`
+      <details open>
+        <summary>StreamedGraph: ${this.statusSummary}</summary>
+        <graph-view
+          .graph=${this.graph}
+          .viewConfig=${this.viewConfig}
+        ></graph-view>
+      </details>
+    `;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    "streamed-graph": StreamedGraph;
+  }
+}
--- a/src/index.ts	Fri May 05 21:23:44 2023 -0700
+++ b/src/index.ts	Fri May 05 21:26:36 2023 -0700
@@ -1,3 +1,5 @@
-import { StreamedGraph } from "./render/StreamedGraph";
+import { StreamedGraph } from "./elements/streamed-graph/StreamedGraph";
+import { SgSource } from "./elements/streamed-graph/SgSource";
+import { SgView } from "./elements/streamed-graph/SgView";
 
-export {StreamedGraph}
+export { StreamedGraph, SgSource, SgView };
--- a/src/layout/Layout.ts	Fri May 05 21:23:44 2023 -0700
+++ b/src/layout/Layout.ts	Fri May 05 21:26:36 2023 -0700
@@ -220,9 +220,12 @@
   return out;
 }
 
-function freeStatmentsSection(stmts: Quad[]): FreeStatements {
+function freeStatmentsSection(viewConfig: ViewConfig, stmts: Quad[]): FreeStatements {
   const subjs: NamedNode[] = [];
   stmts.forEach((q) => {
+    if (viewConfig.freeStatementsHidePred.has(q.predicate)) {
+      return;
+    }
     subjs.push(q.subject as NamedNode);
   });
   return {
@@ -230,7 +233,7 @@
       const preds: NamedNode[] = [];
       let po = Immutable.Map<NamedNode, Term[]>();
       stmts.forEach((q) => {
-        if (q.subject.equals(subj)) {
+        if (q.subject.equals(subj) && !viewConfig.freeStatementsHidePred.has(q.predicate)) {
           const p = q.predicate as NamedNode;
           preds.push(p);
           po = addToValues(po, p, q.object as NamedNode);
@@ -248,7 +251,7 @@
 
 // The description of how this page should look: sections, tables, etc.
 export class Layout {
-  constructor(public viewConfig?: ViewConfig) {}
+  constructor(public viewConfig: ViewConfig) {}
 
   private groupAllStatements(
     graph: Store,
@@ -273,6 +276,7 @@
   }
 
   private generateSections(
+    viewConfig: ViewConfig,
     tableBuilders: AlignedTableBuilder[],
     ungrouped: Quad[]
   ): (AlignedTable | FreeStatements)[] {
@@ -283,13 +287,13 @@
       }
     }
     if (ungrouped.length) {
-      sections.push(freeStatmentsSection(ungrouped));
+      sections.push(freeStatmentsSection(viewConfig, ungrouped));
     }
     return sections;
   }
 
   plan(graph: Store): LayoutResult {
-    const vcTables = this.viewConfig ? this.viewConfig.tables : [];
+    const vcTables = this.viewConfig.tables;
     const tableBuilders = vcTables.map(
       (t) => new AlignedTableBuilder(t.primary, t.joins, t.links)
     );
@@ -297,6 +301,6 @@
     const tablesWantingSubject = subjectsToTablesMap(graph, tableBuilders);
     const ungrouped: Quad[] = [];
     this.groupAllStatements(graph, tablesWantingSubject, ungrouped);
-    return { sections: this.generateSections(tableBuilders, ungrouped) };
+    return { sections: this.generateSections(this.viewConfig, tableBuilders, ungrouped) };
   }
 }
--- a/src/layout/StreamedGraphClient.ts	Fri May 05 21:23:44 2023 -0700
+++ b/src/layout/StreamedGraphClient.ts	Fri May 05 21:26:36 2023 -0700
@@ -1,3 +1,6 @@
+// this got rewritten in SourceGraph.ts
+
+
 import { eachJsonLdQuad } from "./json_ld_quads";
 import { Store } from "n3";
 
--- a/src/layout/ViewConfig.ts	Fri May 05 21:23:44 2023 -0700
+++ b/src/layout/ViewConfig.ts	Fri May 05 21:26:36 2023 -0700
@@ -1,8 +1,8 @@
-// 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";
+import Immutable from "immutable"; // mostly using this for the builtin equals() testing, since NamedNode(x)!=NamedNode(x)
+import { DataFactory, NamedNode, Quad_Predicate, Term } from "n3";
+import { MultiStore } from "../MultiStore";
+import { EX } from "./namespaces";
+import { uriValue } from "./rdf_value";
 const Uri = DataFactory.namedNode;
 
 function firstElem<E>(seq: Iterable<E>): E {
@@ -24,63 +24,75 @@
   links: Link[];
 }
 
+// High-level guide to how to draw the page, independent of the graph data.
+// Layout.ts turns this plus the actual graph data into a structure that's
+// close to the final render.
 export class ViewConfig {
-  graph: Store;
-  viewRoot!: NamedNode;
-  url?: string;
-  tables: TableDesc[] = [];
+  viewRoot: NamedNode; // this structure...
+  graph: MultiStore; // in this graph...
+  tables: TableDesc[] = []; // populates all the rest of these fields for use by Layout
+  freeStatementsHidePred: Immutable.Set<Quad_Predicate> = Immutable.Set();
 
-  constructor() {
-    this.graph = new Store();
+  constructor(graph: MultiStore, viewUri: NamedNode) {
+    this.graph = graph;
+    this.viewRoot = viewUri;
+    // todo
+    const here = "https://bigasterisk.com/lanscape/";
+    if (this.viewRoot.value.startsWith(here)) {
+      this.viewRoot = new NamedNode(this.viewRoot.value.slice(here.length));
+    }
+    // todo: might need to reread if graph changes
+    this.read();
   }
 
-  async readFromUrl(url: string | "") {
-    if (!url) {
-      return;
+  private read() {
+    for (let table of this.graph.getObjects(this.viewRoot, EX("table"), null)) {
+      const t = this.readTable(table);
+      this.tables.push(t);
     }
-    this.url = url;
-    await fetchAndParse(url, this.graph);
+    this.tables.sort();
 
-    this._read();
-  }
-
-  async readFromGraph(n3: string) {
-    this.graph = await n3Graph(n3);
-    this._read();
+    this.readHides();
   }
 
-  _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);
+  private readHides() {
+    for (let hideInstruction of this.graph.getObjects(
+      this.viewRoot,
+      EX("freeStatementsHide"),
+      null
+    )) {
+      for (let pred of this.graph.getObjects(
+        hideInstruction,
+        EX("predicate"),
+        null
+      )) {
+        this.freeStatementsHidePred = this.freeStatementsHidePred.add(
+          pred as Quad_Predicate
+        );
       }
-      joins.sort();
+    }
+  }
 
-      const links: Link[] = [];
-      for (let linkDesc of this.graph.getObjects(table, EX("link"), null)) {
-        links.push({
-          pred: uriValue(this.graph, linkDesc, EX("predicate")),
-        });
-      }
+  private readTable(table: Term): TableDesc {
+    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,
-        links: links,
+    const links: Link[] = [];
+    for (let linkDesc of this.graph.getObjects(table, EX("link"), null)) {
+      links.push({
+        pred: uriValue(this.graph, linkDesc, EX("predicate")),
       });
     }
-    this.tables.sort();
-  }
 
-  label(): string {
-    return this.url !== undefined
-      ? labelOrTail(this.graph, Uri(this.url))
-      : "unnamed";
+    return {
+      uri: table as NamedNode,
+      primary: tableType,
+      joins: joins,
+      links: links,
+    };
   }
 }
--- a/src/layout/json_ld_quads.ts	Fri May 05 21:23:44 2023 -0700
+++ b/src/layout/json_ld_quads.ts	Fri May 05 21:26:36 2023 -0700
@@ -1,3 +1,5 @@
+// unused?
+
 import * as jsonld from "jsonld";
 import { JsonLd, JsonLdArray } from "jsonld/jsonld-spec";
 import { Quad, NamedNode, DataFactory } from "n3";
--- a/src/layout/suffixLabels.ts	Fri May 05 21:23:44 2023 -0700
+++ b/src/layout/suffixLabels.ts	Fri May 05 21:26:36 2023 -0700
@@ -52,7 +52,13 @@
     // 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;
+    const e = curs.children.entries()
+    const n = e.next()
+    if (n.done) {
+      return; // todo - this ignores the bad url
+      throw new Error()
+    }
+    const nextLeftSeg = n.value
     if (nextLeftSeg[1].usedBy) {
       throw new Error("unexpected");
     }
--- a/src/render/GraphView.ts	Fri May 05 21:23:44 2023 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,202 +0,0 @@
-import Immutable from "immutable";
-import { html, TemplateResult } from "lit";
-import { NamedNode, Quad, Store, Term } from "n3";
-import {
-  AlignedTable,
-  FreeStatements,
-  Layout,
-  PredRow,
-  SubjRow,
-} from "../layout/Layout";
-import { uniqueSortedTerms } from "../layout/rdf_value";
-import { SuffixLabels } from "../layout/suffixLabels";
-import { ViewConfig } from "../layout/ViewConfig";
-import { NodeDisplay } from "./NodeDisplay";
-
-type UriSet = Immutable.Set<NamedNode>;
-
-export class GraphView {
-  nodeDisplay!: NodeDisplay;
-  constructor(
-    public dataSourceUrls: string[],
-    public graph: Store,
-    public viewConfig?: ViewConfig
-  ) {}
-
-  async makeTemplate(): Promise<TemplateResult> {
-    const layout = new Layout(this.viewConfig);
-    const lr = layout.plan(this.graph);
-
-    const labels = new SuffixLabels();
-    this._addLabelsForAllTerms(this.graph, labels);
-
-    this.nodeDisplay = new NodeDisplay(labels);
-    let viewTitle = html` (no view)`;
-    if (this.viewConfig?.url) {
-      viewTitle = html` using view
-        <a href="${this.viewConfig?.url}">${this.viewConfig?.label()}</a>`;
-    }
-    return html`
-      <section>
-        <h2>
-          Current graph (<a href="${this.dataSourceUrls[0]}"
-            >${this.dataSourceUrls[0]}</a
-          >)${viewTitle}
-        </h2>
-        <div>
-          <!-- todo: graphs and provenance.
-            These statements are all in the
-            <span data-bind="html: $root.createCurie(graphUri())">...</span> graph.-->
-        </div>
-        ${lr.sections.map(this._renderSection.bind(this))}
-      </section>
-    `;
-  }
-
-  _addLabelsForAllTerms(graph: Store, labels: SuffixLabels) {
-    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
-    );
-  }
-
-  _renderSection(section: AlignedTable | FreeStatements) {
-    if ((section as any).columnHeaders) {
-      return this._renderAlignedTable(section as AlignedTable);
-    } else {
-      return this._renderFreeStatements(section as FreeStatements);
-    }
-  }
-
-  _renderAlignedTable(section: AlignedTable): TemplateResult {
-    const tableTypes: NamedNode[][] = [];
-    const typeHeads: TemplateResult[] = [];
-    const heads: TemplateResult[] = [];
-    for (let ch of section.columnHeaders) {
-      const colSpan = 1; //todo
-      typeHeads.push(
-        html`<th colspan="${colSpan}">
-          ${ch.rdfTypes.map((n) => this.nodeDisplay.render(n))}
-        </th>`
-      );
-
-      tableTypes.push(ch.rdfTypes);
-      heads.push(html`<th>${this.nodeDisplay.render(ch.pred)}</th>`);
-    }
-
-    const cells = [];
-
-    for (let rowIndex in section.rows) {
-      const headerCol = this.nodeDisplay.render(section.rowHeaders[rowIndex]);
-      const bodyCols = [];
-      for (let cellObjs of section.rows[rowIndex]) {
-        const display = cellObjs.map(
-          (t) => html`<div>${this.nodeDisplay.render(t)}</div>`
-        );
-        bodyCols.push(html`<td>${display}</td>`);
-      }
-      cells.push(
-        html`<tr>
-          <th>${headerCol}</th>
-          ${bodyCols}
-        </tr>`
-      );
-    }
-    const tableTypesUnique = uniqueSortedTerms(tableTypes.flat());
-    const typesDisplay = html`${tableTypesUnique.length == 1 ? "type" : "types"}
-    ${tableTypesUnique.map((n) => this.nodeDisplay.render(n))}`;
-
-    return html`
-      <div>[icon] Resources of ${typesDisplay}</div>
-      <div class="typeBlockScroll">
-        <table class="typeBlock">
-          <thead>
-            <tr>
-              <th></th>
-              ${typeHeads}
-            </tr>
-            <tr>
-              <th>Subject</th>
-              ${heads}
-            </tr>
-          </thead>
-          <tbody>
-            ${cells}
-          </tbody>
-        </table>
-      </div>
-    `;
-  }
-
-  _renderFreeStatements(section: FreeStatements): TemplateResult {
-    const subjects: NamedNode[] = [];
-    let subjPreds = Immutable.Map<NamedNode, UriSet>();
-
-    return html`<div class="spoGrid">
-      grid has rowcount ${section.subjRows.length}
-      ${section.subjRows.map(this._subjPredObjsBlock.bind(this))}
-    </div>`;
-  }
-
-  _subjPredObjsBlock(row: SubjRow): TemplateResult {
-    return html`
-      <div class="subject">
-        ${this.nodeDisplay.render(row.subj)}
-        <!-- todo: special section for uri/type-and-icon/label/comment -->
-        <div>${row.predRows.map(this._predObjsBlock.bind(this))}</div>
-      </div>
-    `;
-  }
-
-  _predObjsBlock(row: PredRow): TemplateResult {
-    return html`
-      <div class="predicate">
-        ${this.nodeDisplay.render(row.pred)}
-        <div>${row.objs.map(this._objCell.bind(this))}</div>
-      </div>
-    `;
-  }
-
-  _objCell(obj: Term): TemplateResult {
-    return html`
-      <div class="object">
-        ${this.nodeDisplay.render(obj)}
-        <!-- indicate what source or graph said this stmt -->
-      </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> `;
-  }
-
-  //   return html`
-  //     <div>[icon] Resources of type ${typeNames}</div>
-  //     <div class="typeBlockScroll">
-  //       <table class="typeBlock">
-  //         ${this._thead(layout)} ${layout.subjs.map(this._instanceRow(layout))}
-  //       </table>
-  //     </div>
-  //   `;
-  // }
-}
--- a/src/render/NodeDisplay.ts	Fri May 05 21:23:44 2023 -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 "../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}] `;
-  }
-}
--- a/src/render/StreamedGraph.ts	Fri May 05 21:23:44 2023 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,152 +0,0 @@
-import { html, LitElement, render, TemplateResult } from "lit";
-import { customElement, property } from "lit/decorators.js";
-import { Store } from "n3";
-import { StreamedGraphClient } from "../layout/StreamedGraphClient";
-import { ViewConfig } from "../layout/ViewConfig";
-import { GraphView } from "./GraphView";
-import { addFontToRootPage, style } from "./style";
-
-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) {
-      const vc = new ViewConfig();
-      if (this.view) {
-        await vc.readFromUrl(this.view); // too often!
-      }
-
-      await this._graphAreaShowGraph(
-        new GraphView([this.url], this.graph.store, vc)
-      );
-      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>
--- a/src/render/style.ts	Fri May 05 21:23:44 2023 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,166 +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 tr:nth-child(even) td:nth-child(even) { background: #1a191c; }
-#graphView table tr:nth-child(even) td:nth-child(odd) { background: #181719; }
-#graphView table tr:nth-child(odd) td:nth-child(even) { background: #201e22; }
-#graphView table tr:nth-child(odd) td:nth-child(odd) { background: #1e1c1e; }
-#graphView table td,#graphView table th {
-  vertical-align:top;
-}
-#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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/style.ts	Fri May 05 21:26:36 2023 -0700
@@ -0,0 +1,187 @@
+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 pageStyle = css`
+  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;
+  }
+
+  /*
+  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;
+  }
+`;
+
+export const streamedGraphStyle =   css`
+:host {
+  display: inline-block;
+  min-width: 30em;
+  padding: 0 0 0 1em;
+  border: 1px solid #808080;
+  background: #000;
+  color: #fff;
+  font-family: 'Allerta', sans-serif;
+
+    display: flex;
+    flex-direction: column;
+    padding: 2px 0;
+  
+}
+.isCurrent {
+  font-weight: bold;
+  font-size: 167%;
+  display: inline-block;
+  height: 1.4em;
+  vertical-align: middle;
+}
+.isCurrent-true {
+  color: green;
+}
+.isCurrent-false {
+  color: orange;
+}
+`;
+
+export const graphViewStyle = css`
+  section {
+    padding: 4px;
+  }
+  .spoGrid {
+    display: flex;
+    flex-direction: column;
+  }
+  .subject,
+  .predicate {
+    display: flex;
+    align-items: baseline;
+  }
+  .predicate,
+  .object {
+    margin-left: 5px;
+  }
+  .literal {
+    display: inline-block;
+    margin: 3px;
+    padding: 4px;
+  }
+  .literalType {
+    vertical-align: super;
+    font-size: 80%;
+  }
+  .resource {
+    display: inline-block;
+    margin: 2px;
+    padding: 1px 6px;
+  }
+  .predicate > a::before {
+    padding-right: 2px;
+    content: "━";
+    font-weight: bolder;
+    font-size: 125%;
+  }
+  .predicate > a::after {
+    content: "🠪";
+  }
+  table.typeBlock {
+    border-collapse: collapse;
+  }
+  table.typeBlock td {
+    white-space: nowrap;
+  }
+  table tr:nth-child(even) td:nth-child(even) {
+    background: #1a191c;
+  }
+  table tr:nth-child(even) td:nth-child(odd) {
+    background: #181719;
+  }
+  table tr:nth-child(odd) td:nth-child(even) {
+    background: #201e22;
+  }
+  table tr:nth-child(odd) td:nth-child(odd) {
+    background: #1e1c1e;
+  }
+  table td,
+  table th {
+    vertical-align: top;
+  }
+  table.typeBlock td .literal {
+    padding-top: 1px;
+    padding-bottom: 1px;
+  }
+  .typeBlockScroll {
+    overflow-x: auto;
+    max-width: 100%;
+  }
+  /* ------------------ */
+  :host {
+    background: #000;
+    color: #fff;
+    font-family: "Allerta", sans-serif;
+  }
+  section {
+    border: 1px solid #808080;
+  }
+  .subject {
+    border-top: 1px solid #2f2f2f;
+  }
+  .literal {
+    border: 1px solid #808080;
+    border-radius: 9px;
+    font-size: 115%;
+    font-family: monospace;
+  }
+  .subject > .node {
+    border: 2px solid #448d44;
+  }
+  .resource {
+    border-radius: 6px;
+    background: #add8e6;
+  }
+  .predicate > a {
+    color: #e49dfb;
+  }
+  .comment {
+    color: #008000;
+  }
+  table.typeBlock th {
+    border: 2px #333 outset;
+    background: #1f1f1f;
+  }
+  table.typeBlock td {
+    border: 2px #4a4a4a outset;
+    background: #2b2b2b;
+  }
+`;