comparison src/layout/Layout.ts @ 122:2e8fa3fec0c8

support joining subjects into wider rows
author drewp@bigasterisk.com
date Sun, 20 Mar 2022 14:12:03 -0700
parents 3584f24becf4
children 5a1a79f54779
comparison
equal deleted inserted replaced
121:3584f24becf4 122:2e8fa3fec0c8
1 // Organize graph data into tables (column orders, etc) for the view layer. 1 // Organize graph data into tables (column orders, etc) for the view layer.
2 2
3 import Immutable from "immutable"; // mostly using this for the builtin equals() testing, since NamedNode(x)!=NamedNode(x) 3 import Immutable from "immutable"; // mostly using this for the builtin equals() testing, since NamedNode(x)!=NamedNode(x)
4 import { NamedNode, Quad, Store, Term } from "n3"; 4 import { NamedNode, Quad, Store, Term } from "n3";
5 import { addToValues, multiColumnSort } from "./algorithm";
5 import { rdf, rdfs } from "./namespaces"; 6 import { rdf, rdfs } from "./namespaces";
6 import { uniqueSortedTerms, UriPairMap } from "./rdf_value"; 7 import { uniqueSortedTerms, UriPairMap } from "./rdf_value";
7 import { ViewConfig } from "./ViewConfig"; 8 import { Link, ViewConfig } from "./ViewConfig";
8 9
9 type UriSet = Immutable.Set<NamedNode>; 10 type UriSet = Immutable.Set<NamedNode>;
10 11
11 interface ColumnHeader { 12 interface ColumnHeader {
12 rdfTypes: NamedNode[]; 13 rdfTypes: NamedNode[];
33 subjRows: SubjRow[]; 34 subjRows: SubjRow[];
34 } 35 }
35 36
36 export interface LayoutResult { 37 export interface LayoutResult {
37 sections: (AlignedTable | FreeStatements)[]; 38 sections: (AlignedTable | FreeStatements)[];
38 }
39
40 function addToValues<K, V>(
41 imap: Immutable.Map<K, V[]>,
42 key: K,
43 newValue: V
44 ): Immutable.Map<K, V[]> {
45 let cur = imap.get(key, undefined);
46 let ret = imap;
47 if (cur === undefined) {
48 cur = [];
49 ret = imap.set(key, cur);
50 }
51 cur.push(newValue);
52 return ret;
53 }
54
55 function multiColumnSort<T>(
56 elems: T[],
57 makeSortCols: (elem: T, index: number) => Array<number | string>
58 ): T[] {
59 const tagged = elems.map((p, i) => {
60 return {
61 sort: makeSortCols(p, i),
62 val: p,
63 };
64 });
65 tagged.sort((e1, e2) => {
66 let index = 0;
67 for (let k1 of e1.sort) {
68 const k2 = e2.sort[index];
69 if (!Immutable.is(k1, k2)) {
70 if (typeof k1 === "number") {
71 if (typeof k2 === "number") {
72 return k1 - k2;
73 } else {
74 throw new Error(`${k1} vs ${k2}`);
75 }
76 } else {
77 return k1.localeCompare(k2 as string);
78 }
79 }
80 index++;
81 }
82 return 0;
83 });
84 return tagged.map((e) => e.val);
85 } 39 }
86 40
87 class AlignedTableBuilder { 41 class AlignedTableBuilder {
88 subjSet: UriSet = Immutable.Set(); 42 subjSet: UriSet = Immutable.Set();
89 predSet: UriSet = Immutable.Set(); 43 predSet: UriSet = Immutable.Set();
90 subjsSeenWithPred: Immutable.Map<NamedNode, NamedNode[]> = Immutable.Map(); 44 subjsSeenWithPred: Immutable.Map<NamedNode, NamedNode[]> = Immutable.Map();
91 typesSeenForSubj: Immutable.Map<NamedNode, NamedNode[]> = Immutable.Map(); 45 typesSeenForSubj: Immutable.Map<NamedNode, NamedNode[]> = Immutable.Map();
92 46 aliases: Immutable.Map<NamedNode, NamedNode> = Immutable.Map();
93 cell = new UriPairMap(); 47 cell = new UriPairMap();
94 constructor(public primaryType: NamedNode, public joinTypes: NamedNode[]) {} 48 constructor(
49 public primaryType: NamedNode,
50 public joinTypes: NamedNode[],
51 public links: Link[]
52 ) {}
95 53
96 showsType(rdfType: NamedNode): boolean { 54 showsType(rdfType: NamedNode): boolean {
97 return ( 55 return (
98 this.primaryType.equals(rdfType) || 56 this.primaryType.equals(rdfType) ||
99 Immutable.Set<NamedNode>(this.joinTypes).has(rdfType) 57 Immutable.Set<NamedNode>(this.joinTypes).has(rdfType)
100 ); 58 );
101 } 59 }
102 60
61 // Considering this.links, call adder on more subjs that should be routed to this table.
62 detectLinks(graph: Store, adder: (linkedSubj: NamedNode) => void) {
63 this.links.forEach((link: Link) => {
64 graph.forEach(
65 (q: Quad) => {
66 const linkedSubj = q.object as NamedNode;
67 adder(linkedSubj);
68 this.linkSubjs(q.subject as NamedNode, linkedSubj);
69 },
70 null,
71 link.pred,
72 null,
73 null
74 );
75 });
76 this.chainLinks();
77 }
78
79 // When you make a row for displayedSubj, also include linkedSubj's data
80 private linkSubjs(displayedSubj: NamedNode, linkedSubj: NamedNode) {
81 this.aliases = this.aliases.set(linkedSubj, displayedSubj);
82 }
83
84 // After this.aliases is built; shorten {b: a, c:b} to {b: a, c: a}
85 private chainLinks() {
86 for (let alias of this.aliases.keys()) {
87 let x = this.aliases.get(alias)!;
88 while (this.aliases.has(x)) {
89 x = this.aliases.get(x)!;
90 }
91 this.aliases = this.aliases.set(alias, x);
92 }
93 }
94
95 // Stream in quads that belong to this table (caller has to work out that
96 // part), then call value().
103 addQuad(q: Quad) { 97 addQuad(q: Quad) {
104 const subj = q.subject as NamedNode; 98 const unaliasedSubj = q.subject as NamedNode;
99 const aliasedSubj = this.aliases.get(unaliasedSubj, unaliasedSubj);
105 const pred = q.predicate as NamedNode; 100 const pred = q.predicate as NamedNode;
106 this.subjSet = this.subjSet.add(subj); 101 this.subjSet = this.subjSet.add(aliasedSubj);
107 this.predSet = this.predSet.add(pred); 102 this.predSet = this.predSet.add(pred);
108 this.cell.add(subj, pred, q.object); 103 this.cell.add(aliasedSubj, pred, q.object);
109 104
110 if (pred.equals(rdf.type)) { 105 if (pred.equals(rdf.type)) {
111 this.trackTypes(subj, q.object as NamedNode); 106 this.trackTypes(unaliasedSubj, q.object as NamedNode);
112 } 107 }
113 this.trackSubjs(subj, pred); 108 this.trackSubjs(unaliasedSubj, pred);
114 }
115
116 private trackTypes(subj: NamedNode<string>, rdfType: NamedNode) {
117 let cur = this.typesSeenForSubj.get(subj, undefined);
118 if (cur === undefined) {
119 cur = [];
120 this.typesSeenForSubj = this.typesSeenForSubj.set(subj, cur);
121 }
122 cur.push(rdfType);
123 } 109 }
124 110
125 private trackTypes(unaliasedSubj: NamedNode<string>, rdfType: NamedNode) { 111 private trackTypes(unaliasedSubj: NamedNode<string>, rdfType: NamedNode) {
126 this.typesSeenForSubj = addToValues( 112 this.typesSeenForSubj = addToValues(
127 this.typesSeenForSubj, 113 this.typesSeenForSubj,
136 pred, 122 pred,
137 unaliasedSubj 123 unaliasedSubj
138 ); 124 );
139 } 125 }
140 126
141 _displayedPreds(): NamedNode[] { 127 private displayedPreds(): NamedNode[] {
142 let preds = uniqueSortedTerms(this.predSet); 128 let preds = uniqueSortedTerms(this.predSet);
143 preds = preds.filter((p) => { 129 preds = preds.filter((p) => {
144 if (p.equals(rdf.type)) return false; 130 if (p.equals(rdf.type)) return false;
131 if (this.links.filter((l) => l.pred.equals(p)).length) return false;
145 return true; 132 return true;
146 }); 133 });
147 preds = multiColumnSort(preds, (elem: NamedNode, index: number) => { 134 preds = multiColumnSort(preds, (elem: NamedNode, index: number) => {
148 const types = this.typesSeenWithPred(elem); 135 const types = this.typesSeenWithPred(elem);
149 return [ 136 return [
170 }); 157 });
171 }); 158 });
172 return uniqueSortedTerms(types); 159 return uniqueSortedTerms(types);
173 } 160 }
174 161
175 value(): AlignedTable { 162 private gatherOutputGrid(subjs: NamedNode[], preds: NamedNode[]): Term[][][] {
176 const subjs = uniqueSortedTerms(this.subjSet); 163 const out: Term[][][] = [];
177 const preds = this._displayedPreds();
178 const outputGrid: Term[][][] = [];
179 for (let subj of subjs) { 164 for (let subj of subjs) {
180 const row: Term[][] = []; 165 const row: Term[][] = [];
181 preds.forEach((pred) => { 166 preds.forEach((pred) => {
182 const objs = this.cell.get(subj, pred); 167 const objs = this.cell.get(subj, pred);
183 const uniq = uniqueSortedTerms(objs); 168 const uniq = uniqueSortedTerms(objs);
184 row.push(uniq); 169 row.push(uniq);
185 }); 170 });
186 outputGrid.push(row); 171 out.push(row);
187 } 172 }
188 173 return out;
189 const headers: ColumnHeader[] = preds.map((pred) => { 174 }
175
176 value(): AlignedTable {
177 const subjs = uniqueSortedTerms(this.subjSet);
178 const preds = this.displayedPreds();
179 const outputGrid: Term[][][] = this.gatherOutputGrid(subjs, preds);
180
181 const colHeaders: ColumnHeader[] = preds.map((pred) => {
190 return { rdfTypes: this.typesSeenWithPred(pred), pred: pred }; 182 return { rdfTypes: this.typesSeenWithPred(pred), pred: pred };
191 }); 183 });
192 return { columnHeaders: headers, rowHeaders: subjs, rows: outputGrid }; 184 return { columnHeaders: colHeaders, rowHeaders: subjs, rows: outputGrid };
193 } 185 }
194 } 186 }
195 187
196 type SubjectTableBuilders = Immutable.Map< 188 type SubjectTableBuilders = Immutable.Map<
197 NamedNode<string>, 189 NamedNode<string>,
216 null, 208 null,
217 rdf.type, 209 rdf.type,
218 null, 210 null,
219 null 211 null
220 ); 212 );
213
214 tableBuilders.forEach((tb) => {
215 tb.detectLinks(graph, (linkedSubj: NamedNode) => {
216 out = addToValues(out, linkedSubj, tb);
217 });
218 });
219
221 return out; 220 return out;
222 } 221 }
223 222
224 function freeStatmentsSection(stmts: Quad[]): FreeStatements { 223 function freeStatmentsSection(stmts: Quad[]): FreeStatements {
225 const subjs: NamedNode[] = []; 224 const subjs: NamedNode[] = [];
249 248
250 // The description of how this page should look: sections, tables, etc. 249 // The description of how this page should look: sections, tables, etc.
251 export class Layout { 250 export class Layout {
252 constructor(public viewConfig?: ViewConfig) {} 251 constructor(public viewConfig?: ViewConfig) {}
253 252
254 _groupAllStatements( 253 private groupAllStatements(
255 graph: Store, 254 graph: Store,
256 tablesWantingSubject: SubjectTableBuilders, 255 tablesWantingSubject: SubjectTableBuilders,
257 ungrouped: Quad[] 256 ungrouped: Quad[]
258 ) { 257 ) {
259 graph.forEach( 258 graph.forEach(
271 null, 270 null,
272 null 271 null
273 ); 272 );
274 } 273 }
275 274
276 plan(graph: Store): LayoutResult { 275 private generateSections(
277 const ungrouped: Quad[] = []; 276 tableBuilders: AlignedTableBuilder[],
278 277 ungrouped: Quad[]
279 const tableBuilders = this.viewConfig 278 ): (AlignedTable | FreeStatements)[] {
280 ? this.viewConfig.tables.map( 279 const sections = [];
281 (t) => new AlignedTableBuilder(t.primary, t.joins)
282 )
283 : [];
284
285 const tablesWantingSubject = subjectsToTablesMap(graph, tableBuilders);
286 this._groupAllStatements(graph, tablesWantingSubject, ungrouped);
287 const res: LayoutResult = { sections: [] };
288 for (const t of tableBuilders) { 280 for (const t of tableBuilders) {
289 if (t.gotStatements()) { 281 if (t.gotStatements()) {
290 res.sections.push(t.value()); 282 sections.push(t.value());
291 } 283 }
292 } 284 }
293 if (ungrouped.length) { 285 if (ungrouped.length) {
294 res.sections.push(freeStatmentsSection(ungrouped)); 286 sections.push(freeStatmentsSection(ungrouped));
295 } 287 }
296 return res; 288 return sections;
297 } 289 }
298 } 290
291 plan(graph: Store): LayoutResult {
292 const vcTables = this.viewConfig ? this.viewConfig.tables : [];
293 const tableBuilders = vcTables.map(
294 (t) => new AlignedTableBuilder(t.primary, t.joins, t.links)
295 );
296
297 const tablesWantingSubject = subjectsToTablesMap(graph, tableBuilders);
298 const ungrouped: Quad[] = [];
299 this.groupAllStatements(graph, tablesWantingSubject, ungrouped);
300 return { sections: this.generateSections(tableBuilders, ungrouped) };
301 }
302 }