# HG changeset patch # User drewp@bigasterisk.com # Date 1647810723 25200 # Node ID 2e8fa3fec0c8c74fad8f8fd6772cf9d2752cbf52 # Parent 3584f24becf4e8f04da9b861c0022ccd02f1d2d8 support joining subjects into wider rows diff -r 3584f24becf4 -r 2e8fa3fec0c8 src/layout/Layout.test.ts --- a/src/layout/Layout.test.ts Sun Mar 20 14:10:56 2022 -0700 +++ b/src/layout/Layout.test.ts Sun Mar 20 14:12:03 2022 -0700 @@ -74,7 +74,7 @@ const vc = new ViewConfig(); await vc.readFromGraph(` @prefix : . - + <> a :View; :table [ :primaryType :T1 ] .`); const layout = new Layout(vc); lr = layout.plan(await typedStatements()); @@ -199,7 +199,104 @@ { rdfTypes: [EX("T1")], pred: EX("p3") }, { rdfTypes: [EX("T2")], pred: EX("p2") }, ]); - }) + }); + it("joins over an edge", async () => { + const vc = new ViewConfig(); + await vc.readFromGraph(` + @prefix : . + + <> a :View; :table [ + :primaryType :T1; + :link [:predicate :predLink ] ] . + `); + const layout = new Layout(vc); + const lr = layout.plan( + await n3Graph(` + @prefix : . + :g1 { + :a a :T1; :color :red; :predLink :b . + :b a :T2; :size :big . + } + `) + ); + + expect(lr.sections).toHaveLength(1); // no loose statements + const section0 = lr.sections[0] as AlignedTable; + expect(section0.rowHeaders).toEqual([EX("a")]); + expect(section0.columnHeaders).toEqual([ + { rdfTypes: [EX("T1")], pred: EX("color") }, + // :predLink column is hidden + { rdfTypes: [EX("T2")], pred: EX("size") }, + ]); + }); }); + it("joins 3 subjects into one", async () => { + const vc = new ViewConfig(); + await vc.readFromGraph(` + @prefix : . + + <> a :View; :table [ + :primaryType :T1; + :link + [:predicate :pred1Link ], + [:predicate :pred2Link ] + ] . + `); + const layout = new Layout(vc); + const lr = layout.plan( + await n3Graph(` + @prefix : . + :g1 { + :a a :T1; :color :red; :pred1Link :b; :pred2Link :c . + :b a :T2; :size :big . + :c a :T3; :bright 50 . + } + `) + ); + + expect(lr.sections).toHaveLength(1); // no loose statements + const section0 = lr.sections[0] as AlignedTable; + expect(section0.rowHeaders).toEqual([EX("a")]); + expect(section0.columnHeaders).toEqual([ + { rdfTypes: [EX("T1")], pred: EX("color") }, + { rdfTypes: [EX("T2")], pred: EX("size") }, + { rdfTypes: [EX("T3")], pred: EX("bright") }, + ]); + }); + it("links rows to predicate that themselves are linked in", async () => { + const vc = new ViewConfig(); + await vc.readFromGraph(` + @prefix : . + + <> a :View; :table [ + :primaryType :T1; + :link + [:predicate :pred1Link ], + [:predicate :pred2Link ] + ] . + `); + const layout = new Layout(vc); + const lr = layout.plan( + await n3Graph(` + @prefix : . + :g1 { + :a a :T1; :color :red; :pred1Link :b . + :b a :T2; :size :big; :pred2Link :c . + :c a :T3; :bright 50 . + } + `) + ); + console.log(JSON.stringify(lr.sections, null, " ")); + + expect(lr.sections).toHaveLength(1); // no loose statements + const section0 = lr.sections[0] as AlignedTable; + expect(section0.rowHeaders).toEqual([EX("a")]); + expect(section0.columnHeaders).toEqual([ + { rdfTypes: [EX("T1")], pred: EX("color") }, + { rdfTypes: [EX("T2")], pred: EX("size") }, + { rdfTypes: [EX("T3")], pred: EX("bright") }, + ]); + }); + it.skip("makes a table out of ungrouped triples with the same type", async () => {}); }); diff -r 3584f24becf4 -r 2e8fa3fec0c8 src/layout/Layout.ts --- a/src/layout/Layout.ts Sun Mar 20 14:10:56 2022 -0700 +++ b/src/layout/Layout.ts Sun Mar 20 14:12:03 2022 -0700 @@ -2,9 +2,10 @@ 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 { addToValues, multiColumnSort } from "./algorithm"; import { rdf, rdfs } from "./namespaces"; import { uniqueSortedTerms, UriPairMap } from "./rdf_value"; -import { ViewConfig } from "./ViewConfig"; +import { Link, ViewConfig } from "./ViewConfig"; type UriSet = Immutable.Set; @@ -37,61 +38,18 @@ sections: (AlignedTable | FreeStatements)[]; } -function addToValues( - imap: Immutable.Map, - key: K, - newValue: V -): Immutable.Map { - let cur = imap.get(key, undefined); - let ret = imap; - if (cur === undefined) { - cur = []; - ret = imap.set(key, cur); - } - cur.push(newValue); - return ret; -} - -function multiColumnSort( - elems: T[], - makeSortCols: (elem: T, index: number) => Array -): T[] { - const tagged = elems.map((p, i) => { - return { - sort: makeSortCols(p, i), - val: p, - }; - }); - tagged.sort((e1, e2) => { - let index = 0; - for (let k1 of e1.sort) { - const k2 = e2.sort[index]; - if (!Immutable.is(k1, k2)) { - if (typeof k1 === "number") { - if (typeof k2 === "number") { - return k1 - k2; - } else { - throw new Error(`${k1} vs ${k2}`); - } - } else { - return k1.localeCompare(k2 as string); - } - } - index++; - } - return 0; - }); - return tagged.map((e) => e.val); -} - class AlignedTableBuilder { subjSet: UriSet = Immutable.Set(); predSet: UriSet = Immutable.Set(); subjsSeenWithPred: Immutable.Map = Immutable.Map(); typesSeenForSubj: Immutable.Map = Immutable.Map(); - + aliases: Immutable.Map = Immutable.Map(); cell = new UriPairMap(); - constructor(public primaryType: NamedNode, public joinTypes: NamedNode[]) {} + constructor( + public primaryType: NamedNode, + public joinTypes: NamedNode[], + public links: Link[] + ) {} showsType(rdfType: NamedNode): boolean { return ( @@ -100,26 +58,54 @@ ); } + // Considering this.links, call adder on more subjs that should be routed to this table. + detectLinks(graph: Store, adder: (linkedSubj: NamedNode) => void) { + this.links.forEach((link: Link) => { + graph.forEach( + (q: Quad) => { + const linkedSubj = q.object as NamedNode; + adder(linkedSubj); + this.linkSubjs(q.subject as NamedNode, linkedSubj); + }, + null, + link.pred, + null, + null + ); + }); + this.chainLinks(); + } + + // When you make a row for displayedSubj, also include linkedSubj's data + private linkSubjs(displayedSubj: NamedNode, linkedSubj: NamedNode) { + this.aliases = this.aliases.set(linkedSubj, displayedSubj); + } + + // After this.aliases is built; shorten {b: a, c:b} to {b: a, c: a} + private chainLinks() { + for (let alias of this.aliases.keys()) { + let x = this.aliases.get(alias)!; + while (this.aliases.has(x)) { + x = this.aliases.get(x)!; + } + this.aliases = this.aliases.set(alias, x); + } + } + + // Stream in quads that belong to this table (caller has to work out that + // part), then call value(). addQuad(q: Quad) { - const subj = q.subject as NamedNode; + const unaliasedSubj = q.subject as NamedNode; + const aliasedSubj = this.aliases.get(unaliasedSubj, unaliasedSubj); const pred = q.predicate as NamedNode; - this.subjSet = this.subjSet.add(subj); + this.subjSet = this.subjSet.add(aliasedSubj); this.predSet = this.predSet.add(pred); - this.cell.add(subj, pred, q.object); + this.cell.add(aliasedSubj, pred, q.object); if (pred.equals(rdf.type)) { - this.trackTypes(subj, q.object as NamedNode); + this.trackTypes(unaliasedSubj, q.object as NamedNode); } - this.trackSubjs(subj, pred); - } - - private trackTypes(subj: NamedNode, rdfType: NamedNode) { - let cur = this.typesSeenForSubj.get(subj, undefined); - if (cur === undefined) { - cur = []; - this.typesSeenForSubj = this.typesSeenForSubj.set(subj, cur); - } - cur.push(rdfType); + this.trackSubjs(unaliasedSubj, pred); } private trackTypes(unaliasedSubj: NamedNode, rdfType: NamedNode) { @@ -138,10 +124,11 @@ ); } - _displayedPreds(): NamedNode[] { + private displayedPreds(): NamedNode[] { let preds = uniqueSortedTerms(this.predSet); preds = preds.filter((p) => { if (p.equals(rdf.type)) return false; + if (this.links.filter((l) => l.pred.equals(p)).length) return false; return true; }); preds = multiColumnSort(preds, (elem: NamedNode, index: number) => { @@ -172,10 +159,8 @@ return uniqueSortedTerms(types); } - value(): AlignedTable { - const subjs = uniqueSortedTerms(this.subjSet); - const preds = this._displayedPreds(); - const outputGrid: Term[][][] = []; + private gatherOutputGrid(subjs: NamedNode[], preds: NamedNode[]): Term[][][] { + const out: Term[][][] = []; for (let subj of subjs) { const row: Term[][] = []; preds.forEach((pred) => { @@ -183,13 +168,20 @@ const uniq = uniqueSortedTerms(objs); row.push(uniq); }); - outputGrid.push(row); + out.push(row); } + return out; + } - const headers: ColumnHeader[] = preds.map((pred) => { + value(): AlignedTable { + const subjs = uniqueSortedTerms(this.subjSet); + const preds = this.displayedPreds(); + const outputGrid: Term[][][] = this.gatherOutputGrid(subjs, preds); + + const colHeaders: ColumnHeader[] = preds.map((pred) => { return { rdfTypes: this.typesSeenWithPred(pred), pred: pred }; }); - return { columnHeaders: headers, rowHeaders: subjs, rows: outputGrid }; + return { columnHeaders: colHeaders, rowHeaders: subjs, rows: outputGrid }; } } @@ -218,6 +210,13 @@ null, null ); + + tableBuilders.forEach((tb) => { + tb.detectLinks(graph, (linkedSubj: NamedNode) => { + out = addToValues(out, linkedSubj, tb); + }); + }); + return out; } @@ -251,7 +250,7 @@ export class Layout { constructor(public viewConfig?: ViewConfig) {} - _groupAllStatements( + private groupAllStatements( graph: Store, tablesWantingSubject: SubjectTableBuilders, ungrouped: Quad[] @@ -273,26 +272,31 @@ ); } - plan(graph: Store): LayoutResult { - const ungrouped: Quad[] = []; - - const tableBuilders = this.viewConfig - ? this.viewConfig.tables.map( - (t) => new AlignedTableBuilder(t.primary, t.joins) - ) - : []; - - const tablesWantingSubject = subjectsToTablesMap(graph, tableBuilders); - this._groupAllStatements(graph, tablesWantingSubject, ungrouped); - const res: LayoutResult = { sections: [] }; + private generateSections( + tableBuilders: AlignedTableBuilder[], + ungrouped: Quad[] + ): (AlignedTable | FreeStatements)[] { + const sections = []; for (const t of tableBuilders) { if (t.gotStatements()) { - res.sections.push(t.value()); + sections.push(t.value()); } } if (ungrouped.length) { - res.sections.push(freeStatmentsSection(ungrouped)); + sections.push(freeStatmentsSection(ungrouped)); } - return res; + return sections; + } + + plan(graph: Store): LayoutResult { + const vcTables = this.viewConfig ? this.viewConfig.tables : []; + const tableBuilders = vcTables.map( + (t) => new AlignedTableBuilder(t.primary, t.joins, t.links) + ); + + const tablesWantingSubject = subjectsToTablesMap(graph, tableBuilders); + const ungrouped: Quad[] = []; + this.groupAllStatements(graph, tablesWantingSubject, ungrouped); + return { sections: this.generateSections(tableBuilders, ungrouped) }; } } diff -r 3584f24becf4 -r 2e8fa3fec0c8 src/layout/ViewConfig.test.ts --- a/src/layout/ViewConfig.test.ts Sun Mar 20 14:10:56 2022 -0700 +++ b/src/layout/ViewConfig.test.ts Sun Mar 20 14:12:03 2022 -0700 @@ -1,4 +1,5 @@ import { Util } from "n3"; +import { EX } from "./namespaces"; import { ViewConfig } from "./ViewConfig"; describe("ViewModel", () => { @@ -13,7 +14,7 @@ <> a ex:View ; rdfs:label "repos" . <> ex:table demo:table1 . - demo:table1 + demo:table1 ex:primaryType :FilteredNic; ex:joinType :Traffic . `); @@ -22,4 +23,18 @@ expect(vc.tables[0].primary).toEqual(NET("FilteredNic")); expect(vc.tables[0].joins).toEqual([NET("Traffic")]); }); + it("gets pred joins", async () => { + const vc = new ViewConfig(); + await vc.readFromGraph(` + @prefix ex: . + @prefix rdfs: . + @prefix : . + + <> a :View; :table [ + :primaryType :T1; + :link [:predicate :predLink; :rtype :T2 ] ] . + `); + expect(vc.tables[0].primary).toEqual(EX("T1")); + expect(vc.tables[0].links).toEqual([{ pred: EX("predLink") }]); + }); }); diff -r 3584f24becf4 -r 2e8fa3fec0c8 src/layout/ViewConfig.ts --- a/src/layout/ViewConfig.ts Sun Mar 20 14:10:56 2022 -0700 +++ b/src/layout/ViewConfig.ts Sun Mar 20 14:12:03 2022 -0700 @@ -12,10 +12,16 @@ throw new Error("no elems"); } -export interface TableDesc { +export interface Link { + // If you display a subject u1 with a `pred` edge to u2, then treat u2 as an alias of u1. + pred: NamedNode; +} + +interface TableDesc { uri: NamedNode; primary: NamedNode; joins: NamedNode[]; + links: Link[]; } export class ViewConfig { @@ -54,10 +60,19 @@ joins.push(joinType as NamedNode); } 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")), + }); + } + this.tables.push({ uri: table as NamedNode, primary: tableType, joins: joins, + links: links, }); } this.tables.sort(); diff -r 3584f24becf4 -r 2e8fa3fec0c8 src/layout/algorithm.ts --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/layout/algorithm.ts Sun Mar 20 14:12:03 2022 -0700 @@ -0,0 +1,46 @@ +import Immutable from "immutable"; + +export function addToValues( + imap: Immutable.Map, + key: K, + newValue: V): Immutable.Map { + let cur = imap.get(key, undefined); + let ret = imap; + if (cur === undefined) { + cur = []; + ret = imap.set(key, cur); + } + cur.push(newValue); + return ret; +} + +export function multiColumnSort( + elems: T[], + makeSortCols: (elem: T, index: number) => Array): T[] { + const tagged = elems.map((p, i) => { + return { + sort: makeSortCols(p, i), + val: p + }; + }); + tagged.sort((e1, e2) => { + let index = 0; + for (let k1 of e1.sort) { + const k2 = e2.sort[index]; + if (!Immutable.is(k1, k2)) { + if (typeof k1 === "number") { + if (typeof k2 === "number") { + return k1 - k2; + } else { + throw new Error(`${k1} vs ${k2}`); + } + } else { + return k1.localeCompare(k2 as string); + } + } + index++; + } + return 0; + }); + return tagged.map((e) => e.val); +}