Mercurial > code > home > repos > streamed-graph
changeset 107:042bd3361339
renames
author | drewp@bigasterisk.com |
---|---|
date | Sun, 13 Mar 2022 22:02:30 -0700 |
parents | 2468f2227d22 |
children | 5e6840229a05 |
files | src/index.ts src/layout/StreamedGraphClient.ts src/layout/streamed_graph_client.ts src/render/GraphView.ts src/render/StreamedGraph.ts src/render/element.ts src/render/graph_view.ts |
diffstat | 7 files changed, 488 insertions(+), 489 deletions(-) [+] |
line wrap: on
line diff
--- a/src/index.ts Sun Mar 13 22:00:30 2022 -0700 +++ b/src/index.ts Sun Mar 13 22:02:30 2022 -0700 @@ -1,3 +1,3 @@ -import { StreamedGraph } from "./render/element"; +import { StreamedGraph } from "./render/StreamedGraph"; export {StreamedGraph}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/layout/StreamedGraphClient.ts Sun Mar 13 22:02: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(); + }); + }); + } +}
--- a/src/layout/streamed_graph_client.ts Sun Mar 13 22:00:30 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(); - }); - }); - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/render/GraphView.ts Sun Mar 13 22:02:30 2022 -0700 @@ -0,0 +1,195 @@ +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); + + this.view.ready.then(() => { + this._addLabelsForAllTerms(this.view.graph, labels); + }); + this.nodeDisplay = new NodeDisplay(labels); + } + + _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 + ); + } + + _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/StreamedGraph.ts Sun Mar 13 22:02: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 "./GraphView"; +import { StreamedGraphClient } from "../layout/StreamedGraphClient"; +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>
--- a/src/render/element.ts Sun Mar 13 22:00:30 2022 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,150 +0,0 @@ -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>
--- a/src/render/graph_view.ts Sun Mar 13 22:00:30 2022 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,196 +0,0 @@ -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> - `; - } -}