Mercurial > code > home > repos > streamed-graph
view src/layout/Layout.ts @ 119:8715633f5213
fancier column sorting to bring preds from the same type together
author | drewp@bigasterisk.com |
---|---|
date | Sun, 20 Mar 2022 01:17:35 -0700 |
parents | c2923b20bf5c |
children | a7519d92dbc6 |
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)[]; } 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 trackSubjs(subj: NamedNode, pred: NamedNode<string>) { let cur = this.subjsSeenWithPred.get(pred, undefined); if (cur === undefined) { cur = []; this.subjsSeenWithPred = this.subjsSeenWithPred.set(pred, cur); } cur.push(subj); } _displayedPreds(): NamedNode[] { let preds = uniqueSortedTerms(this.predSet); preds = preds.filter((p) => { return !p.equals(rdf.type); }); const tagged = preds.map((p, i) => { const types = this.typesSeenWithPred(p); return { sort: [ p.equals(rdfs.label) ? 0 : 1, types.length == 1 ? 0 : 1, types[0].equals(this.primaryType) ? 0 : 1, types[0], i, ], val: p, }; }); tagged.sort((a, b) => { let ib = 0; for (let k1 of a.sort) { const k2 = b.sort[ib]; 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 as NamedNode).id.localeCompare((k2 as NamedNode).id); } } ib++; } return 0; }); preds = tagged.map((e) => e.val); 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)) { let cur = out.get(s, undefined); if (cur === undefined) { cur = []; out = out.set(s, cur); } cur.push(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 = po.set(p, po.get(p, [])); po.get(p)?.push(q.object as Term); } }); 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; } }