# HG changeset patch # User drewp@bigasterisk.com # Date 1683347196 25200 # Node ID 5a1a79f54779d71cc4cd2dffa4d9c0f80c562abf # Parent d2580faef0570f08f641b6e4baa2fd7d1dccd05b big rewrite diff -r d2580faef057 -r 5a1a79f54779 src/ConfiguredSources.ts --- /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 , , , MultiStore, and ViewConfig. +// Makes the (single) MultiStore and the (updated as needed) ViewConfig. + +// This is poorly named since it deals in both the 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; +} + +export class ConfiguredSources implements IConfiguredSources { + graph: MultiStore; + viewConfig: ViewConfig; + + viewConfigChanged: SubEvent = 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(); + } +} diff -r d2580faef057 -r 5a1a79f54779 src/MultiStore.ts --- /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 = 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 = 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 & RDF.DatasetCore { + throw new Error("notimplemented"); + } + + every( + callback: QuadPredicate, + subject: OTerm, + predicate: OTerm, + object: OTerm, + graph: OTerm + ): boolean { + throw new Error("notimplemented"); + } + some( + callback: QuadPredicate, + subject: OTerm, + predicate: OTerm, + object: OTerm, + graph: OTerm + ): boolean { + throw new Error("notimplemented"); + } + getSubjects( + predicate: OTerm, + object: OTerm, + graph: OTerm + ): Array { + 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 { + 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 { + 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 { + 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 { + throw new Error("notimplemented"); + } + [Symbol.iterator](): Iterator { + 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"); + } +} diff -r d2580faef057 -r 5a1a79f54779 src/Patch.ts --- /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 { + 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); + } +} diff -r d2580faef057 -r 5a1a79f54779 src/README --- 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 diff -r d2580faef057 -r 5a1a79f54779 src/SourceGraph.ts --- /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 = 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 { + 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((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); + } +} diff -r d2580faef057 -r 5a1a79f54779 src/elements/graph-view/GraphView.ts --- /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; + +@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 + {this.nodeDisplay.render(this.viewConfig.viewRoot)}`; + return html` +
+

View: ${viewTitle}

+
+ +
+ ${lr.sections.map(this._renderSection.bind(this))} +
+ `; + } + + _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` + ${ch.rdfTypes.map((n) => nodeDisplay.render(n))} + ` + ); + + tableTypes.push(ch.rdfTypes); + heads.push(html`${nodeDisplay.render(ch.pred)}`); + } + + 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`
${nodeDisplay.render(t)}
` + ); + bodyCols.push(html`${display}`); + } + cells.push( + html` + ${headerCol} + ${bodyCols} + ` + ); + } + const tableTypesUnique = uniqueSortedTerms(tableTypes.flat()); + const typesDisplay = html`${tableTypesUnique.length == 1 ? "type" : "types"} + ${tableTypesUnique.map((n) => nodeDisplay.render(n))}`; + + return html` +
[icon] Resources of ${typesDisplay}
+
+ + + + + ${typeHeads} + + + + ${heads} + + + + ${cells} + +
Subject
+
+ `; + } + + _renderFreeStatements(section: FreeStatements): TemplateResult { + const subjects: NamedNode[] = []; + let subjPreds = Immutable.Map(); + + return html`
+ grid has rowcount ${section.subjRows.length} + ${section.subjRows.map(this._subjPredObjsBlock.bind(this))} +
`; + } + + _subjPredObjsBlock(row: SubjRow): TemplateResult { + return html` +
+ ${this.nodeDisplay?.render(row.subj)} + +
${row.predRows.map(this._predObjsBlock.bind(this))}
+
+ `; + } + + _predObjsBlock(row: PredRow): TemplateResult { + return html` +
+ ${this.nodeDisplay?.render(row.pred)} +
${row.objs.map(this._objCell.bind(this))}
+
+ `; + } + + _objCell(obj: Term): TemplateResult { + return html` +
+ ${this.nodeDisplay?.render(obj)} + +
+ `; + } + + _drawObj(obj: Term): TemplateResult { + return html`
${this.nodeDisplay?.render(obj)}
`; + } + + _drawColumnHead(pred: NamedNode): TemplateResult { + return html` ${this.nodeDisplay?.render(pred)} `; + } + + // return html` + //
[icon] Resources of type ${typeNames}
+ //
+ // + // ${this._thead(layout)} ${layout.subjs.map(this._instanceRow(layout))} + //
+ //
+ // `; + // } +} + +declare global { + interface HTMLElementTagNameMap { + "graph-view": GraphView; + } +} diff -r d2580faef057 -r 5a1a79f54779 src/elements/graph-view/NodeDisplay.ts --- /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` + ^^ ${this.render(n.datatype)} + `; + } + return html` ${n.value}${dtPart} `; + } + + 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` ${uriValue} `; + } + + return html` [${n.termType} ${n.value}] `; + } +} diff -r d2580faef057 -r 5a1a79f54779 src/elements/streamed-graph/SgSource.ts --- /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 . +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 = new SubEvent(); + newSourceGraph: SubEvent = 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 || "", + isCurrent: this.isCurrent(), + quadCount: this.quadCount(), + }; + } +} + +declare global { + interface HTMLElementTagNameMap { + "sg-source": SgSource; + } +} diff -r d2580faef057 -r 5a1a79f54779 src/elements/streamed-graph/SgView.ts --- /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 . +*/ +@customElement("sg-view") +export class SgView extends LitElement { + @property() uri: string = ""; + viewUriChanged: SubEvent = new SubEvent(); + update(changes: PropertyValueMap) { + super.update(changes); + if (changes.has("uri")) { + this.viewUriChanged.emit(new NamedNode(this.uri)); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "sg-view": SgView; + } +} diff -r d2580faef057 -r 5a1a79f54779 src/elements/streamed-graph/StreamedGraph.ts --- /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): 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} + + ${st.isCurrent ? "✓" : "…"} + + ${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` +
+ StreamedGraph: ${this.statusSummary} + +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "streamed-graph": StreamedGraph; + } +} diff -r d2580faef057 -r 5a1a79f54779 src/index.ts --- 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 }; diff -r d2580faef057 -r 5a1a79f54779 src/layout/Layout.ts --- 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(); 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) }; } } diff -r d2580faef057 -r 5a1a79f54779 src/layout/StreamedGraphClient.ts --- 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"; diff -r d2580faef057 -r 5a1a79f54779 src/layout/ViewConfig.ts --- 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(seq: Iterable): 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 = 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, + }; } } diff -r d2580faef057 -r 5a1a79f54779 src/layout/json_ld_quads.ts --- 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"; diff -r d2580faef057 -r 5a1a79f54779 src/layout/suffixLabels.ts --- 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"); } diff -r d2580faef057 -r 5a1a79f54779 src/render/GraphView.ts --- 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; - -export class GraphView { - nodeDisplay!: NodeDisplay; - constructor( - public dataSourceUrls: string[], - public graph: Store, - public viewConfig?: ViewConfig - ) {} - - async makeTemplate(): Promise { - 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 - ${this.viewConfig?.label()}`; - } - return html` -
-

