Mercurial > code > home > repos > streamed-graph
view src/layout/Layout.ts @ 128:5a1a79f54779
big rewrite
author | drewp@bigasterisk.com |
---|---|
date | Fri, 05 May 2023 21:26:36 -0700 |
parents | 2e8fa3fec0c8 |
children | cf642d395be4 |
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 { addToValues, multiColumnSort } from "./algorithm"; import { rdf, rdfs } from "./namespaces"; import { uniqueSortedTerms, UriPairMap } from "./rdf_value"; import { Link, 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(); aliases: Immutable.Map<NamedNode, NamedNode> = Immutable.Map(); cell = new UriPairMap(); constructor( public primaryType: NamedNode, public joinTypes: NamedNode[], public links: Link[] ) {} showsType(rdfType: NamedNode): boolean { return ( this.primaryType.equals(rdfType) || Immutable.Set<NamedNode>(this.joinTypes).has(rdfType) ); } // 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 unaliasedSubj = q.subject as NamedNode; const aliasedSubj = this.aliases.get(unaliasedSubj, unaliasedSubj); const pred = q.predicate as NamedNode; this.subjSet = this.subjSet.add(aliasedSubj); this.predSet = this.predSet.add(pred); this.cell.add(aliasedSubj, pred, q.object); if (pred.equals(rdf.type)) { this.trackTypes(unaliasedSubj, q.object as NamedNode); } this.trackSubjs(unaliasedSubj, pred); } 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 ); } 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) => { 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); } private gatherOutputGrid(subjs: NamedNode[], preds: NamedNode[]): Term[][][] { const out: 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); }); out.push(row); } return out; } 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: colHeaders, 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 ); tableBuilders.forEach((tb) => { tb.detectLinks(graph, (linkedSubj: NamedNode) => { out = addToValues(out, linkedSubj, tb); }); }); return out; } function freeStatmentsSection(viewConfig: ViewConfig, stmts: Quad[]): FreeStatements { const subjs: NamedNode[] = []; stmts.forEach((q) => { if (viewConfig.freeStatementsHidePred.has(q.predicate)) { return; } 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) && !viewConfig.freeStatementsHidePred.has(q.predicate)) { 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) {} private 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 ); } private generateSections( viewConfig: ViewConfig, tableBuilders: AlignedTableBuilder[], ungrouped: Quad[] ): (AlignedTable | FreeStatements)[] { const sections = []; for (const t of tableBuilders) { if (t.gotStatements()) { sections.push(t.value()); } } if (ungrouped.length) { sections.push(freeStatmentsSection(viewConfig, ungrouped)); } return sections; } plan(graph: Store): LayoutResult { const vcTables = 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(this.viewConfig, tableBuilders, ungrouped) }; } }