Mercurial > code > home > repos > streamed-graph
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; + } +`;