- Current graph (${this.dataSourceUrls[0]})${viewTitle} -

-
- -
- ${lr.sections.map(this._renderSection.bind(this))} -
- `; - } - - _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` - ${ch.rdfTypes.map((n) => this.nodeDisplay.render(n))} - ` - ); - - tableTypes.push(ch.rdfTypes); - heads.push(html`${this.nodeDisplay.render(ch.pred)}`); - } - - 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`
${this.nodeDisplay.render(t)}
` - ); - bodyCols.push(html`${display}`); - } - cells.push( - html` - ${headerCol} - ${bodyCols} - ` - ); - } - const tableTypesUnique = uniqueSortedTerms(tableTypes.flat()); - const typesDisplay = html`${tableTypesUnique.length == 1 ? "type" : "types"} - ${tableTypesUnique.map((n) => this.nodeDisplay.render(n))}`; - - return html` -
[icon] Resources of ${typesDisplay}
-
- - - - - ${typeHeads} - - - - ${heads} - - - - ${cells} - -
Subject
-
- `; - } - - _renderFreeStatements(section: FreeStatements): TemplateResult { - const subjects: NamedNode[] = []; - let subjPreds = Immutable.Map(); - - return html`
- grid has rowcount ${section.subjRows.length} - ${section.subjRows.map(this._subjPredObjsBlock.bind(this))} -
`; - } - - _subjPredObjsBlock(row: SubjRow): TemplateResult { - return html` -
- ${this.nodeDisplay.render(row.subj)} - -
${row.predRows.map(this._predObjsBlock.bind(this))}
-
- `; - } - - _predObjsBlock(row: PredRow): TemplateResult { - return html` -
- ${this.nodeDisplay.render(row.pred)} -
${row.objs.map(this._objCell.bind(this))}
-
- `; - } - - _objCell(obj: Term): TemplateResult { - return html` -
- ${this.nodeDisplay.render(obj)} - -
- `; - } - - _drawObj(obj: Term): TemplateResult { - return html`
${this.nodeDisplay.render(obj)}
`; - } - - _drawColumnHead(pred: NamedNode): TemplateResult { - return html` ${this.nodeDisplay.render(pred)} `; - } - - // return html` - //
[icon] Resources of type ${typeNames}
- //
- // - // ${this._thead(layout)} ${layout.subjs.map(this._instanceRow(layout))} - //
- //
- // `; - // } -} diff -r d2580faef057 -r 5a1a79f54779 src/render/NodeDisplay.ts --- 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` - ^^ ${this.render(n.datatype)} - `; - } - return html` ${n.value}${dtPart} `; - } - - 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` ${uriValue} `; - } - - return html` [${n.termType} ${n.value}] `; - } -} diff -r d2580faef057 -r 5a1a79f54779 src/render/StreamedGraph.ts --- 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` -
- - StreamedGraph [source]: ${this.status} -
-
- `; - } - - 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 = 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` waiting for data... `); - } - - 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 -// // -// // -// // -// // diff -r d2580faef057 -r 5a1a79f54779 src/render/style.ts --- 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 diff -r d2580faef057 -r 5a1a79f54779 src/style.ts --- /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; + } +`;