# HG changeset patch # User drewp@bigasterisk.com # Date 1647629858 25200 # Node ID 5e6840229a05b55edd767fd8d034a8dc3ddcd49b # Parent 042bd3361339bd5ae22807312eaa29b529406722 rewrite freeStatements rendering to put more planning in layout diff -r 042bd3361339 -r 5e6840229a05 package.json --- 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", diff -r 042bd3361339 -r 5e6840229a05 pnpm-lock.yaml --- 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 diff -r 042bd3361339 -r 5e6840229a05 src/layout/Layout.test.ts --- 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")] }, + ], + }, ], }); }); diff -r 042bd3361339 -r 5e6840229a05 src/layout/Layout.ts --- 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; export type TypeToSubjs = Immutable.Map; -// 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(); + 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; } } diff -r 042bd3361339 -r 5e6840229a05 src/layout/rdf_value.test.ts --- /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]); + }); +}); diff -r 042bd3361339 -r 5e6840229a05 src/layout/rdf_value.ts --- 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(terms: Iterable): 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; +} diff -r 042bd3361339 -r 5e6840229a05 src/render/GraphView.ts --- 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; +const emptyUriSet = Immutable.Set(); // 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 { + 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 + ${this.viewConfig.label()}`; + } + // const tables = this.view.toplevelTables(typesPresent); + return html` +
+

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

+
+ +
+ ${lr.sections.map(this._renderSection.bind(this))} +
+ `; } _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(); + + return html`
+ grid has rowcount ${section.subjRows.length} + ${section.subjRows.map(this._subjPredObjsBlock.bind(this))} +
`; + } + + _subjPredObjsBlock( row: SubjRow ): TemplateResult { return html`
- ${this.nodeDisplay.render(subj)} + ${this.nodeDisplay.render(row.subj)}
- ${columns.map((p) => { - return this._predObjsBlock(subj, p); - })} + ${row.predRows.map(this._predObjsBlock.bind(this))}
`; } - _objCell(obj: Term) { + _predObjsBlock(row: PredRow): TemplateResult { return html` -
- ${this.nodeDisplay.render(obj)} - +
+ ${this.nodeDisplay.render(row.pred)} +
${row.objs.map(this._objCell.bind(this))}
`; } - _predObjsBlock(subj: NamedNode, pred: NamedNode) { - const objsSet = new Set(); - 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` -
- ${this.nodeDisplay.render(pred)} -
${objs.map(this._objCell.bind(this))}
+
+ ${this.nodeDisplay.render(obj)} +
`; } @@ -109,87 +139,59 @@ return html` ${this.nodeDisplay.render(pred)} `; } - _thead(layout: MultiSubjsTypeBlockLayout): TemplateResult { - return html` - - - - ${layout.preds.map(this._drawColumnHead.bind(this))} - - - `; - } + // _thead(layout: MultiSubjsTypeBlockLayout): TemplateResult { + // return html` + // + // + // + // ${layout.preds.map(this._drawColumnHead.bind(this))} + // + // + // `; + // } - _msbCell(layout: MultiSubjsTypeBlockLayout, subj: NamedNode) { - return (pred: NamedNode): TemplateResult => { - const objs = layout.graphCells.get(layout.makeCellKey(subj, pred)); - if (!objs || !objs.size) { - return html` `; - } - const objsList = Array.from(objs); - objsList.sort(); - return html` ${objsList.map(this._drawObj.bind(this))} `; - }; - } - - _instanceRow(layout: MultiSubjsTypeBlockLayout) { - return (subj: NamedNode): TemplateResult => { - return html` - - ${this.nodeDisplay.render(subj)} - ${layout.preds.map(this._msbCell(layout, subj))} - - `; - }; - } - - _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` `; + // } + // const objsList = Array.from(objs); + // objsList.sort(); + // return html` ${objsList.map(this._drawObj.bind(this))} `; + // }; + // } - 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` + // + // ${this.nodeDisplay.render(subj)} + // ${layout.preds.map(this._msbCell(layout, subj))} + // + // `; + // }; + // } - return html` -
[icon] Resources of type ${typeNames}
-
- - ${this._thead(layout)} - ${layout.subjs.map(this._instanceRow(layout))} -
-
- `; - } + // _multiSubjsTypeBlock(byType: TypeToSubjs, table: TableDesc) { + // const layout = new MultiSubjsTypeBlockLayout(this.graph, byType, table); - async makeTemplate(): Promise { - await this.view.ready; - const { byType, typesPresent, untypedSubjs } = groupByRdfType(this.graph); - let viewTitle = html` (no view)`; - if (this.view.url) { - viewTitle = html` using view - ${this.view.label()}`; - } - const tables = this.view.toplevelTables(typesPresent); - return html` -
-

- Current graph (${this.url})${viewTitle} -

-
- -
- ${tables.map((t: TableDesc) => this._multiSubjsTypeBlock(byType, t))} -
- ${untypedSubjs.map(this._subjPredObjsBlock.bind(this))} -
-
- `; - } + // 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` + //
[icon] Resources of type ${typeNames}
+ //
+ // + // ${this._thead(layout)} ${layout.subjs.map(this._instanceRow(layout))} + //
+ //
+ // `; + // } } diff -r 042bd3361339 -r 5e6840229a05 src/render/StreamedGraph.ts --- 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 { diff -r 042bd3361339 -r 5e6840229a05 tasks.py --- a/tasks.py Sun Mar 13 22:02:30 2022 -0700 +++ b/tasks.py Fri Mar 18 11:57:38 2022 -0700 @@ -29,3 +29,7 @@ @task def release(ctx): ctx.run(f'pnpm publish --registry https://bigasterisk.com/js', pty=True) + +@task +def dev(ctx): + ctx.run('pnpm dev') \ No newline at end of file