Mercurial > code > home > repos > streamed-graph
changeset 104:1aea03d306af
WIP Layout. tests are passing but they're a little wrong
author | drewp@bigasterisk.com |
---|---|
date | Sat, 12 Mar 2022 22:41:43 -0800 |
parents | f12feced00ce |
children | 4bb8c7775c83 |
files | package.json src/Layout.test.ts src/Layout.ts |
diffstat | 3 files changed, 124 insertions(+), 130 deletions(-) [+] |
line wrap: on
line diff
--- a/package.json Sat Mar 12 00:42:00 2022 -0800 +++ b/package.json Sat Mar 12 22:41:43 2022 -0800 @@ -25,6 +25,7 @@ "n3": "^1.13.0", "rdf-js": "^4.0.2" }, + "n3 is hacked":"to remove hashCode getter", "devDependencies": { "@types/jest": "^27.4.0", "jest": "^27.5.1",
--- a/src/Layout.test.ts Sat Mar 12 00:42:00 2022 -0800 +++ b/src/Layout.test.ts Sat Mar 12 22:41:43 2022 -0800 @@ -1,25 +1,15 @@ -import { Layout, LayoutResult } from "./Layout"; +import { Quad, Store, Term } from "n3"; +import { n3Graph } from "./fetchAndParse"; +import { AlignedTable, Layout, LayoutResult } from "./Layout"; +import { EX, rdf } from "./namespaces"; import { ViewConfig } from "./ViewConfig"; -import { - DataFactory, - Store, - Prefixes, - Parser, - Quad, - NamedNode, - Term, -} from "n3"; -import { EX, rdf } from "./namespaces"; -import Immutable from "immutable"; -import { n3Graph } from "./fetchAndParse"; -const { namedNode } = DataFactory; const twoStatements = async (): Promise<Store> => { return n3Graph(` @prefix : <http://example.com/> . :g1 { - :a :b :c . - :d :e :f . + :a0 :b0 :c0 . + :d0 :e0 :f0 . } `); }; @@ -58,8 +48,8 @@ sections: [ { statements: [ - G1(EX("a"), EX("b"), EX("c")), - G1(EX("d"), EX("e"), EX("f")), + G1(EX("a0"), EX("b0"), EX("c0")), + G1(EX("d0"), EX("e0"), EX("f0")), ], }, ], @@ -76,24 +66,28 @@ <> a :View; :table [ :primaryType :T1 ] .`); const layout = new Layout(vc); - const lr = layout.plan(await typedStatements()); + lr = layout.plan(await typedStatements()); + }); + it("returns 2 sections", ()=>{ expect(lr.sections).toHaveLength(2); - }); + }) it("puts the right type in the table", async () => { - expect(lr.sections[0]).toEqual({ - columnHeaders: [{ rdfType: EX("T1"), pred: EX("color") }], - rows: [ - [EX("a"), EX("red")], - [EX("b"), EX("blue")], - [EX("c"), null], - [EX("e"), null], - ], - }); + const sec0 = lr.sections[0] as AlignedTable; + expect(sec0.columnHeaders).toEqual([ + { rdfType: EX("T1"), pred: EX("color") }, + { 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')], + ]); }); it("leaves the rest ungrouped", async () => { expect(lr.sections[1]).toEqual({ statements: [ - G1(EX("d"), rdf.type, EX("T1")), + G1(EX("d"), rdf.type, EX("T2")), G1(EX("d"), EX("size"), EX("big")), ], });
--- a/src/Layout.ts Sat Mar 12 00:42:00 2022 -0800 +++ b/src/Layout.ts Sat Mar 12 22:41:43 2022 -0800 @@ -1,35 +1,21 @@ // Organize graph data into tables (column orders, etc) for the view layer. import Immutable from "immutable"; // mostly using this for the builtin equals() testing, since NamedNode(x)!=NamedNode(x) -import { - DataFactory, - NamedNode, - Quad, - Quad_Object, - Store, - Term, - Util, -} from "n3"; -import { ViewConfig } from "./ViewConfig"; - -const { namedNode } = DataFactory; - -// // import ns from 'n3/src/IRIs'; -// // const { rdf } = ns; +import { NamedNode, Quad, Store, Term } from "n3"; +import { rdf } from "./namespaces"; +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 -if ((NamedNode.prototype as any).hashCode === undefined) { - (NamedNode.prototype as any).hashCode = () => 0; -} +(NamedNode.prototype as any).hashCode = () => 0; interface ColumnHeader { - rdfType?: NamedNode; // could be more than one column that introduces an rdf:type for a join + rdfType: NamedNode; pred: NamedNode; } -interface AlignedTable { +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 } @@ -40,102 +26,115 @@ sections: (AlignedTable | FreeStatements)[]; } +class AlignedTableBuilder { + columnPreds = Immutable.List<NamedNode>(); + subjRowIndices = Immutable.Map<NamedNode, number>(); + rows: (Term | null)[][] = []; + constructor( + public rdfType: NamedNode /* plus join types, sort instructions */ + ) {} + + addQuad(q: Quad) { + const pred = q.predicate as NamedNode; + const omittedColumn = pred.equals(rdf.type); + if (!this.columnPreds.contains(pred) && !omittedColumn) { + this.columnPreds = this.columnPreds.push(pred); // this is putting cols in random order + this.rows.forEach((r) => r.push(null)); + } + + const predIndex = omittedColumn ? null : this.columnPreds.indexOf(pred); + let rowIndex = this.subjRowIndices.get(q.subject as NamedNode); + if (rowIndex === undefined) { + const newRow = new Array(1 + this.columnPreds.size).fill(null); + newRow[0] = q.subject; + this.rows.push(newRow); + rowIndex = this.rows.length - 1; + this.subjRowIndices = this.subjRowIndices.set( + q.subject as NamedNode, + rowIndex + ); + } + if (predIndex !== null) { + this.rows[rowIndex][1 + predIndex] = q.object; + } + } + + value(): AlignedTable { + this.rows.sort((a, b) => { + const uriA = (a[0] as NamedNode).value, + uriB = (b[0] as NamedNode).value; + return uriA.localeCompare(uriB); + }); + const headers = this.columnPreds + .map((pred) => { + return { rdfType: this.rdfType, pred: pred }; + }) + .toArray(); + return { columnHeaders: headers, rows: this.rows }; + } +} + +function findTypesNeededForTables(viewConfig?: ViewConfig): UriSet { + const typesToGather: NamedNode[] = []; + if (viewConfig) { + viewConfig.tables.forEach((t: TableDesc) => { + typesToGather.push(t.primary); + }); + } + return Immutable.Set(typesToGather); +} + +function findSubjectsWithTypes(graph: Store, typesToGatherSet: UriSet): UriSet { + const subjectsToGather: NamedNode[] = []; + graph.forEach( + (q: Quad) => { + if (typesToGatherSet.contains(q.object as NamedNode)) { + subjectsToGather.push(q.subject as NamedNode); + } + }, + null, + rdf.type, + null, + null + ); + return Immutable.Set(subjectsToGather); +} + export class Layout { constructor(public viewConfig?: ViewConfig) {} plan(graph: Store): LayoutResult { + const typesToGatherSet = findTypesNeededForTables(this.viewConfig); + + const subjectsToGatherSet = findSubjectsWithTypes(graph, typesToGatherSet); const ungrouped: Quad[] = []; + const vc = this.viewConfig; + const table = + vc && vc.tables.length > 0 + ? new AlignedTableBuilder(vc.tables[0].primary) + : null; graph.forEach( (q: Quad) => { - ungrouped.push(q); + if (!subjectsToGatherSet.contains(q.subject as NamedNode) || !table) { + ungrouped.push(q); + } else { + table.addQuad(q); + } }, null, null, null, null ); - return { sections: [{ statements: ungrouped }] }; + const res: LayoutResult = { sections: [] }; + if (table) { + res.sections.push(table.value()); + } + res.sections.push({ statements: ungrouped }); + return res; } } -// function getType(graph: Store, subj: NamedNode): NamedNode | null { -// let subjType: NamedNode | null = null; - -// graph.forObjects( -// (o: Quad_Object) => { -// subjType = o as NamedNode; -// }, -// subj, -// rdf.type, -// null -// ); -// return subjType; -// } - -// // When there are multiple types, an arbitrary one is used. -// export function groupByRdfType( -// graph: Store -// ): { -// byType: TypeToSubjs; -// typesPresent: NamedNode[]; -// untypedSubjs: NamedNode[]; -// } { -// let byType: TypeToSubjs = Immutable.Map(); -// let untyped: UriSet = Immutable.Set(); // subjs -// const internSubjs = new Map<string, NamedNode>(); -// graph.forEach( -// (q) => { -// if (!Util.isNamedNode(q.subject)) { -// throw new Error("unsupported " + q.subject.value); -// } -// const subj = q.subject as NamedNode; - -// const subjType = getType(graph, subj); - -// if (subjType !== null) { -// // (subj, rdf:type, subjType) in graph -// const oldKeys = Array.from(byType.keys()); -// const oldVal = byType.get(subjType, Immutable.Set<NamedNode>()); -// const newVal = oldVal.add(subj); -// byType = byType.set(subjType, newVal); -// } else { -// untyped = untyped.add(subj); -// } -// }, -// null, -// null, -// null, -// null -// ); - -// const typesPresent = Array.from(byType.keys()); -// typesPresent.sort(); - -// const untypedSubjs = Array.from(untyped.values()); -// untypedSubjs.sort(); -// return { -// byType: byType, -// typesPresent: typesPresent, -// untypedSubjs: untypedSubjs, -// }; -// } - -// export function predsForSubj(graph: Store, typeUri: NamedNode): NamedNode[] { -// const predsSet: Set<NamedNode> = new Set(); -// graph.forEach( -// (q: Quad) => { -// predsSet.add(q.predicate as NamedNode); -// }, -// typeUri, -// null, -// null, -// null -// ); -// const preds = Array.from(predsSet.values()); -// preds.sort(); -// return preds; -// } - // interface ISP { // subj: NamedNode; // pred: NamedNode;