Mercurial > code > home > repos > streamed-graph
changeset 118:c2923b20bf5c
support multi labels per column
author | drewp@bigasterisk.com |
---|---|
date | Sun, 20 Mar 2022 00:54:19 -0700 |
parents | 069c1f70afa5 |
children | 8715633f5213 |
files | src/layout/Layout.test.ts src/layout/Layout.ts src/render/GraphView.ts |
diffstat | 3 files changed, 151 insertions(+), 46 deletions(-) [+] |
line wrap: on
line diff
--- a/src/layout/Layout.test.ts Sat Mar 19 17:45:01 2022 -0700 +++ b/src/layout/Layout.test.ts Sun Mar 20 00:54:19 2022 -0700 @@ -84,14 +84,14 @@ expect(lr.sections).toHaveLength(2); }); it("puts the right type in the table", async () => { - const sec0 = lr.sections[0] as AlignedTable; - expect(sec0.columnHeaders).toEqual([ - { rdfType: EX("T1"), pred: EX("color") }, - { rdfType: EX("T1"), pred: EX("size") }, + const section0 = lr.sections[0] as AlignedTable; + expect(section0.columnHeaders).toEqual([ + { rdfTypes: [EX("T1")], pred: EX("color") }, + { rdfTypes: [EX("T1"), EX("T2")], pred: EX("size") }, // and doesn't include rdf:type as a column header here ]); - expect(sec0.rowHeaders).toEqual([EX("a"), EX("b"), EX("c"), EX("e")]); - expect(sec0.rows).toEqual([ + expect(section0.rowHeaders).toEqual([EX("a"), EX("b"), EX("c"), EX("e")]); + expect(section0.rows).toEqual([ [[EX("red")], []], [[EX("blue")], []], [[], []], @@ -123,26 +123,60 @@ const layout = new Layout(vc); const lr = layout.plan(await typedStatements()); expect(lr.sections).toHaveLength(2); - expect(lr.sections[0]).toEqual({ - columnHeaders: [ - { rdfType: EX("T1"), pred: EX("color") }, - { rdfType: EX("T1"), pred: EX("size") }, - ], - rowHeaders: [EX("a"), EX("b"), EX("c"), EX("e")], - rows: [ + const section0 = lr.sections[0] as AlignedTable; + expect(section0.columnHeaders).toEqual([ + { rdfTypes: [EX("T1")], pred: EX("color") }, + { rdfTypes: [EX("T1"), EX("T2")], pred: EX("size") }, + ]); + expect(section0.rowHeaders).toEqual([EX("a"), EX("b"), EX("c"), EX("e")]); + expect(section0.rows).toEqual([ + [[EX("red")], []], + [[EX("blue")], []], + [[], []], + [[], [EX("small")]], + ]); + const section1 = lr.sections[1] as AlignedTable; + expect(section1.columnHeaders).toEqual([ + { rdfTypes: [EX("T1"), EX("T2")], pred: EX("size") }, + ]); + expect(section1.rowHeaders).toEqual([EX("d"), EX("e")]); + expect(section1.rows).toEqual([ + [[EX("big")]], // + [[EX("small")]], + ]); + }); + describe("joins multiple types into one table", () => { + it("can simply merge types", async () => { + const vc = new ViewConfig(); + await vc.readFromGraph(` + @prefix ex: <http://example.com/> . + @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . + @prefix : <http://example.com/> . + + <> a :View; :table [ :primaryType :T1; :joinType :T2 ] . + `); + vc.graph.forEach((q) => console.log("vc", q), null, null, null, null); + const layout = new Layout(vc); + const lr = layout.plan( + await n3Graph(` + @prefix : <http://example.com/> . + :g1 { + :a a :T1 ; :color :red . + :b a :T2 ; :size :big . + } + `) + ); + expect(lr.sections).toHaveLength(1); + const section0 = lr.sections[0] as AlignedTable; + expect(section0.columnHeaders).toEqual([ + { rdfTypes: [EX("T1")], pred: EX("color") }, + { rdfTypes: [EX("T2")], pred: EX("size") }, + ]); + expect(section0.rowHeaders).toEqual([EX("a"), EX("b")]); + expect(section0.rows).toEqual([ [[EX("red")], []], - [[EX("blue")], []], - [[], []], - [[], [EX("small")]], - ], - }); - expect(lr.sections[1]).toEqual({ - columnHeaders: [{ rdfType: EX("T2"), pred: EX("size") }], - rowHeaders: [EX("d"), EX("e")], - rows: [ - [[EX("big")]], // - [[EX("small")]], - ], + [[], [EX("big")]], + ]); }); }); it.skip("makes a table out of ungrouped triples with the same type", async () => {});
--- a/src/layout/Layout.ts Sat Mar 19 17:45:01 2022 -0700 +++ b/src/layout/Layout.ts Sun Mar 20 00:54:19 2022 -0700 @@ -9,7 +9,7 @@ type UriSet = Immutable.Set<NamedNode>; interface ColumnHeader { - rdfType: NamedNode; + rdfTypes: NamedNode[]; pred: NamedNode; } @@ -40,10 +40,18 @@ 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 rdfType: NamedNode /* plus join types, sort instructions */ - ) {} + 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; @@ -51,6 +59,29 @@ 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[] { @@ -70,9 +101,11 @@ preds = tagged.map((e) => e.val); return preds; } + gotStatements(): boolean { return !this.subjSet.isEmpty(); } + value(): AlignedTable { const subjs = uniqueSortedTerms(this.subjSet); const preds = this._displayedPreds(); @@ -87,8 +120,16 @@ outputGrid.push(row); } - const headers = preds.map((pred) => { - return { rdfType: this.rdfType, pred: pred }; + const headers: ColumnHeader[] = preds.map((pred) => { + const subs = this.subjsSeenWithPred.get(pred, []); + const types: NamedNode[] = []; + subs.forEach((s) => { + this.typesSeenForSubj.get(s, []).forEach((t) => { + types.push(t); + }); + }); + + return { rdfTypes: uniqueSortedTerms(types), pred: pred }; }); return { columnHeaders: headers, rowHeaders: subjs, rows: outputGrid }; } @@ -107,9 +148,10 @@ graph.forEach( (q: Quad) => { const s = q.subject as NamedNode; + const rdfType = q.object as NamedNode; tableBuilders.forEach((tb) => { - if (tb.rdfType.equals(q.object)) { - let cur = out.get(s); + if (tb.showsType(rdfType)) { + let cur = out.get(s, undefined); if (cur === undefined) { cur = []; out = out.set(s, cur); @@ -165,6 +207,7 @@ graph.forEach( (q: Quad) => { const tables = tablesWantingSubject.get(q.subject as NamedNode); + if (tables && tables.length) { tables.forEach((t: AlignedTableBuilder) => t.addQuad(q)); } else { @@ -182,7 +225,9 @@ const ungrouped: Quad[] = []; const tableBuilders = this.viewConfig - ? this.viewConfig.tables.map((t) => new AlignedTableBuilder(t.primary)) + ? this.viewConfig.tables.map( + (t) => new AlignedTableBuilder(t.primary, t.joins) + ) : []; const tablesWantingSubject = subjectsToTablesMap(graph, tableBuilders);
--- a/src/render/GraphView.ts Sat Mar 19 17:45:01 2022 -0700 +++ b/src/render/GraphView.ts Sun Mar 20 00:54:19 2022 -0700 @@ -8,6 +8,7 @@ PredRow, SubjRow, } from "../layout/Layout"; +import { uniqueSortedTerms } from "../layout/rdf_value"; import { SuffixLabels } from "../layout/suffixLabels"; import { ViewConfig } from "../layout/ViewConfig"; import { NodeDisplay } from "./NodeDisplay"; @@ -74,6 +75,7 @@ null ); } + _renderSection(section: AlignedTable | FreeStatements) { if ((section as any).columnHeaders) { return this._renderAlignedTable(section as AlignedTable); @@ -83,31 +85,56 @@ } _renderAlignedTable(section: AlignedTable): TemplateResult { - let anyType: NamedNode = new NamedNode(''); - const heads = section.columnHeaders.map((ch) => { - anyType = ch.rdfType; - return html`<th>${this.nodeDisplay.render(ch.pred)}</th>`; - }); + const tableTypes: NamedNode[][] = []; + const typeHeads: TemplateResult[] = []; + const heads: TemplateResult[] = []; + for (let ch of section.columnHeaders) { + const colSpan = 1; //todo + typeHeads.push( + html`<th colspan="${colSpan}"> + ${ch.rdfTypes.map((n) => this.nodeDisplay.render(n))} + </th>` + ); + + tableTypes.push(ch.rdfTypes); + heads.push(html`<th>${this.nodeDisplay.render(ch.pred)}</th>`); + } + const cells = []; for (let rowIndex in section.rows) { - const headerCol = html`<th>${this.nodeDisplay.render(section.rowHeaders[rowIndex])}</th>`; - const bodyCols = [] + const headerCol = this.nodeDisplay.render(section.rowHeaders[rowIndex]); + const bodyCols = []; for (let cellObjs of section.rows[rowIndex]) { const display = cellObjs.map( (t) => html`<div>${this.nodeDisplay.render(t)}</div>` ); bodyCols.push(html`<td>${display}</td>`); } - cells.push(html`<tr>${headerCol}${bodyCols}</tr>`); + cells.push( + html`<tr> + <th>${headerCol}</th> + ${bodyCols} + </tr>` + ); } + const tableTypesUnique = uniqueSortedTerms(tableTypes.flat()); + const typesDisplay = html`${tableTypesUnique.length == 1 ? "type" : "types"} + ${tableTypesUnique.map((n) => this.nodeDisplay.render(n))}`; + return html` - <div>[icon] Resources of type ${this.nodeDisplay.render(anyType)}</div> + <div>[icon] Resources of ${typesDisplay}</div> <div class="typeBlockScroll"> <table class="typeBlock"> <thead> - <th>Subject</th> - ${heads} + <tr> + <th></th> + ${typeHeads} + </tr> + <tr> + <th>Subject</th> + ${heads} + </tr> </thead> <tbody> ${cells} @@ -163,7 +190,6 @@ return html` <th>${this.nodeDisplay.render(pred)}</th> `; } - // return html` // <div>[icon] Resources of type ${typeNames}</div> // <div class="typeBlockScroll">