# 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);
+}