Mercurial > code > home > repos > streamed-graph
diff src/layout/Layout.ts @ 106:2468f2227d22
make src/layout/ and src/render/ separation
author | drewp@bigasterisk.com |
---|---|
date | Sun, 13 Mar 2022 22:00:30 -0700 |
parents | src/Layout.ts@1aea03d306af |
children | 5e6840229a05 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/layout/Layout.ts Sun Mar 13 22:00:30 2022 -0700 @@ -0,0 +1,200 @@ +// 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 } from "./namespaces"; +import { TableDesc, ViewConfig } from "./ViewConfig"; + +type UriSet = Immutable.Set<NamedNode>; +export type TypeToSubjs = Immutable.Map<NamedNode, UriSet>; + +// https://github.com/rdfjs/N3.js/issues/265 +(NamedNode.prototype as any).hashCode = () => 0; + +interface ColumnHeader { + rdfType: NamedNode; + pred: NamedNode; +} +export interface AlignedTable { + columnHeaders: ColumnHeader[]; + rows: (Term | null)[][]; // each row is 1 wider than columnHeaders since the 1st element is the subject for that row +} +interface FreeStatements { + statements: Quad[]; +} +export interface LayoutResult { + sections: (AlignedTable | FreeStatements)[]; +} + +class AlignedTableBuilder { + columnPreds = Immutable.List<NamedNode>(); + subjRowIndices = Immutable.Map<NamedNode, number>(); + rows: (Term | null)[][] = []; + constructor( + public rdfType: NamedNode /* plus join types, sort instructions */ + ) {} + + addQuad(q: Quad) { + const pred = q.predicate as NamedNode; + const omittedColumn = pred.equals(rdf.type); + if (!this.columnPreds.contains(pred) && !omittedColumn) { + this.columnPreds = this.columnPreds.push(pred); // this is putting cols in random order + this.rows.forEach((r) => r.push(null)); + } + + const predIndex = omittedColumn ? null : this.columnPreds.indexOf(pred); + let rowIndex = this.subjRowIndices.get(q.subject as NamedNode); + if (rowIndex === undefined) { + const newRow = new Array(1 + this.columnPreds.size).fill(null); + newRow[0] = q.subject; + this.rows.push(newRow); + rowIndex = this.rows.length - 1; + this.subjRowIndices = this.subjRowIndices.set( + q.subject as NamedNode, + rowIndex + ); + } + if (predIndex !== null) { + this.rows[rowIndex][1 + predIndex] = q.object; + } + } + + value(): AlignedTable { + this.rows.sort((a, b) => { + const uriA = (a[0] as NamedNode).value, + uriB = (b[0] as NamedNode).value; + return uriA.localeCompare(uriB); + }); + const headers = this.columnPreds + .map((pred) => { + return { rdfType: this.rdfType, pred: pred }; + }) + .toArray(); + return { columnHeaders: headers, rows: this.rows }; + } +} + +function findTypesNeededForTables(viewConfig?: ViewConfig): UriSet { + const typesToGather: NamedNode[] = []; + if (viewConfig) { + viewConfig.tables.forEach((t: TableDesc) => { + typesToGather.push(t.primary); + }); + } + return Immutable.Set(typesToGather); +} + +function findSubjectsWithTypes(graph: Store, typesToGatherSet: UriSet): UriSet { + const subjectsToGather: NamedNode[] = []; + graph.forEach( + (q: Quad) => { + if (typesToGatherSet.contains(q.object as NamedNode)) { + subjectsToGather.push(q.subject as NamedNode); + } + }, + null, + rdf.type, + null, + null + ); + return Immutable.Set(subjectsToGather); +} + +export class Layout { + constructor(public viewConfig?: ViewConfig) {} + plan(graph: Store): LayoutResult { + const typesToGatherSet = findTypesNeededForTables(this.viewConfig); + + const subjectsToGatherSet = findSubjectsWithTypes(graph, typesToGatherSet); + const ungrouped: Quad[] = []; + const vc = this.viewConfig; + const table = + vc && vc.tables.length > 0 + ? new AlignedTableBuilder(vc.tables[0].primary) + : null; + + graph.forEach( + (q: Quad) => { + if (!subjectsToGatherSet.contains(q.subject as NamedNode) || !table) { + ungrouped.push(q); + } else { + table.addQuad(q); + } + }, + null, + null, + null, + null + ); + const res: LayoutResult = { sections: [] }; + if (table) { + res.sections.push(table.value()); + } + res.sections.push({ statements: ungrouped }); + return res; + } +} + +// interface ISP { +// subj: NamedNode; +// pred: NamedNode; +// } +// const SP = Immutable.Record<ISP>({ +// subj: new NamedNode(""), +// pred: new NamedNode(""), +// }); + +// // One table of rows with a common rdf:type. +// export class MultiSubjsTypeBlockLayout { +// subjs: NamedNode[]; +// preds: NamedNode[]; +// graphCells: Immutable.Map<ISP, Immutable.Set<Term>>; +// constructor(graph: Store, byType: TypeToSubjs, table: TableDesc) { +// const subjSet = byType.get(table.primary); +// this.subjs = subjSet ? Array.from(subjSet) : []; +// this.subjs.sort(); + +// let preds = Immutable.Set<NamedNode>(); + +// this.graphCells = Immutable.Map<ISP, Immutable.Set<Term>>().withMutations( +// (mutGraphCells) => { +// this.subjs.forEach((subj: NamedNode) => { +// graph.forEach( +// (q: Quad) => { +// if (!Util.isNamedNode(q.predicate)) { +// throw new Error(); +// } + +// const pred = q.predicate as NamedNode; +// if (pred.equals(rdf.type)) { +// // the whole block is labeled with the type +// return; +// } +// preds = preds.add(pred); +// const cellKey = this.makeCellKey(subj, pred); +// mutGraphCells.set( +// cellKey, +// mutGraphCells.get(cellKey, Immutable.Set<Term>()).add(q.object) +// ); +// }, +// subj, +// null, +// null, +// null +// ); +// }); +// } +// ); +// this.preds = Array.from(preds); +// this.preds.splice(this.preds.indexOf(rdf.type), 1); +// // also pull out label, which should be used on 1st column +// this.preds.sort(); +// } + +// makeCellKey(subj: NamedNode, pred: NamedNode): ISP { +// return SP({ +// subj: subj, +// pred: pred, +// }); +// } +// }