Mercurial > code > home > repos > streamed-graph
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 } |