From 378f627a6fc5e9d78344cb521c4e7ddcad96b70c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dandelion=20Man=C3=A9?= Date: Fri, 10 Aug 2018 17:05:59 -0700 Subject: [PATCH] Add `aggregateFlat` and `flattenAggregation` (#629) For #502: The UI that I currently have in mind displays aggregations grouped by connection type and node type together, rather than nested. I think it will be cumbersome to have multiple hierarchical levels of expansion. To make that UI easy to write, this commit adds some logic for flattening the hiearchical aggregation from #624. I add an extra translation to flatten, rather than just having the logic produce nested structures, because it's convenient to keep around the nested structure in case I decide to implement the hierarchical UI instead. Once we have solidified how we want the UI to behave, we might choose to simplify this code. Test plan: The implementation is rather simple. There are some unit tests. --- .../credExplorer/pagerankTable/aggregate.js | 38 +++++++++ .../pagerankTable/aggregate.test.js | 78 ++++++++++++++++++- 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/src/app/credExplorer/pagerankTable/aggregate.js b/src/app/credExplorer/pagerankTable/aggregate.js index 803d414..1aca483 100644 --- a/src/app/credExplorer/pagerankTable/aggregate.js +++ b/src/app/credExplorer/pagerankTable/aggregate.js @@ -6,6 +6,8 @@ import {NodeTrie, EdgeTrie} from "../../../core/trie"; import type {NodeType, EdgeType} from "../../adapters/pluginAdapter"; import type {ScoredConnection} from "../../../core/attribution/pagerankNodeDecomposition"; +// Sorted by descending `summary.score` +export type FlatAggregations = $ReadOnlyArray; // Sorted by descending `summary.score` export type ConnectionAggregations = $ReadOnlyArray; @@ -32,6 +34,14 @@ export type ConnectionAggregation = {| +nodeAggregations: $ReadOnlyArray, |}; +export type FlatAggregation = {| + +connectionType: ConnectionType, + +nodeType: NodeType, + +summary: AggregationSummary, + // sorted by `scoredConnection.connectionScore` + +connections: $ReadOnlyArray, +|}; + export function aggregateByNodeType( xs: $ReadOnlyArray, nodeTypes: $ReadOnlyArray @@ -146,3 +156,31 @@ export function aggregateByConnectionType( return sortBy(result, (x) => -x.summary.score); } + +export function flattenAggregation( + xs: ConnectionAggregations +): FlatAggregations { + const result = []; + for (const {connectionType, nodeAggregations} of xs) { + for (const {summary, connections, nodeType} of nodeAggregations) { + const flat: FlatAggregation = { + summary, + connections, + nodeType, + connectionType, + }; + result.push(flat); + } + } + return sortBy(result, (x) => -x.summary.score); +} + +export function aggregateFlat( + xs: $ReadOnlyArray, + nodeTypes: $ReadOnlyArray, + edgeTypes: $ReadOnlyArray +): FlatAggregations { + return flattenAggregation( + aggregateByConnectionType(xs, nodeTypes, edgeTypes) + ); +} diff --git a/src/app/credExplorer/pagerankTable/aggregate.test.js b/src/app/credExplorer/pagerankTable/aggregate.test.js index 8690575..2eef03d 100644 --- a/src/app/credExplorer/pagerankTable/aggregate.test.js +++ b/src/app/credExplorer/pagerankTable/aggregate.test.js @@ -1,7 +1,13 @@ // @flow import {EdgeAddress, NodeAddress} from "../../../core/graph"; -import {aggregateByNodeType, aggregateByConnectionType} from "./aggregate"; +import * as NullUtil from "../../../util/null"; +import { + aggregateByNodeType, + aggregateByConnectionType, + flattenAggregation, + aggregateFlat, +} from "./aggregate"; describe("app/credExplorer/aggregate", () => { function example() { @@ -340,4 +346,74 @@ describe("app/credExplorer/aggregate", () => { } }); }); + + describe("flattenAggregation", () => { + function getFlatAggregations() { + const { + nodeTypesArray, + edgeTypesArray, + scoredConnectionsArray, + } = example(); + const byCT = aggregateByConnectionType( + scoredConnectionsArray, + nodeTypesArray, + edgeTypesArray + ); + const flat = flattenAggregation(byCT); + return {byCT, flat}; + } + it("works on an empty aggregation", () => { + expect(flattenAggregation([])).toEqual([]); + }); + it("returns aggregations in score order", () => { + const {flat} = getFlatAggregations(); + let lastScore = Infinity; + for (const agg of flat) { + const score = agg.summary.score; + expect(lastScore >= score).toBe(true); + lastScore = score; + } + }); + it("each FlatAggregation corresponds to a nested NodeAggregation", () => { + const {flat, byCT} = getFlatAggregations(); + for (const agg of flat) { + const matchingConnectionAggregation = NullUtil.get( + byCT.find((x) => x.connectionType === agg.connectionType) + ); + const matchingNodeAggregation = NullUtil.get( + matchingConnectionAggregation.nodeAggregations.find( + (x) => x.nodeType === agg.nodeType + ) + ); + expect(agg.summary).toEqual(matchingNodeAggregation.summary); + expect(agg.connections).toEqual(matchingNodeAggregation.connections); + } + let numNodeAggregations = 0; + for (const agg of byCT) { + numNodeAggregations += agg.nodeAggregations.length; + } + expect(numNodeAggregations).toEqual(flat.length); + }); + }); + describe("aggregateFlat", () => { + it("is the composition of aggregateByConnectionType and flattenAggregation", () => { + const { + nodeTypesArray, + edgeTypesArray, + scoredConnectionsArray, + } = example(); + const byCT = aggregateByConnectionType( + scoredConnectionsArray, + nodeTypesArray, + edgeTypesArray + ); + const flat = flattenAggregation(byCT); + const fromScratch = aggregateFlat( + scoredConnectionsArray, + nodeTypesArray, + edgeTypesArray + ); + expect(fromScratch).toEqual(flat); + }); + }); });