diff src/elements/graph-view/GraphView.ts @ 128:5a1a79f54779

big rewrite
author drewp@bigasterisk.com
date Fri, 05 May 2023 21:26:36 -0700
parents src/render/GraphView.ts@c2923b20bf5c
children cf642d395be4
line wrap: on
line diff
--- /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;
+  }
+}