Mercurial > code > home > repos > streamed-graph
changeset 108:5e6840229a05
rewrite freeStatements rendering to put more planning in layout
author | drewp@bigasterisk.com |
---|---|
date | Fri, 18 Mar 2022 11:57:38 -0700 |
parents | 042bd3361339 |
children | cbcd82d21356 |
files | package.json pnpm-lock.yaml src/layout/Layout.test.ts src/layout/Layout.ts src/layout/rdf_value.test.ts src/layout/rdf_value.ts src/render/GraphView.ts src/render/StreamedGraph.ts tasks.py |
diffstat | 9 files changed, 271 insertions(+), 152 deletions(-) [+] |
line wrap: on
line diff
--- a/package.json Sun Mar 13 22:02:30 2022 -0700 +++ b/package.json Fri Mar 18 11:57:38 2022 -0700 @@ -22,10 +22,9 @@ "immutable": "^4.0.0", "jsonld": "^2.0.2", "lit": "^2.1.3", - "n3": "^1.13.0", + "n3": "git+https://github.com/rdfjs/N3.js.git#088006449c9e8275351db604b3d184071fef31a7", "rdf-js": "^4.0.2" }, - "n3 is hacked":"to remove hashCode getter", "devDependencies": { "@types/jest": "^27.4.0", "jest": "^27.5.1",
--- a/pnpm-lock.yaml Sun Mar 13 22:02:30 2022 -0700 +++ b/pnpm-lock.yaml Fri Mar 18 11:57:38 2022 -0700 @@ -8,7 +8,7 @@ jest: ^27.5.1 jsonld: ^2.0.2 lit: ^2.1.3 - n3: ^1.13.0 + n3: git+https://github.com/rdfjs/N3.js.git#088006449c9e8275351db604b3d184071fef31a7 rdf-js: ^4.0.2 stylus: ^0.56.0 ts-jest: ^27.1.3 @@ -22,7 +22,7 @@ immutable: 4.0.0 jsonld: 2.0.2 lit: 2.1.3 - n3: 1.13.0 + n3: github.com/rdfjs/N3.js/088006449c9e8275351db604b3d184071fef31a7 rdf-js: 4.0.2 devDependencies: @@ -2556,14 +2556,6 @@ resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: true - /n3/1.13.0: - resolution: {integrity: sha512-GMB4ypBfnuf6mmwbtyN6Whc8TfuVDedxc4n+3wsacQH/h0+RjaEobGMhlWrFLDsqVbT94XA6+q9yysMO5SadKA==} - engines: {node: '>=8.0'} - dependencies: - queue-microtask: 1.2.3 - readable-stream: 3.6.0 - dev: false - /nanoid/3.2.0: resolution: {integrity: sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3432,3 +3424,15 @@ y18n: 5.0.8 yargs-parser: 20.2.9 dev: true + + github.com/rdfjs/N3.js/088006449c9e8275351db604b3d184071fef31a7: + resolution: {tarball: https://codeload.github.com/rdfjs/N3.js/tar.gz/088006449c9e8275351db604b3d184071fef31a7} + name: n3 + version: 1.13.0 + engines: {node: '>=8.0'} + prepare: true + requiresBuild: true + dependencies: + queue-microtask: 1.2.3 + readable-stream: 3.6.0 + dev: false
--- a/src/layout/Layout.test.ts Sun Mar 13 22:02:30 2022 -0700 +++ b/src/layout/Layout.test.ts Fri Mar 18 11:57:38 2022 -0700 @@ -47,9 +47,9 @@ expect(lr).toEqual({ sections: [ { - statements: [ - G1(EX("a0"), EX("b0"), EX("c0")), - G1(EX("d0"), EX("e0"), EX("f0")), + subjRows: [ + { subj: EX("a0"), predRows: [{ pred: EX("b0"), objs: [EX("c0")] }] }, + { subj: EX("d0"), predRows: [{ pred: EX("e0"), objs: [EX("f0")] }] }, ], }, ], @@ -68,27 +68,32 @@ const layout = new Layout(vc); lr = layout.plan(await typedStatements()); }); - it("returns 2 sections", ()=>{ + it("returns 2 sections", () => { expect(lr.sections).toHaveLength(2); - }) + }); it("puts the right type in the table", async () => { const sec0 = lr.sections[0] as AlignedTable; expect(sec0.columnHeaders).toEqual([ { rdfType: EX("T1"), pred: EX("color") }, - { rdfType: EX("T1"), pred: EX("size") } - ]) + { rdfType: EX("T1"), pred: EX("size") }, + ]); expect(sec0.rows).toEqual([ - [EX("a"), EX("red"), null], - [EX("b"), EX("blue"),null], - [EX("c"), null, null], - [EX("e"), null, EX('small')], - ]); + [EX("a"), EX("red"), null], + [EX("b"), EX("blue"), null], + [EX("c"), null, null], + [EX("e"), null, EX("small")], + ]); }); it("leaves the rest ungrouped", async () => { expect(lr.sections[1]).toEqual({ - statements: [ - G1(EX("d"), rdf.type, EX("T2")), - G1(EX("d"), EX("size"), EX("big")), + subjRows: [ + { + subj: EX("d"), + predRows: [ + { pred: EX("size"), objs: [EX("big")] }, + { pred: rdf.type, objs: [EX("T2")] }, + ], + }, ], }); });
--- a/src/layout/Layout.ts Sun Mar 13 22:02:30 2022 -0700 +++ b/src/layout/Layout.ts Fri Mar 18 11:57:38 2022 -0700 @@ -3,25 +3,36 @@ import Immutable from "immutable"; // mostly using this for the builtin equals() testing, since NamedNode(x)!=NamedNode(x) import { NamedNode, Quad, Store, Term } from "n3"; import { rdf } from "./namespaces"; +import { uniqueSortedTerms } from "./rdf_value"; import { TableDesc, ViewConfig } from "./ViewConfig"; type UriSet = Immutable.Set<NamedNode>; export type TypeToSubjs = Immutable.Map<NamedNode, UriSet>; -// https://github.com/rdfjs/N3.js/issues/265 -(NamedNode.prototype as any).hashCode = () => 0; - interface ColumnHeader { rdfType: NamedNode; pred: NamedNode; } + export interface AlignedTable { columnHeaders: ColumnHeader[]; rows: (Term | null)[][]; // each row is 1 wider than columnHeaders since the 1st element is the subject for that row } -interface FreeStatements { - statements: Quad[]; + +export interface PredRow { + pred: NamedNode; + objs: Term[]; } + +export interface SubjRow { + subj: NamedNode; + predRows: PredRow[]; +} + +export interface FreeStatements { + subjRows: SubjRow[]; +} + export interface LayoutResult { sections: (AlignedTable | FreeStatements)[]; } @@ -100,6 +111,34 @@ return Immutable.Set(subjectsToGather); } +function freeStatmentsSection(stmts: Quad[]): FreeStatements { + const subjs: NamedNode[] = []; + stmts.forEach((q) => { + subjs.push(q.subject as NamedNode); + }); + return { + subjRows: uniqueSortedTerms(subjs).map((subj) => { + const preds: NamedNode[] = []; + let po = Immutable.Map<NamedNode, Term[]>(); + stmts.forEach((q) => { + if (q.subject.equals(subj)) { + const p = q.predicate as NamedNode; + preds.push(p); + po = po.set(p, po.get(p, [])); + po.get(p)?.push(q.object as Term); + } + }); + + const rows: PredRow[] = []; + uniqueSortedTerms(preds).forEach((p) => { + rows.push({ pred: p, objs: uniqueSortedTerms(po.get(p, [])) }); + }); + return { subj: subj, predRows: rows }; + }), + }; +} + +// The description of how this page should look: sections, tables, etc. export class Layout { constructor(public viewConfig?: ViewConfig) {} plan(graph: Store): LayoutResult { @@ -130,7 +169,7 @@ if (table) { res.sections.push(table.value()); } - res.sections.push({ statements: ungrouped }); + res.sections.push(freeStatmentsSection(ungrouped)); return res; } }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/layout/rdf_value.test.ts Fri Mar 18 11:57:38 2022 -0700 @@ -0,0 +1,49 @@ +import Immutable from "immutable"; +import { DataFactory, NamedNode, Term } from "n3"; +import { EX } from "./namespaces"; +import { uniqueSortedTerms } from "./rdf_value"; +const { namedNode, literal } = DataFactory; +describe("Immutable.Set", () => { + it("contains", () => { + const s = Immutable.Set([EX("e1")]); + expect(s.contains(EX("e1"))).toBeTruthy(); + expect(s.contains(EX("e2"))).toBeFalsy(); + }); +}); + +const uri1 = namedNode("http://example.com/1"); +const uri2 = namedNode("http://example.com/2"); +const lit1 = literal("lit1"); +const lit2 = literal("lit2"); +const lit3 = literal("lit2", namedNode("#othertype")); +const lit4 = literal("http://example.com/1"); // sic literal (that looks like a URI) + +describe("uniqueSortedTerms", () => { + it("takes Term arrays", () => { + const actual = uniqueSortedTerms([lit1] as Term[]); + expect(actual).toEqual([lit1]); + }); + it("takes NamedNode arrays", () => { + const actual = uniqueSortedTerms([uri1] as NamedNode[]); + expect(actual).toEqual([uri1]); + }); + it("dedups URIs", () => { + expect(uniqueSortedTerms([uri1, uri1, uri2])).toEqual([uri1, uri2]); + }); + it("sorts URIs", () => { + expect(uniqueSortedTerms([uri2, uri1])).toEqual([uri1, uri2]); + }); + it("dedups literals", () => { + expect(uniqueSortedTerms([lit1, lit2, lit2, lit3, lit3])).toEqual([ + lit1, + lit2, + lit3, + ]); + }); + it("sorts literals", () => { + expect(uniqueSortedTerms([lit3, lit2, lit1])).toEqual([lit1, lit2, lit3]); + }); + it("doesn't confuse literal URI strings", () => { + expect(uniqueSortedTerms([uri1, lit4])).toEqual([lit4, uri1]); + }); +});
--- a/src/layout/rdf_value.ts Sun Mar 13 22:02:30 2022 -0700 +++ b/src/layout/rdf_value.ts Fri Mar 18 11:57:38 2022 -0700 @@ -44,3 +44,16 @@ } return ret; } + +export function uniqueSortedTerms<T extends NamedNode | Term>(terms: Iterable<T>): T[] { + const uniques: T[] = []; + const seen = new Set(); + for (let o of terms) { + if (!seen.has(o.id)) { + seen.add(o.id); + uniques.push(o); + } + } + uniques.sort((a,b)=>{return a.id.localeCompare(b.id)}); + return uniques; +}
--- a/src/render/GraphView.ts Sun Mar 13 22:02:30 2022 -0700 +++ b/src/render/GraphView.ts Fri Mar 18 11:57:38 2022 -0700 @@ -1,12 +1,16 @@ +import Immutable from "immutable"; import { html, TemplateResult } from "lit"; import { DataFactory, Literal, NamedNode, Quad, Store, Term } from "n3"; import { NodeDisplay } from "./NodeDisplay"; import { SuffixLabels } from "../layout/suffixLabels"; -import { Layout } from "../layout/Layout"; +import { AlignedTable, FreeStatements, Layout, PredRow, SubjRow } from "../layout/Layout"; import { TableDesc, ViewConfig } from "../layout/ViewConfig"; +import { uniqueSortedTerms } from "../layout/rdf_value"; const { namedNode } = DataFactory; +type UriSet = Immutable.Set<NamedNode>; +const emptyUriSet = Immutable.Set<NamedNode>(); // https://github.com/rdfjs/N3.js/issues/265 if ((Literal.prototype as any).hashCode === undefined) { (Literal.prototype as any).hashCode = () => 0; @@ -15,22 +19,42 @@ (NamedNode.prototype as any).hashCode = () => 0; } export class GraphView { - url: string; - view: View; - graph: Store; - nodeDisplay: NodeDisplay; - constructor(url: string, viewUrl: string, graph: Store) { - this.url = url; - this.view = new View(viewUrl); - this.graph = graph; + 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.view.ready.then(() => { - this._addLabelsForAllTerms(this.view.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>`; + } + // const tables = this.view.toplevelTables(typesPresent); + 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) { @@ -55,48 +79,54 @@ null ); } + _renderSection(section: AlignedTable | FreeStatements) { + if ((section as any).columnHeaders) { + return this._renderAlignedTable(section as AlignedTable); + } else { + return this._renderFreeStatements(section as FreeStatements); + } + } - _subjPredObjsBlock(subj: NamedNode) { - const columns = predsForSubj(this.graph, subj); + _renderAlignedTable(section: AlignedTable): TemplateResult { + return html`aligned table section`; + } + + _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(subj)} + ${this.nodeDisplay.render(row.subj)} <!-- todo: special section for uri/type-and-icon/label/comment --> <div> - ${columns.map((p) => { - return this._predObjsBlock(subj, p); - })} + ${row.predRows.map(this._predObjsBlock.bind(this))} </div> </div> `; } - _objCell(obj: Term) { + _predObjsBlock(row: PredRow): TemplateResult { return html` - <div class="object"> - ${this.nodeDisplay.render(obj)} - <!-- indicate what source or graph said this stmt --> + <div class="predicate"> + ${this.nodeDisplay.render(row.pred)} + <div>${row.objs.map(this._objCell.bind(this))}</div> </div> `; } - _predObjsBlock(subj: NamedNode, pred: NamedNode) { - const objsSet = new Set<Term>(); - this.graph.forEach( - (q: Quad) => { - objsSet.add(q.object); - }, - subj, - pred, - null, - null - ); - const objs = Array.from(objsSet.values()); - objs.sort(); + _objCell(obj: Term): TemplateResult { return html` - <div class="predicate"> - ${this.nodeDisplay.render(pred)} - <div>${objs.map(this._objCell.bind(this))}</div> + <div class="object"> + ${this.nodeDisplay.render(obj)} + <!-- indicate what source or graph said this stmt --> </div> `; } @@ -109,87 +139,59 @@ return html` <th>${this.nodeDisplay.render(pred)}</th> `; } - _thead(layout: MultiSubjsTypeBlockLayout): TemplateResult { - return html` - <thead> - <tr> - <th></th> - ${layout.preds.map(this._drawColumnHead.bind(this))} - </tr> - </thead> - `; - } + // _thead(layout: MultiSubjsTypeBlockLayout): TemplateResult { + // return html` + // <thead> + // <tr> + // <th></th> + // ${layout.preds.map(this._drawColumnHead.bind(this))} + // </tr> + // </thead> + // `; + // } - _msbCell(layout: MultiSubjsTypeBlockLayout, subj: NamedNode) { - return (pred: NamedNode): TemplateResult => { - const objs = layout.graphCells.get(layout.makeCellKey(subj, pred)); - if (!objs || !objs.size) { - return html` <td></td> `; - } - const objsList = Array.from(objs); - objsList.sort(); - return html` <td>${objsList.map(this._drawObj.bind(this))}</td> `; - }; - } - - _instanceRow(layout: MultiSubjsTypeBlockLayout) { - return (subj: NamedNode): TemplateResult => { - return html` - <tr> - <td>${this.nodeDisplay.render(subj)}</td> - ${layout.preds.map(this._msbCell(layout, subj))} - </tr> - `; - }; - } - - _multiSubjsTypeBlock(byType: TypeToSubjs, table: TableDesc) { - const layout = new MultiSubjsTypeBlockLayout(this.graph, byType, table); + // _msbCell(layout: MultiSubjsTypeBlockLayout, subj: NamedNode) { + // return (pred: NamedNode): TemplateResult => { + // const objs = layout.graphCells.get(layout.makeCellKey(subj, pred)); + // if (!objs || !objs.size) { + // return html` <td></td> `; + // } + // const objsList = Array.from(objs); + // objsList.sort(); + // return html` <td>${objsList.map(this._drawObj.bind(this))}</td> `; + // }; + // } - let typeNames = [html`${this.nodeDisplay.render(table.primary)}`]; - if (table.joins) { - typeNames.push(html` joined with [`); - for (let j of table.joins) { - typeNames.push(html`${this.nodeDisplay.render(j)}`); - } - typeNames.push(html`]`); - } + // _instanceRow(layout: MultiSubjsTypeBlockLayout) { + // return (subj: NamedNode): TemplateResult => { + // return html` + // <tr> + // <td>${this.nodeDisplay.render(subj)}</td> + // ${layout.preds.map(this._msbCell(layout, subj))} + // </tr> + // `; + // }; + // } - 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> - `; - } + // _multiSubjsTypeBlock(byType: TypeToSubjs, table: TableDesc) { + // const layout = new MultiSubjsTypeBlockLayout(this.graph, byType, table); - async makeTemplate(): Promise<TemplateResult> { - await this.view.ready; - const { byType, typesPresent, untypedSubjs } = groupByRdfType(this.graph); - let viewTitle = html` (no view)`; - if (this.view.url) { - viewTitle = html` using view - <a href="${this.view.url}">${this.view.label()}</a>`; - } - const tables = this.view.toplevelTables(typesPresent); - return html` - <section> - <h2> - Current graph (<a href="${this.url}">${this.url}</a>)${viewTitle} - </h2> - <div> - <!-- todo: graphs and provenance. - These statements are all in the - <span data-bind="html: $root.createCurie(graphUri())">...</span> graph.--> - </div> - ${tables.map((t: TableDesc) => this._multiSubjsTypeBlock(byType, t))} - <div class="spoGrid"> - ${untypedSubjs.map(this._subjPredObjsBlock.bind(this))} - </div> - </section> - `; - } + // let typeNames = [html`${this.nodeDisplay.render(table.primary)}`]; + // if (table.joins) { + // typeNames.push(html` joined with [`); + // for (let j of table.joins) { + // typeNames.push(html`${this.nodeDisplay.render(j)}`); + // } + // typeNames.push(html`]`); + // } + + // return html` + // <div>[icon] Resources of type ${typeNames}</div> + // <div class="typeBlockScroll"> + // <table class="typeBlock"> + // ${this._thead(layout)} ${layout.subjs.map(this._instanceRow(layout))} + // </table> + // </div> + // `; + // } }
--- a/src/render/StreamedGraph.ts Sun Mar 13 22:02:30 2022 -0700 +++ b/src/render/StreamedGraph.ts Fri Mar 18 11:57:38 2022 -0700 @@ -6,6 +6,7 @@ import { GraphView } from "./GraphView"; import { StreamedGraphClient } from "../layout/StreamedGraphClient"; import { style, addFontToRootPage } from "./style"; +import { ViewConfig } from "../layout/ViewConfig"; // export * from "./graph_queries"; @@ -107,8 +108,11 @@ if (!this.graphViewDirty) return; if ((this.graph as VersionedGraph).store && this.graph.store) { + + const vc = new ViewConfig() + await this._graphAreaShowGraph( - new GraphView(this.url, this.view, this.graph.store) + new GraphView([this.url], this.graph.store, vc) ); this.graphViewDirty = false; } else {