Mercurial > code > home > repos > streamed-graph
view src/layout/Layout.ts @ 121:3584f24becf4
cleanup
author | drewp@bigasterisk.com |
---|---|
date | Sun, 20 Mar 2022 14:10:56 -0700 |
parents | a7519d92dbc6 |
children | 2e8fa3fec0c8 |
line wrap: on
line source
// 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 { NamedNode, Quad, Store, Term } from "n3"; import { rdf, rdfs } from "./namespaces"; import { uniqueSortedTerms, UriPairMap } from "./rdf_value"; import { ViewConfig } from "./ViewConfig"; type UriSet = Immutable.Set<NamedNode>; interface ColumnHeader { rdfTypes: NamedNode[]; pred: NamedNode; } export interface AlignedTable { columnHeaders: ColumnHeader[]; rowHeaders: NamedNode[]; rows: Term[][][]; } export interface PredRow { pred: NamedNode; objs: Term[]; } export interface SubjRow { subj: NamedNode; predRows: PredRow[]; } export interface FreeStatements { subjRows: SubjRow[]; } export interface LayoutResult { sections: (AlignedTable | FreeStatements)[]; } function addToValues<K, V>( imap: Immutable.Map<K, V[]>, key: K, newValue: V ): Immutable.Map<K, V[]> { 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<T>( elems: T[], makeSortCols: (elem: T, index: number) => Array<number | string> ): 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<NamedNode, NamedNode[]> = Immutable.Map(); typesSeenForSubj: Immutable.Map<NamedNode, NamedNode[]> = Immutable.Map(); cell = new UriPairMap(); constructor(public primaryType: NamedNode, public joinTypes: NamedNode[]) {} showsType(rdfType: NamedNode): boolean { return ( this.primaryType.equals(rdfType) || Immutable.Set<NamedNode>(this.joinTypes).has(rdfType) ); } addQuad(q: Quad) { const subj = q.subject as NamedNode; const pred = q.predicate as NamedNode; this.subjSet = this.subjSet.add(subj); this.predSet = this.predSet.add(pred); this.cell.add(subj, pred, q.object); if (pred.equals(rdf.type)) { this.trackTypes(subj, q.object as NamedNode); } this.trackSubjs(subj, pred); } private trackTypes(subj: NamedNode<string>, rdfType: NamedNode) { let cur = this.typesSeenForSubj.get(subj, undefined); if (cur === undefined) { cur = []; this.typesSeenForSubj = this.typesSeenForSubj.set(subj, cur); } cur.push(rdfType); } private trackTypes(unaliasedSubj: NamedNode<string>, rdfType: NamedNode) { this.typesSeenForSubj = addToValues( this.typesSeenForSubj, unaliasedSubj, rdfType ); } private trackSubjs(unaliasedSubj: NamedNode, pred: NamedNode<string>) { this.subjsSeenWithPred = addToValues( this.subjsSeenWithPred, pred, unaliasedSubj ); } _displayedPreds(): NamedNode[] { let preds = uniqueSortedTerms(this.predSet); preds = preds.filter((p) => { if (p.equals(rdf.type)) return false; return true; }); preds = multiColumnSort(preds, (elem: NamedNode, index: number) => { const types = this.typesSeenWithPred(elem); return [ elem.equals(rdfs.label) ? 0 : 1, types.length == 1 ? 0 : 1, types[0].equals(this.primaryType) ? 0 : 1, types[0].id, index, ]; }); return preds; } gotStatements(): boolean { return !this.subjSet.isEmpty(); } private typesSeenWithPred(pred: NamedNode): NamedNode[] { const subs = this.subjsSeenWithPred.get(pred, []); const types: NamedNode[] = []; subs.forEach((s) => { this.typesSeenForSubj.get(s, []).forEach((t) => { types.push(t); }); }); return uniqueSortedTerms(types); } value(): AlignedTable { const subjs = uniqueSortedTerms(this.subjSet); const preds = this._displayedPreds(); const outputGrid: Term[][][] = []; for (let subj of subjs) { const row: Term[][] = []; preds.forEach((pred) => { const objs = this.cell.get(subj, pred); const uniq = uniqueSortedTerms(objs); row.push(uniq); }); outputGrid.push(row); } const headers: ColumnHeader[] = preds.map((pred) => { return { rdfTypes: this.typesSeenWithPred(pred), pred: pred }; }); return { columnHeaders: headers, rowHeaders: subjs, rows: outputGrid }; } } type SubjectTableBuilders = Immutable.Map< NamedNode<string>, AlignedTableBuilder[] >; function subjectsToTablesMap( graph: Store, tableBuilders: AlignedTableBuilder[] ): SubjectTableBuilders { let out: SubjectTableBuilders = Immutable.Map(); graph.forEach( (q: Quad) => { const s = q.subject as NamedNode; const rdfType = q.object as NamedNode; tableBuilders.forEach((tb) => { if (tb.showsType(rdfType)) { out = addToValues(out, s, tb); } }); }, null, rdf.type, null, null ); return out; } function freeStatmentsSection(stmts: Quad[]): FreeStatements { const subjs: NamedNode[] = []; stmts.forEach((q) => { subjs.push(q.subject as NamedNode); }); return { subjRows: uniqueSortedTerms(subjs).map((subj) => { const preds: NamedNode[] = []; let po = Immutable.Map<NamedNode, Term[]>(); stmts.forEach((q) => { if (q.subject.equals(subj)) { const p = q.predicate as NamedNode; preds.push(p); po = addToValues(po, p, q.object as NamedNode); } }); const rows: PredRow[] = []; uniqueSortedTerms(preds).forEach((p) => { rows.push({ pred: p, objs: uniqueSortedTerms(po.get(p, [])) }); }); return { subj: subj, predRows: rows }; }), }; } // The description of how this page should look: sections, tables, etc. export class Layout { constructor(public viewConfig?: ViewConfig) {} _groupAllStatements( graph: Store, tablesWantingSubject: SubjectTableBuilders, ungrouped: Quad[] ) { graph.forEach( (q: Quad) => { const tables = tablesWantingSubject.get(q.subject as NamedNode); if (tables && tables.length) { tables.forEach((t: AlignedTableBuilder) => t.addQuad(q)); } else { ungrouped.push(q); } }, null, null, null, null ); } 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: [] }; for (const t of tableBuilders) { if (t.gotStatements()) { res.sections.push(t.value()); } } if (ungrouped.length) { res.sections.push(freeStatmentsSection(ungrouped)); } return res; } }