explorer: tweak weights on a per-node basis (#1143)
This pull request adds a weight slider to every NodeRow in the explorer, enabling the user to manually set a weight for that node. The weights are multiplicative with the type level weights, so that they can be changed independently (e.g. you can have a comment that is weighted 2x higher than regular comments, but still have comments get a low weight in general). This pull coordinates a number of different changes across the codebase, all of which are tested: Adding support for manual weights in the weights and weightsToEdgeEvaluator modules. Modifying pagerankTable.TableRow so that it can show a slider in the second column. Adding piping for manual weights into the PagerankTable shared props, and into the explorer app Adding the slider to the NodeRow class that displays the current weight, and can trigger the upstream weight change Ensuring that the runPagerank call in the explorer actually uses the manual weights At present, there is no way to save these weights (they are ephemeral in the frontend) and so this is clearly a prototype/tech demo level feature rather than being ready for real usage. Correspondingly, CLI pagerank command always uses an empty set of manual weights. I plan to remedy this in a follow-on pull request. Test plan: Run the included unit tests (yarn test) and also spin up the UI, verify that it visually looks good in both Firefox and Chrome, and verify that changing the weights and then re-running PageRank actually causes the cred of the modified node to change. Review plan: In addition to carefully reading the code, ensure that all of the changes described a few paragraphs up are actually tested. Merge plan: Squash and merge. Thanks to @s-ben for proposing this feature in Discord, and to everyone discussing its implications in this Discourse thread.
This commit is contained in:
parent
2999d24593
commit
d2559960bb
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Allow tweaking weights on a per-node basis (#1143)
|
||||||
- Add the `pagerank` command (#1114)
|
- Add the `pagerank` command (#1114)
|
||||||
- Add the `clear` command (#1111)
|
- Add the `clear` command (#1111)
|
||||||
- Add description tooltips for node and edge types in the weight configuration UI (#1081)
|
- Add description tooltips for node and edge types in the weight configuration UI (#1081)
|
||||||
|
|
|
@ -19,6 +19,8 @@ export type WeightedTypes = {|
|
||||||
+edges: Map<EdgeAddressT, WeightedEdgeType>,
|
+edges: Map<EdgeAddressT, WeightedEdgeType>,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
|
export type ManualWeights = Map<NodeAddressT, number>;
|
||||||
|
|
||||||
export function defaultWeightedNodeType(type: NodeType): WeightedNodeType {
|
export function defaultWeightedNodeType(type: NodeType): WeightedNodeType {
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import type {Edge} from "../core/graph";
|
import * as NullUtil from "../util/null";
|
||||||
import type {WeightedTypes} from "./weights";
|
import type {Edge, NodeAddressT} from "../core/graph";
|
||||||
|
import type {WeightedTypes, ManualWeights} from "./weights";
|
||||||
import type {EdgeEvaluator} from "./pagerank";
|
import type {EdgeEvaluator} from "./pagerank";
|
||||||
import {NodeTrie, EdgeTrie} from "../core/trie";
|
import {NodeTrie, EdgeTrie} from "../core/trie";
|
||||||
|
|
||||||
export function weightsToEdgeEvaluator(weights: WeightedTypes): EdgeEvaluator {
|
export function weightsToEdgeEvaluator(
|
||||||
|
weights: WeightedTypes,
|
||||||
|
manualWeights: ManualWeights
|
||||||
|
): EdgeEvaluator {
|
||||||
const nodeTrie = new NodeTrie();
|
const nodeTrie = new NodeTrie();
|
||||||
for (const {type, weight} of weights.nodes.values()) {
|
for (const {type, weight} of weights.nodes.values()) {
|
||||||
nodeTrie.add(type.prefix, weight);
|
nodeTrie.add(type.prefix, weight);
|
||||||
|
@ -15,9 +19,15 @@ export function weightsToEdgeEvaluator(weights: WeightedTypes): EdgeEvaluator {
|
||||||
edgeTrie.add(type.prefix, {forwardWeight, backwardWeight});
|
edgeTrie.add(type.prefix, {forwardWeight, backwardWeight});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nodeWeight(n: NodeAddressT): number {
|
||||||
|
const typeWeight = nodeTrie.getLast(n);
|
||||||
|
const manualWeight = NullUtil.orElse(manualWeights.get(n), 1);
|
||||||
|
return typeWeight * manualWeight;
|
||||||
|
}
|
||||||
|
|
||||||
return function evaluator(edge: Edge) {
|
return function evaluator(edge: Edge) {
|
||||||
const srcWeight = nodeTrie.getLast(edge.src);
|
const srcWeight = nodeWeight(edge.src);
|
||||||
const dstWeight = nodeTrie.getLast(edge.dst);
|
const dstWeight = nodeWeight(edge.dst);
|
||||||
const {forwardWeight, backwardWeight} = edgeTrie.getLast(edge.address);
|
const {forwardWeight, backwardWeight} = edgeTrie.getLast(edge.address);
|
||||||
return {
|
return {
|
||||||
toWeight: dstWeight * forwardWeight,
|
toWeight: dstWeight * forwardWeight,
|
||||||
|
|
|
@ -7,7 +7,11 @@ import {
|
||||||
machineNodeType,
|
machineNodeType,
|
||||||
assemblesEdgeType,
|
assemblesEdgeType,
|
||||||
} from "../plugins/demo/declaration";
|
} from "../plugins/demo/declaration";
|
||||||
import {edges as factorioEdges} from "../plugins/demo/graph";
|
import {
|
||||||
|
edges as factorioEdges,
|
||||||
|
nodes as factorioNodes,
|
||||||
|
} from "../plugins/demo/graph";
|
||||||
|
import type {ManualWeights} from "./weights";
|
||||||
import {weightsToEdgeEvaluator} from "./weightsToEdgeEvaluator";
|
import {weightsToEdgeEvaluator} from "./weightsToEdgeEvaluator";
|
||||||
|
|
||||||
describe("analysis/weightsToEdgeEvaluator", () => {
|
describe("analysis/weightsToEdgeEvaluator", () => {
|
||||||
|
@ -20,6 +24,7 @@ describe("analysis/weightsToEdgeEvaluator", () => {
|
||||||
+inserter?: number,
|
+inserter?: number,
|
||||||
+machine?: number,
|
+machine?: number,
|
||||||
+baseNode?: number,
|
+baseNode?: number,
|
||||||
|
+manualWeights?: ManualWeights,
|
||||||
|};
|
|};
|
||||||
function weights({
|
function weights({
|
||||||
assemblesForward,
|
assemblesForward,
|
||||||
|
@ -53,7 +58,8 @@ describe("analysis/weightsToEdgeEvaluator", () => {
|
||||||
}
|
}
|
||||||
function exampleEdgeWeights(weightArgs: WeightArgs) {
|
function exampleEdgeWeights(weightArgs: WeightArgs) {
|
||||||
const ws = weights(weightArgs);
|
const ws = weights(weightArgs);
|
||||||
const ee = weightsToEdgeEvaluator(ws);
|
const manualWeights = weightArgs.manualWeights || new Map();
|
||||||
|
const ee = weightsToEdgeEvaluator(ws, manualWeights);
|
||||||
// src is a machine, dst is an inserter, edge type is assembles
|
// src is a machine, dst is an inserter, edge type is assembles
|
||||||
return ee(factorioEdges.assembles1);
|
return ee(factorioEdges.assembles1);
|
||||||
}
|
}
|
||||||
|
@ -91,5 +97,22 @@ describe("analysis/weightsToEdgeEvaluator", () => {
|
||||||
})
|
})
|
||||||
).toEqual({toWeight: 8, froWeight: 15});
|
).toEqual({toWeight: 8, froWeight: 15});
|
||||||
});
|
});
|
||||||
|
it("manualWeight and nodeTypeWeight both multiply the weight", () => {
|
||||||
|
const manualWeights = new Map();
|
||||||
|
manualWeights.set(factorioNodes.inserter2, 2);
|
||||||
|
// Putting a weight of 2 on the inserter node type as a whole or on the the
|
||||||
|
// particular insterter node will have the same effect
|
||||||
|
expect(exampleEdgeWeights({inserter: 2})).toEqual(
|
||||||
|
exampleEdgeWeights({manualWeights})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("manualWeight and nodeTypeWeight compose multiplicatively", () => {
|
||||||
|
const manualWeights = new Map();
|
||||||
|
manualWeights.set(factorioNodes.inserter2, 2);
|
||||||
|
expect(exampleEdgeWeights({inserter: 3, manualWeights})).toEqual({
|
||||||
|
toWeight: 6,
|
||||||
|
froWeight: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {loadGraph, type LoadGraphResult} from "../analysis/loadGraph";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type WeightedTypes,
|
type WeightedTypes,
|
||||||
|
type ManualWeights,
|
||||||
combineWeights,
|
combineWeights,
|
||||||
defaultWeightsForDeclaration,
|
defaultWeightsForDeclaration,
|
||||||
} from "../analysis/weights";
|
} from "../analysis/weights";
|
||||||
|
@ -144,10 +145,11 @@ export function makePagerankCommand(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runPagerank(
|
export async function runPagerank(
|
||||||
weights: WeightedTypes,
|
typeWeights: WeightedTypes,
|
||||||
|
manualWeights: ManualWeights,
|
||||||
graph: Graph
|
graph: Graph
|
||||||
): Promise<PagerankGraph> {
|
): Promise<PagerankGraph> {
|
||||||
const evaluator = weightsToEdgeEvaluator(weights);
|
const evaluator = weightsToEdgeEvaluator(typeWeights, manualWeights);
|
||||||
const pagerankGraph = new PagerankGraph(
|
const pagerankGraph = new PagerankGraph(
|
||||||
graph,
|
graph,
|
||||||
evaluator,
|
evaluator,
|
||||||
|
@ -187,7 +189,8 @@ export const defaultAdapters = () => [
|
||||||
const defaultLoader = (r: RepoId) =>
|
const defaultLoader = (r: RepoId) =>
|
||||||
loadGraph(Common.sourcecredDirectory(), defaultAdapters(), r);
|
loadGraph(Common.sourcecredDirectory(), defaultAdapters(), r);
|
||||||
export const defaultWeights = () => weightsForAdapters(defaultAdapters());
|
export const defaultWeights = () => weightsForAdapters(defaultAdapters());
|
||||||
export const defaultPagerank = (g: Graph) => runPagerank(defaultWeights(), g);
|
export const defaultPagerank = (g: Graph) =>
|
||||||
|
runPagerank(defaultWeights(), new Map(), g);
|
||||||
export const defaultSaver = (r: RepoId, pg: PagerankGraph) =>
|
export const defaultSaver = (r: RepoId, pg: PagerankGraph) =>
|
||||||
savePagerankGraph(Common.sourcecredDirectory(), r, pg);
|
savePagerankGraph(Common.sourcecredDirectory(), r, pg);
|
||||||
|
|
||||||
|
|
|
@ -230,11 +230,17 @@ describe("cli/pagerank", () => {
|
||||||
fallbackWeightedTypes,
|
fallbackWeightedTypes,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const manualWeights = new Map();
|
||||||
|
manualWeights.set(advancedGraph().nodes.src(), 4);
|
||||||
const graph = advancedGraph().graph1();
|
const graph = advancedGraph().graph1();
|
||||||
const actualPagerankGraph = await runPagerank(weightedTypes, graph);
|
const actualPagerankGraph = await runPagerank(
|
||||||
|
weightedTypes,
|
||||||
|
manualWeights,
|
||||||
|
graph
|
||||||
|
);
|
||||||
const expectedPagerankGraph = new PagerankGraph(
|
const expectedPagerankGraph = new PagerankGraph(
|
||||||
graph,
|
graph,
|
||||||
weightsToEdgeEvaluator(weightedTypes),
|
weightsToEdgeEvaluator(weightedTypes, manualWeights),
|
||||||
DEFAULT_SYNTHETIC_LOOP_WEIGHT
|
DEFAULT_SYNTHETIC_LOOP_WEIGHT
|
||||||
);
|
);
|
||||||
await expectedPagerankGraph.runPagerank({
|
await expectedPagerankGraph.runPagerank({
|
||||||
|
|
|
@ -8,6 +8,7 @@ import CheckedLocalStore from "../webutil/checkedLocalStore";
|
||||||
import BrowserLocalStore from "../webutil/browserLocalStore";
|
import BrowserLocalStore from "../webutil/browserLocalStore";
|
||||||
import Link from "../webutil/Link";
|
import Link from "../webutil/Link";
|
||||||
import type {RepoId} from "../core/repoId";
|
import type {RepoId} from "../core/repoId";
|
||||||
|
import {type NodeAddressT} from "../core/graph";
|
||||||
|
|
||||||
import {PagerankTable} from "./pagerankTable/Table";
|
import {PagerankTable} from "./pagerankTable/Table";
|
||||||
import type {WeightedTypes} from "../analysis/weights";
|
import type {WeightedTypes} from "../analysis/weights";
|
||||||
|
@ -61,6 +62,7 @@ type Props = {|
|
||||||
type State = {|
|
type State = {|
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
weightedTypes: WeightedTypes,
|
weightedTypes: WeightedTypes,
|
||||||
|
manualWeights: Map<NodeAddressT, number>,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
export function createApp(
|
export function createApp(
|
||||||
|
@ -77,6 +79,7 @@ export function createApp(
|
||||||
this.state = {
|
this.state = {
|
||||||
appState: initialState(this.props.repoId),
|
appState: initialState(this.props.repoId),
|
||||||
weightedTypes: defaultWeightsForAdapterSet(props.adapters),
|
weightedTypes: defaultWeightsForAdapterSet(props.adapters),
|
||||||
|
manualWeights: new Map(),
|
||||||
};
|
};
|
||||||
this.stateTransitionMachine = createSTM(
|
this.stateTransitionMachine = createSTM(
|
||||||
() => this.state.appState,
|
() => this.state.appState,
|
||||||
|
@ -98,6 +101,13 @@ export function createApp(
|
||||||
onWeightedTypesChange={(weightedTypes) =>
|
onWeightedTypesChange={(weightedTypes) =>
|
||||||
this.setState({weightedTypes})
|
this.setState({weightedTypes})
|
||||||
}
|
}
|
||||||
|
manualWeights={this.state.manualWeights}
|
||||||
|
onManualWeightsChange={(addr: NodeAddressT, weight: number) =>
|
||||||
|
this.setState(({manualWeights}) => {
|
||||||
|
manualWeights.set(addr, weight);
|
||||||
|
return {manualWeights};
|
||||||
|
})
|
||||||
|
}
|
||||||
pnd={pnd}
|
pnd={pnd}
|
||||||
maxEntriesPerList={100}
|
maxEntriesPerList={100}
|
||||||
/>
|
/>
|
||||||
|
@ -124,6 +134,7 @@ export function createApp(
|
||||||
this.props.assets,
|
this.props.assets,
|
||||||
this.props.adapters,
|
this.props.adapters,
|
||||||
this.state.weightedTypes,
|
this.state.weightedTypes,
|
||||||
|
this.state.manualWeights,
|
||||||
GithubPrefix.user
|
GithubPrefix.user
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {shallow} from "enzyme";
|
import {shallow} from "enzyme";
|
||||||
|
|
||||||
import {Graph} from "../core/graph";
|
import {Graph, NodeAddress} from "../core/graph";
|
||||||
import {makeRepoId} from "../core/repoId";
|
import {makeRepoId} from "../core/repoId";
|
||||||
import {Assets} from "../webutil/assets";
|
import {Assets} from "../webutil/assets";
|
||||||
import testLocalStore from "../webutil/testLocalStore";
|
import testLocalStore from "../webutil/testLocalStore";
|
||||||
|
@ -153,6 +153,7 @@ describe("explorer/App", () => {
|
||||||
el.instance().props.assets,
|
el.instance().props.assets,
|
||||||
el.instance().props.adapters,
|
el.instance().props.adapters,
|
||||||
el.instance().state.weightedTypes,
|
el.instance().state.weightedTypes,
|
||||||
|
el.instance().state.manualWeights,
|
||||||
GithubPrefix.user
|
GithubPrefix.user
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -184,6 +185,10 @@ describe("explorer/App", () => {
|
||||||
);
|
);
|
||||||
prtWeightedTypesChange(newTypes);
|
prtWeightedTypesChange(newTypes);
|
||||||
expect(el.instance().state.weightedTypes).toBe(newTypes);
|
expect(el.instance().state.weightedTypes).toBe(newTypes);
|
||||||
|
const prtManualWeightsChange = prt.props().onManualWeightsChange;
|
||||||
|
const node = NodeAddress.fromParts(["foo"]);
|
||||||
|
prtManualWeightsChange(node, 32);
|
||||||
|
expect(el.instance().state.manualWeights.get(node)).toEqual(32);
|
||||||
} else {
|
} else {
|
||||||
expect(prt).toHaveLength(0);
|
expect(prt).toHaveLength(0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,13 +59,14 @@ export class AggregationRow extends React.PureComponent<AggregationRowProps> {
|
||||||
const score = aggregation.summary.score;
|
const score = aggregation.summary.score;
|
||||||
const {score: targetScore} = NullUtil.get(pnd.get(target));
|
const {score: targetScore} = NullUtil.get(pnd.get(target));
|
||||||
const connectionProportion = score / targetScore;
|
const connectionProportion = score / targetScore;
|
||||||
|
const connectionPercent = (connectionProportion * 100).toFixed(2) + "%";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
depth={depth}
|
depth={depth}
|
||||||
indent={1}
|
indent={1}
|
||||||
showPadding={false}
|
showPadding={false}
|
||||||
connectionProportion={connectionProportion}
|
multiuseColumn={connectionPercent}
|
||||||
cred={score}
|
cred={score}
|
||||||
description={<AggregationView aggregation={aggregation} />}
|
description={<AggregationView aggregation={aggregation} />}
|
||||||
>
|
>
|
||||||
|
|
|
@ -23,11 +23,9 @@ require("../../webutil/testUtil").configureEnzyme();
|
||||||
describe("explorer/pagerankTable/Aggregation", () => {
|
describe("explorer/pagerankTable/Aggregation", () => {
|
||||||
describe("AggregationRowList", () => {
|
describe("AggregationRowList", () => {
|
||||||
it("instantiates AggregationRows for each aggregation", async () => {
|
it("instantiates AggregationRows for each aggregation", async () => {
|
||||||
const {adapters, pnd} = await example();
|
const {adapters, pnd, sharedProps} = await example();
|
||||||
const node = factorioNodes.inserter1;
|
const node = factorioNodes.inserter1;
|
||||||
const depth = 20;
|
const depth = 20;
|
||||||
const maxEntriesPerList = 50;
|
|
||||||
const sharedProps = {adapters, pnd, maxEntriesPerList};
|
|
||||||
const connections = NullUtil.get(pnd.get(node)).scoredConnections;
|
const connections = NullUtil.get(pnd.get(node)).scoredConnections;
|
||||||
const aggregations = aggregateFlat(
|
const aggregations = aggregateFlat(
|
||||||
connections,
|
connections,
|
||||||
|
@ -58,8 +56,7 @@ describe("explorer/pagerankTable/Aggregation", () => {
|
||||||
|
|
||||||
describe("AggregationRow", () => {
|
describe("AggregationRow", () => {
|
||||||
async function setup() {
|
async function setup() {
|
||||||
const {pnd, adapters} = await example();
|
const {pnd, adapters, sharedProps} = await example();
|
||||||
const sharedProps = {adapters, pnd, maxEntriesPerList: 123};
|
|
||||||
const target = factorioNodes.inserter1;
|
const target = factorioNodes.inserter1;
|
||||||
const {scoredConnections} = NullUtil.get(pnd.get(target));
|
const {scoredConnections} = NullUtil.get(pnd.get(target));
|
||||||
const aggregations = aggregateFlat(
|
const aggregations = aggregateFlat(
|
||||||
|
@ -105,12 +102,12 @@ describe("explorer/pagerankTable/Aggregation", () => {
|
||||||
const {row, aggregation} = await setup();
|
const {row, aggregation} = await setup();
|
||||||
expect(row.props().cred).toBe(aggregation.summary.score);
|
expect(row.props().cred).toBe(aggregation.summary.score);
|
||||||
});
|
});
|
||||||
it("with the aggregation's contribution proportion", async () => {
|
it("with the aggregation's score contribution as a %", async () => {
|
||||||
const {row, target, aggregation, sharedProps} = await setup();
|
const {row, target, aggregation, sharedProps} = await setup();
|
||||||
const targetScore = NullUtil.get(sharedProps.pnd.get(target)).score;
|
const targetScore = NullUtil.get(sharedProps.pnd.get(target)).score;
|
||||||
expect(row.props().connectionProportion).toBe(
|
const expectedPercent =
|
||||||
aggregation.summary.score / targetScore
|
((aggregation.summary.score * 100) / targetScore).toFixed(2) + "%";
|
||||||
);
|
expect(row.props().multiuseColumn).toBe(expectedPercent);
|
||||||
});
|
});
|
||||||
it("with a AggregationView as description", async () => {
|
it("with a AggregationView as description", async () => {
|
||||||
const {row, aggregation} = await setup();
|
const {row, aggregation} = await setup();
|
||||||
|
|
|
@ -61,6 +61,7 @@ export class ConnectionRow extends React.PureComponent<ConnectionRowProps> {
|
||||||
const {pnd, adapters} = sharedProps;
|
const {pnd, adapters} = sharedProps;
|
||||||
const {score: targetScore} = NullUtil.get(pnd.get(target));
|
const {score: targetScore} = NullUtil.get(pnd.get(target));
|
||||||
const connectionProportion = connectionScore / targetScore;
|
const connectionProportion = connectionScore / targetScore;
|
||||||
|
const connectionPercent = (connectionProportion * 100).toFixed(2) + "%";
|
||||||
|
|
||||||
const connectionView = (
|
const connectionView = (
|
||||||
<ConnectionView connection={connection} adapters={adapters} />
|
<ConnectionView connection={connection} adapters={adapters} />
|
||||||
|
@ -70,7 +71,7 @@ export class ConnectionRow extends React.PureComponent<ConnectionRowProps> {
|
||||||
indent={2}
|
indent={2}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
description={connectionView}
|
description={connectionView}
|
||||||
connectionProportion={connectionProportion}
|
multiuseColumn={connectionPercent}
|
||||||
showPadding={false}
|
showPadding={false}
|
||||||
cred={connectionScore}
|
cred={connectionScore}
|
||||||
>
|
>
|
||||||
|
|
|
@ -15,11 +15,11 @@ require("../../webutil/testUtil").configureEnzyme();
|
||||||
|
|
||||||
describe("explorer/pagerankTable/Connection", () => {
|
describe("explorer/pagerankTable/Connection", () => {
|
||||||
describe("ConnectionRowList", () => {
|
describe("ConnectionRowList", () => {
|
||||||
async function setup(maxEntriesPerList: number = 100000) {
|
async function setup(maxEntriesPerList: number = 123) {
|
||||||
const {adapters, pnd} = await example();
|
let {sharedProps} = await example();
|
||||||
|
sharedProps = {...sharedProps, maxEntriesPerList};
|
||||||
const depth = 2;
|
const depth = 2;
|
||||||
const node = factorioNodes.inserter1;
|
const node = factorioNodes.inserter1;
|
||||||
const sharedProps = {adapters, pnd, maxEntriesPerList};
|
|
||||||
const connections = NullUtil.get(sharedProps.pnd.get(node))
|
const connections = NullUtil.get(sharedProps.pnd.get(node))
|
||||||
.scoredConnections;
|
.scoredConnections;
|
||||||
const component = (
|
const component = (
|
||||||
|
@ -66,8 +66,7 @@ describe("explorer/pagerankTable/Connection", () => {
|
||||||
|
|
||||||
describe("ConnectionRow", () => {
|
describe("ConnectionRow", () => {
|
||||||
async function setup() {
|
async function setup() {
|
||||||
const {pnd, adapters} = await example();
|
const {pnd, sharedProps} = await example();
|
||||||
const sharedProps = {adapters, pnd, maxEntriesPerList: 123};
|
|
||||||
const target = factorioNodes.inserter1;
|
const target = factorioNodes.inserter1;
|
||||||
const {scoredConnections} = NullUtil.get(pnd.get(target));
|
const {scoredConnections} = NullUtil.get(pnd.get(target));
|
||||||
const scoredConnection = scoredConnections[0];
|
const scoredConnection = scoredConnections[0];
|
||||||
|
@ -100,12 +99,13 @@ describe("explorer/pagerankTable/Connection", () => {
|
||||||
const {row, scoredConnection} = await setup();
|
const {row, scoredConnection} = await setup();
|
||||||
expect(row.props().cred).toBe(scoredConnection.connectionScore);
|
expect(row.props().cred).toBe(scoredConnection.connectionScore);
|
||||||
});
|
});
|
||||||
it("with the connectionProportion", async () => {
|
it("with the connectionProportion in the multiuseColumn", async () => {
|
||||||
const {row, target, scoredConnection, sharedProps} = await setup();
|
const {row, target, scoredConnection, sharedProps} = await setup();
|
||||||
const targetScore = NullUtil.get(sharedProps.pnd.get(target)).score;
|
const targetScore = NullUtil.get(sharedProps.pnd.get(target)).score;
|
||||||
expect(row.props().connectionProportion).toBe(
|
const expectedPercent =
|
||||||
scoredConnection.connectionScore / targetScore
|
((scoredConnection.connectionScore * 100) / targetScore).toFixed(2) +
|
||||||
);
|
"%";
|
||||||
|
expect(row.props().multiuseColumn).toBe(expectedPercent);
|
||||||
});
|
});
|
||||||
it("with a ConnectionView as description", async () => {
|
it("with a ConnectionView as description", async () => {
|
||||||
const {row, sharedProps, scoredConnection} = await setup();
|
const {row, sharedProps, scoredConnection} = await setup();
|
||||||
|
|
|
@ -6,6 +6,13 @@ import * as NullUtil from "../../util/null";
|
||||||
|
|
||||||
import {type NodeAddressT} from "../../core/graph";
|
import {type NodeAddressT} from "../../core/graph";
|
||||||
import {TableRow} from "./TableRow";
|
import {TableRow} from "./TableRow";
|
||||||
|
import {
|
||||||
|
MIN_SLIDER,
|
||||||
|
MAX_SLIDER,
|
||||||
|
formatWeight,
|
||||||
|
sliderToWeight,
|
||||||
|
weightToSlider,
|
||||||
|
} from "../weights/WeightSlider";
|
||||||
|
|
||||||
import {nodeDescription, type SharedProps} from "./shared";
|
import {nodeDescription, type SharedProps} from "./shared";
|
||||||
|
|
||||||
|
@ -48,8 +55,25 @@ export type NodeRowProps = {|
|
||||||
export class NodeRow extends React.PureComponent<NodeRowProps> {
|
export class NodeRow extends React.PureComponent<NodeRowProps> {
|
||||||
render() {
|
render() {
|
||||||
const {depth, node, sharedProps, showPadding} = this.props;
|
const {depth, node, sharedProps, showPadding} = this.props;
|
||||||
const {pnd, adapters} = sharedProps;
|
const {pnd, adapters, onManualWeightsChange, manualWeights} = sharedProps;
|
||||||
const {score} = NullUtil.get(pnd.get(node));
|
const {score} = NullUtil.get(pnd.get(node));
|
||||||
|
const weight = NullUtil.orElse(manualWeights.get(node), 1);
|
||||||
|
const slider = (
|
||||||
|
<label>
|
||||||
|
<span>{formatWeight(weight)}</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={MIN_SLIDER}
|
||||||
|
max={MAX_SLIDER}
|
||||||
|
step={1}
|
||||||
|
value={weightToSlider(weight)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const weight = sliderToWeight(e.target.valueAsNumber);
|
||||||
|
onManualWeightsChange(node, weight);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
const description = <span>{nodeDescription(node, adapters)}</span>;
|
const description = <span>{nodeDescription(node, adapters)}</span>;
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
|
@ -57,7 +81,7 @@ export class NodeRow extends React.PureComponent<NodeRowProps> {
|
||||||
indent={0}
|
indent={0}
|
||||||
showPadding={showPadding}
|
showPadding={showPadding}
|
||||||
description={description}
|
description={description}
|
||||||
connectionProportion={null}
|
multiuseColumn={slider}
|
||||||
cred={score}
|
cred={score}
|
||||||
>
|
>
|
||||||
<AggregationRowList
|
<AggregationRowList
|
||||||
|
|
|
@ -6,6 +6,12 @@ import sortBy from "lodash.sortby";
|
||||||
import * as NullUtil from "../../util/null";
|
import * as NullUtil from "../../util/null";
|
||||||
import {TableRow} from "./TableRow";
|
import {TableRow} from "./TableRow";
|
||||||
import {AggregationRowList} from "./Aggregation";
|
import {AggregationRowList} from "./Aggregation";
|
||||||
|
import {
|
||||||
|
MIN_SLIDER,
|
||||||
|
MAX_SLIDER,
|
||||||
|
sliderToWeight,
|
||||||
|
weightToSlider,
|
||||||
|
} from "../weights/WeightSlider";
|
||||||
|
|
||||||
import type {NodeAddressT} from "../../core/graph";
|
import type {NodeAddressT} from "../../core/graph";
|
||||||
|
|
||||||
|
@ -21,11 +27,11 @@ describe("explorer/pagerankTable/Node", () => {
|
||||||
function sortedByScore(nodes: $ReadOnlyArray<NodeAddressT>, pnd) {
|
function sortedByScore(nodes: $ReadOnlyArray<NodeAddressT>, pnd) {
|
||||||
return sortBy(nodes, (node) => -NullUtil.get(pnd.get(node)).score);
|
return sortBy(nodes, (node) => -NullUtil.get(pnd.get(node)).score);
|
||||||
}
|
}
|
||||||
async function setup(maxEntriesPerList: number = 100000) {
|
async function setup(maxEntriesPerList: number = 123) {
|
||||||
const {adapters, pnd} = await example();
|
const {adapters, pnd, sharedProps: changeEntries} = await example();
|
||||||
|
const sharedProps = {...changeEntries, maxEntriesPerList};
|
||||||
const nodes = Array.from(pnd.keys());
|
const nodes = Array.from(pnd.keys());
|
||||||
expect(nodes).not.toHaveLength(0);
|
expect(nodes).not.toHaveLength(0);
|
||||||
const sharedProps = {adapters, pnd, maxEntriesPerList};
|
|
||||||
const component = <NodeRowList sharedProps={sharedProps} nodes={nodes} />;
|
const component = <NodeRowList sharedProps={sharedProps} nodes={nodes} />;
|
||||||
const element = shallow(component);
|
const element = shallow(component);
|
||||||
return {element, adapters, sharedProps, nodes};
|
return {element, adapters, sharedProps, nodes};
|
||||||
|
@ -71,15 +77,17 @@ describe("explorer/pagerankTable/Node", () => {
|
||||||
describe("NodeRow", () => {
|
describe("NodeRow", () => {
|
||||||
async function setup(props: $Shape<{...NodeRowProps}>) {
|
async function setup(props: $Shape<{...NodeRowProps}>) {
|
||||||
props = props || {};
|
props = props || {};
|
||||||
const {pnd, adapters} = await example();
|
let {sharedProps} = await example();
|
||||||
const sharedProps = {adapters, pnd, maxEntriesPerList: 123};
|
if (props.sharedProps !== null) {
|
||||||
|
sharedProps = {...sharedProps, ...props.sharedProps};
|
||||||
|
}
|
||||||
const node = factorioNodes.inserter1;
|
const node = factorioNodes.inserter1;
|
||||||
const component = shallow(
|
const component = shallow(
|
||||||
<NodeRow
|
<NodeRow
|
||||||
node={NullUtil.orElse(props.node, node)}
|
node={NullUtil.orElse(props.node, node)}
|
||||||
showPadding={NullUtil.orElse(props.showPadding, false)}
|
showPadding={NullUtil.orElse(props.showPadding, false)}
|
||||||
depth={NullUtil.orElse(props.depth, 0)}
|
depth={NullUtil.orElse(props.depth, 0)}
|
||||||
sharedProps={NullUtil.orElse(props.sharedProps, sharedProps)}
|
sharedProps={sharedProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const row = component.find(TableRow);
|
const row = component.find(TableRow);
|
||||||
|
@ -103,9 +111,58 @@ describe("explorer/pagerankTable/Node", () => {
|
||||||
const score = NullUtil.get(sharedProps.pnd.get(node)).score;
|
const score = NullUtil.get(sharedProps.pnd.get(node)).score;
|
||||||
expect(row.props().cred).toBe(score);
|
expect(row.props().cred).toBe(score);
|
||||||
});
|
});
|
||||||
it("with no connectionProportion", async () => {
|
describe("with a weight slider", () => {
|
||||||
const {row} = await setup();
|
async function setupSlider(initialWeight: number = 0) {
|
||||||
expect(row.props().connectionProportion).not.toEqual(expect.anything());
|
const node = factorioNodes.inserter1;
|
||||||
|
const manualWeights = new Map([[node, initialWeight]]);
|
||||||
|
const partialSharedProps: any = {manualWeights};
|
||||||
|
const {row, sharedProps} = await setup({
|
||||||
|
sharedProps: partialSharedProps,
|
||||||
|
});
|
||||||
|
const multiuseColumn = shallow(row.props().multiuseColumn);
|
||||||
|
const label = multiuseColumn.find("label");
|
||||||
|
const input = label.find("input");
|
||||||
|
const span = label.find("span");
|
||||||
|
const {onManualWeightsChange} = sharedProps;
|
||||||
|
return {
|
||||||
|
onManualWeightsChange,
|
||||||
|
manualWeights,
|
||||||
|
input,
|
||||||
|
span,
|
||||||
|
node,
|
||||||
|
label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
it("which consists of a range input and span within a label", async () => {
|
||||||
|
const {label, input, span} = await setupSlider();
|
||||||
|
expect(label).toHaveLength(1);
|
||||||
|
expect(input).toHaveLength(1);
|
||||||
|
expect(input.props().type).toEqual("range");
|
||||||
|
expect(span).toHaveLength(1);
|
||||||
|
});
|
||||||
|
it("whose onChange triggers onManualWeightsChange", async () => {
|
||||||
|
const {node, input, onManualWeightsChange} = await setupSlider();
|
||||||
|
expect(onManualWeightsChange).toHaveBeenCalledTimes(0);
|
||||||
|
input.simulate("change", {target: {valueAsNumber: MIN_SLIDER}});
|
||||||
|
expect(onManualWeightsChange).toHaveBeenLastCalledWith(
|
||||||
|
node,
|
||||||
|
sliderToWeight(MIN_SLIDER)
|
||||||
|
);
|
||||||
|
input.simulate("change", {target: {valueAsNumber: MAX_SLIDER}});
|
||||||
|
expect(onManualWeightsChange).toHaveBeenLastCalledWith(
|
||||||
|
node,
|
||||||
|
sliderToWeight(MAX_SLIDER)
|
||||||
|
);
|
||||||
|
expect(onManualWeightsChange).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
it("which encodes the weight in slider position", async () => {
|
||||||
|
const {input} = await setupSlider(4);
|
||||||
|
expect(input.props().value).toEqual(weightToSlider(4));
|
||||||
|
});
|
||||||
|
it("which prints the weight in text format", async () => {
|
||||||
|
const {span} = await setupSlider(4);
|
||||||
|
expect(span.text()).toEqual("4×");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it("with the node description", async () => {
|
it("with the node description", async () => {
|
||||||
const {row, sharedProps, node} = await setup();
|
const {row, sharedProps, node} = await setup();
|
||||||
|
|
|
@ -4,7 +4,7 @@ import React from "react";
|
||||||
import sortBy from "lodash.sortby";
|
import sortBy from "lodash.sortby";
|
||||||
import * as NullUtil from "../../util/null";
|
import * as NullUtil from "../../util/null";
|
||||||
|
|
||||||
import {NodeAddress} from "../../core/graph";
|
import {NodeAddress, type NodeAddressT} from "../../core/graph";
|
||||||
import type {PagerankNodeDecomposition} from "../../analysis/pagerankNodeDecomposition";
|
import type {PagerankNodeDecomposition} from "../../analysis/pagerankNodeDecomposition";
|
||||||
import {DynamicExplorerAdapterSet} from "../adapters/explorerAdapterSet";
|
import {DynamicExplorerAdapterSet} from "../adapters/explorerAdapterSet";
|
||||||
import type {DynamicExplorerAdapter} from "../adapters/explorerAdapter";
|
import type {DynamicExplorerAdapter} from "../adapters/explorerAdapter";
|
||||||
|
@ -22,6 +22,8 @@ type PagerankTableProps = {|
|
||||||
+onWeightedTypesChange: (WeightedTypes) => void,
|
+onWeightedTypesChange: (WeightedTypes) => void,
|
||||||
+maxEntriesPerList: number,
|
+maxEntriesPerList: number,
|
||||||
+defaultNodeType: ?NodeType,
|
+defaultNodeType: ?NodeType,
|
||||||
|
+manualWeights: Map<NodeAddressT, number>,
|
||||||
|
+onManualWeightsChange: (NodeAddressT, number) => void,
|
||||||
|};
|
|};
|
||||||
type PagerankTableState = {|
|
type PagerankTableState = {|
|
||||||
selectedNodeType: NodeType,
|
selectedNodeType: NodeType,
|
||||||
|
@ -128,12 +130,24 @@ export class PagerankTable extends React.PureComponent<
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTable() {
|
renderTable() {
|
||||||
const {pnd, adapters, maxEntriesPerList} = this.props;
|
const {
|
||||||
|
pnd,
|
||||||
|
adapters,
|
||||||
|
maxEntriesPerList,
|
||||||
|
manualWeights,
|
||||||
|
onManualWeightsChange,
|
||||||
|
} = this.props;
|
||||||
if (pnd == null || adapters == null || maxEntriesPerList == null) {
|
if (pnd == null || adapters == null || maxEntriesPerList == null) {
|
||||||
throw new Error("Impossible.");
|
throw new Error("Impossible.");
|
||||||
}
|
}
|
||||||
const selectedNodeTypePrefix = this.state.selectedNodeType.prefix;
|
const selectedNodeTypePrefix = this.state.selectedNodeType.prefix;
|
||||||
const sharedProps = {pnd, adapters, maxEntriesPerList};
|
const sharedProps = {
|
||||||
|
pnd,
|
||||||
|
adapters,
|
||||||
|
maxEntriesPerList,
|
||||||
|
manualWeights,
|
||||||
|
onManualWeightsChange,
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<table
|
<table
|
||||||
style={{
|
style={{
|
||||||
|
|
|
@ -18,9 +18,16 @@ require("../../webutil/testUtil").configureEnzyme();
|
||||||
describe("explorer/pagerankTable/Table", () => {
|
describe("explorer/pagerankTable/Table", () => {
|
||||||
describe("PagerankTable", () => {
|
describe("PagerankTable", () => {
|
||||||
async function setup(defaultNodeType?: NodeType) {
|
async function setup(defaultNodeType?: NodeType) {
|
||||||
const {pnd, adapters, weightedTypes} = await example();
|
const {
|
||||||
const onWeightedTypesChange = jest.fn();
|
pnd,
|
||||||
const maxEntriesPerList = 321;
|
adapters,
|
||||||
|
weightedTypes,
|
||||||
|
sharedProps,
|
||||||
|
manualWeights,
|
||||||
|
onManualWeightsChange,
|
||||||
|
onWeightedTypesChange,
|
||||||
|
maxEntriesPerList,
|
||||||
|
} = await example();
|
||||||
const element = shallow(
|
const element = shallow(
|
||||||
<PagerankTable
|
<PagerankTable
|
||||||
defaultNodeType={defaultNodeType}
|
defaultNodeType={defaultNodeType}
|
||||||
|
@ -29,9 +36,20 @@ describe("explorer/pagerankTable/Table", () => {
|
||||||
pnd={pnd}
|
pnd={pnd}
|
||||||
adapters={adapters}
|
adapters={adapters}
|
||||||
maxEntriesPerList={maxEntriesPerList}
|
maxEntriesPerList={maxEntriesPerList}
|
||||||
|
manualWeights={manualWeights}
|
||||||
|
onManualWeightsChange={onManualWeightsChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return {pnd, adapters, element, maxEntriesPerList, onWeightedTypesChange};
|
return {
|
||||||
|
pnd,
|
||||||
|
adapters,
|
||||||
|
element,
|
||||||
|
maxEntriesPerList,
|
||||||
|
onWeightedTypesChange,
|
||||||
|
onManualWeightsChange,
|
||||||
|
manualWeights,
|
||||||
|
sharedProps,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
it("renders thead column order properly", async () => {
|
it("renders thead column order properly", async () => {
|
||||||
const {element} = await setup();
|
const {element} = await setup();
|
||||||
|
@ -145,10 +163,9 @@ describe("explorer/pagerankTable/Table", () => {
|
||||||
|
|
||||||
describe("creates a NodeRowList", () => {
|
describe("creates a NodeRowList", () => {
|
||||||
it("with the correct SharedProps", async () => {
|
it("with the correct SharedProps", async () => {
|
||||||
const {element, adapters, pnd, maxEntriesPerList} = await setup();
|
const {element, sharedProps} = await setup();
|
||||||
const nrl = element.find(NodeRowList);
|
const nrl = element.find(NodeRowList);
|
||||||
const expectedSharedProps = {adapters, pnd, maxEntriesPerList};
|
expect(nrl.props().sharedProps).toEqual(sharedProps);
|
||||||
expect(nrl.props().sharedProps).toEqual(expectedSharedProps);
|
|
||||||
});
|
});
|
||||||
it("including all nodes by default", async () => {
|
it("including all nodes by default", async () => {
|
||||||
const {element, pnd} = await setup();
|
const {element, pnd} = await setup();
|
||||||
|
|
|
@ -10,8 +10,10 @@ type TableRowProps = {|
|
||||||
+indent: number,
|
+indent: number,
|
||||||
// The node that goes in the Description column
|
// The node that goes in the Description column
|
||||||
+description: ReactNode,
|
+description: ReactNode,
|
||||||
// What proportion should be formatted in the connection column
|
|
||||||
+connectionProportion: ?number,
|
// The content for the "multiuse column"
|
||||||
|
// Could be a weight slider or a cred proportion depending on context.
|
||||||
|
+multiuseColumn: ReactNode,
|
||||||
// The cred amount to format and display
|
// The cred amount to format and display
|
||||||
+cred: number,
|
+cred: number,
|
||||||
// Children to show when the row is expanded
|
// Children to show when the row is expanded
|
||||||
|
@ -39,16 +41,12 @@ export class TableRow extends React.PureComponent<
|
||||||
depth,
|
depth,
|
||||||
indent,
|
indent,
|
||||||
description,
|
description,
|
||||||
connectionProportion,
|
|
||||||
cred,
|
cred,
|
||||||
children,
|
children,
|
||||||
showPadding,
|
showPadding,
|
||||||
|
multiuseColumn,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {expanded} = this.state;
|
const {expanded} = this.state;
|
||||||
const percent =
|
|
||||||
connectionProportion == null
|
|
||||||
? ""
|
|
||||||
: (connectionProportion * 100).toFixed(2) + "%";
|
|
||||||
const backgroundColor = `hsla(150,100%,28%,${1 - 0.9 ** depth})`;
|
const backgroundColor = `hsla(150,100%,28%,${1 - 0.9 ** depth})`;
|
||||||
const makeGradient = (color) =>
|
const makeGradient = (color) =>
|
||||||
`linear-gradient(to top, ${color}, ${color})`;
|
`linear-gradient(to top, ${color}, ${color})`;
|
||||||
|
@ -80,7 +78,7 @@ export class TableRow extends React.PureComponent<
|
||||||
</button>
|
</button>
|
||||||
{description}
|
{description}
|
||||||
</td>
|
</td>
|
||||||
<td style={{textAlign: "right"}}>{percent}</td>
|
<td style={{textAlign: "right"}}>{multiuseColumn}</td>
|
||||||
<td style={{textAlign: "right"}}>
|
<td style={{textAlign: "right"}}>
|
||||||
<span style={{marginRight: 5}}>{credDisplay(cred)}</span>
|
<span style={{marginRight: 5}}>{credDisplay(cred)}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -15,7 +15,7 @@ describe("explorer/pagerankTable/TableRow", () => {
|
||||||
depth={1}
|
depth={1}
|
||||||
indent={1}
|
indent={1}
|
||||||
description={<span data-test-description={true} />}
|
description={<span data-test-description={true} />}
|
||||||
connectionProportion={0.5}
|
multiuseColumn={"50.00%"}
|
||||||
cred={133.7}
|
cred={133.7}
|
||||||
children={<div data-test-children={true} />}
|
children={<div data-test-children={true} />}
|
||||||
showPadding={false}
|
showPadding={false}
|
||||||
|
@ -30,7 +30,7 @@ describe("explorer/pagerankTable/TableRow", () => {
|
||||||
indent={1}
|
indent={1}
|
||||||
showPadding={false}
|
showPadding={false}
|
||||||
description={<span data-test-description={true} />}
|
description={<span data-test-description={true} />}
|
||||||
connectionProportion={0.5}
|
multiuseColumn={"50.00%"}
|
||||||
cred={133.7}
|
cred={133.7}
|
||||||
children={<div data-test-children={true} />}
|
children={<div data-test-children={true} />}
|
||||||
/>
|
/>
|
||||||
|
@ -50,7 +50,7 @@ describe("explorer/pagerankTable/TableRow", () => {
|
||||||
indent={indent}
|
indent={indent}
|
||||||
showPadding={false}
|
showPadding={false}
|
||||||
description={<span data-test-description={true} />}
|
description={<span data-test-description={true} />}
|
||||||
connectionProportion={0.5}
|
multiuseColumn={"50.00%"}
|
||||||
cred={133.7}
|
cred={133.7}
|
||||||
children={<div data-test-children={true} />}
|
children={<div data-test-children={true} />}
|
||||||
/>
|
/>
|
||||||
|
@ -92,7 +92,7 @@ describe("explorer/pagerankTable/TableRow", () => {
|
||||||
const el = example();
|
const el = example();
|
||||||
expect(el.find("td")).toHaveLength(COLUMNS().length);
|
expect(el.find("td")).toHaveLength(COLUMNS().length);
|
||||||
});
|
});
|
||||||
it("displays formatted connectionPercentage in the correct column", () => {
|
it("can display literal text in the multiuseColumn", () => {
|
||||||
const index = COLUMNS().indexOf("");
|
const index = COLUMNS().indexOf("");
|
||||||
expect(index).not.toEqual(-1);
|
expect(index).not.toEqual(-1);
|
||||||
const td = example()
|
const td = example()
|
||||||
|
@ -100,13 +100,14 @@ describe("explorer/pagerankTable/TableRow", () => {
|
||||||
.at(index);
|
.at(index);
|
||||||
expect(td.text()).toEqual("50.00%");
|
expect(td.text()).toEqual("50.00%");
|
||||||
});
|
});
|
||||||
it("displays empty column when connectionProportion not set", () => {
|
it("displays general react nodes in the multiuseColumn", () => {
|
||||||
const index = COLUMNS().indexOf("");
|
const index = COLUMNS().indexOf("");
|
||||||
expect(index).not.toEqual(-1);
|
expect(index).not.toEqual(-1);
|
||||||
const el = example();
|
const el = example();
|
||||||
el.setProps({connectionProportion: null});
|
const multiuseColumn = <span data-test-multiuse={true} />;
|
||||||
|
el.setProps({multiuseColumn});
|
||||||
const td = el.find("td").at(index);
|
const td = el.find("td").at(index);
|
||||||
expect(td.text()).toEqual("");
|
expect(td.find({"data-test-multiuse": true})).toHaveLength(1);
|
||||||
});
|
});
|
||||||
it("displays formatted cred in the correct column", () => {
|
it("displays formatted cred in the correct column", () => {
|
||||||
const index = COLUMNS().indexOf("Cred");
|
const index = COLUMNS().indexOf("Cred");
|
||||||
|
@ -135,7 +136,7 @@ describe("explorer/pagerankTable/TableRow", () => {
|
||||||
depth={2}
|
depth={2}
|
||||||
indent={1}
|
indent={1}
|
||||||
description={<span data-test-description={true} />}
|
description={<span data-test-description={true} />}
|
||||||
connectionProportion={0.5}
|
multiuseColumn={"50.00%"}
|
||||||
cred={133.7}
|
cred={133.7}
|
||||||
children={<div data-test-children={true} />}
|
children={<div data-test-children={true} />}
|
||||||
showPadding={true}
|
showPadding={true}
|
||||||
|
|
|
@ -38,6 +38,8 @@ export type SharedProps = {|
|
||||||
+pnd: PagerankNodeDecomposition,
|
+pnd: PagerankNodeDecomposition,
|
||||||
+adapters: DynamicExplorerAdapterSet,
|
+adapters: DynamicExplorerAdapterSet,
|
||||||
+maxEntriesPerList: number,
|
+maxEntriesPerList: number,
|
||||||
|
+manualWeights: Map<NodeAddressT, number>,
|
||||||
|
+onManualWeightsChange: (NodeAddressT, number) => void,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
export function Badge({children}: {children: ReactNode}): ReactNode {
|
export function Badge({children}: {children: ReactNode}): ReactNode {
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
import {type NodeAddressT} from "../../core/graph";
|
||||||
import {pagerank} from "../../analysis/pagerank";
|
import {pagerank} from "../../analysis/pagerank";
|
||||||
|
import type {WeightedTypes} from "../../analysis/weights";
|
||||||
import {defaultWeightsForAdapterSet} from "../weights/weights";
|
import {defaultWeightsForAdapterSet} from "../weights/weights";
|
||||||
import {dynamicExplorerAdapterSet} from "../../plugins/demo/explorerAdapter";
|
import {dynamicExplorerAdapterSet} from "../../plugins/demo/explorerAdapter";
|
||||||
|
import type {SharedProps} from "./shared";
|
||||||
|
|
||||||
export const COLUMNS = () => ["Description", "", "Cred"];
|
export const COLUMNS = () => ["Description", "", "Cred"];
|
||||||
|
|
||||||
|
@ -14,6 +17,27 @@ export async function example() {
|
||||||
toWeight: 1,
|
toWeight: 1,
|
||||||
froWeight: 1,
|
froWeight: 1,
|
||||||
}));
|
}));
|
||||||
|
const maxEntriesPerList = 123;
|
||||||
|
const manualWeights: Map<NodeAddressT, number> = new Map();
|
||||||
|
const onManualWeightsChange: (NodeAddressT, number) => void = jest.fn();
|
||||||
|
const onWeightedTypesChange: (WeightedTypes) => void = jest.fn();
|
||||||
|
|
||||||
return {adapters, pnd, weightedTypes};
|
const sharedProps: SharedProps = {
|
||||||
|
adapters,
|
||||||
|
pnd,
|
||||||
|
maxEntriesPerList,
|
||||||
|
manualWeights,
|
||||||
|
onManualWeightsChange,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
adapters,
|
||||||
|
pnd,
|
||||||
|
weightedTypes,
|
||||||
|
maxEntriesPerList,
|
||||||
|
sharedProps,
|
||||||
|
manualWeights,
|
||||||
|
onManualWeightsChange,
|
||||||
|
onWeightedTypesChange,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,8 @@ export type PagerankEvaluated = {|
|
||||||
+loading: LoadingState,
|
+loading: LoadingState,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
|
type ManualWeights = Map<NodeAddressT, number>;
|
||||||
|
|
||||||
export function initialState(repoId: RepoId): ReadyToLoadGraph {
|
export function initialState(repoId: RepoId): ReadyToLoadGraph {
|
||||||
return {type: "READY_TO_LOAD_GRAPH", repoId, loading: "NOT_LOADING"};
|
return {type: "READY_TO_LOAD_GRAPH", repoId, loading: "NOT_LOADING"};
|
||||||
}
|
}
|
||||||
|
@ -71,11 +73,12 @@ export function createStateTransitionMachine(
|
||||||
// Exported for testing purposes.
|
// Exported for testing purposes.
|
||||||
export interface StateTransitionMachineInterface {
|
export interface StateTransitionMachineInterface {
|
||||||
+loadGraph: (Assets, StaticExplorerAdapterSet) => Promise<boolean>;
|
+loadGraph: (Assets, StaticExplorerAdapterSet) => Promise<boolean>;
|
||||||
+runPagerank: (WeightedTypes, NodeAddressT) => Promise<void>;
|
+runPagerank: (WeightedTypes, ManualWeights, NodeAddressT) => Promise<void>;
|
||||||
+loadGraphAndRunPagerank: (
|
+loadGraphAndRunPagerank: (
|
||||||
Assets,
|
Assets,
|
||||||
StaticExplorerAdapterSet,
|
StaticExplorerAdapterSet,
|
||||||
WeightedTypes,
|
WeightedTypes,
|
||||||
|
ManualWeights,
|
||||||
NodeAddressT
|
NodeAddressT
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -157,6 +160,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
|
||||||
|
|
||||||
async runPagerank(
|
async runPagerank(
|
||||||
weightedTypes: WeightedTypes,
|
weightedTypes: WeightedTypes,
|
||||||
|
manualWeights: ManualWeights,
|
||||||
totalScoreNodePrefix: NodeAddressT
|
totalScoreNodePrefix: NodeAddressT
|
||||||
) {
|
) {
|
||||||
const state = this.getState();
|
const state = this.getState();
|
||||||
|
@ -177,7 +181,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
|
||||||
try {
|
try {
|
||||||
const pagerankNodeDecomposition = await this.pagerank(
|
const pagerankNodeDecomposition = await this.pagerank(
|
||||||
graph,
|
graph,
|
||||||
weightsToEdgeEvaluator(weightedTypes),
|
weightsToEdgeEvaluator(weightedTypes, manualWeights),
|
||||||
{
|
{
|
||||||
verbose: true,
|
verbose: true,
|
||||||
totalScoreNodePrefix: totalScoreNodePrefix,
|
totalScoreNodePrefix: totalScoreNodePrefix,
|
||||||
|
@ -207,6 +211,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
|
||||||
assets: Assets,
|
assets: Assets,
|
||||||
adapters: StaticExplorerAdapterSet,
|
adapters: StaticExplorerAdapterSet,
|
||||||
weightedTypes: WeightedTypes,
|
weightedTypes: WeightedTypes,
|
||||||
|
manualWeights: ManualWeights,
|
||||||
totalScoreNodePrefix: NodeAddressT
|
totalScoreNodePrefix: NodeAddressT
|
||||||
) {
|
) {
|
||||||
const state = this.getState();
|
const state = this.getState();
|
||||||
|
@ -215,12 +220,20 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
|
||||||
case "READY_TO_LOAD_GRAPH":
|
case "READY_TO_LOAD_GRAPH":
|
||||||
const loadedGraph = await this.loadGraph(assets, adapters);
|
const loadedGraph = await this.loadGraph(assets, adapters);
|
||||||
if (loadedGraph) {
|
if (loadedGraph) {
|
||||||
await this.runPagerank(weightedTypes, totalScoreNodePrefix);
|
await this.runPagerank(
|
||||||
|
weightedTypes,
|
||||||
|
manualWeights,
|
||||||
|
totalScoreNodePrefix
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "READY_TO_RUN_PAGERANK":
|
case "READY_TO_RUN_PAGERANK":
|
||||||
case "PAGERANK_EVALUATED":
|
case "PAGERANK_EVALUATED":
|
||||||
await this.runPagerank(weightedTypes, totalScoreNodePrefix);
|
await this.runPagerank(
|
||||||
|
weightedTypes,
|
||||||
|
manualWeights,
|
||||||
|
totalScoreNodePrefix
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error((type: empty));
|
throw new Error((type: empty));
|
||||||
|
|
|
@ -167,7 +167,7 @@ describe("explorer/state", () => {
|
||||||
const badState = readyToLoadGraph();
|
const badState = readyToLoadGraph();
|
||||||
const {stm} = example(badState);
|
const {stm} = example(badState);
|
||||||
await expect(
|
await expect(
|
||||||
stm.runPagerank(weightedTypes(), NodeAddress.empty)
|
stm.runPagerank(weightedTypes(), new Map(), NodeAddress.empty)
|
||||||
).rejects.toThrow("incorrect state");
|
).rejects.toThrow("incorrect state");
|
||||||
});
|
});
|
||||||
it("can be run when READY_TO_RUN_PAGERANK or PAGERANK_EVALUATED", async () => {
|
it("can be run when READY_TO_RUN_PAGERANK or PAGERANK_EVALUATED", async () => {
|
||||||
|
@ -176,7 +176,7 @@ describe("explorer/state", () => {
|
||||||
const {stm, getState, pagerankMock} = example(g);
|
const {stm, getState, pagerankMock} = example(g);
|
||||||
const pnd = pagerankNodeDecomposition();
|
const pnd = pagerankNodeDecomposition();
|
||||||
pagerankMock.mockResolvedValue(pnd);
|
pagerankMock.mockResolvedValue(pnd);
|
||||||
await stm.runPagerank(weightedTypes(), NodeAddress.empty);
|
await stm.runPagerank(weightedTypes(), new Map(), NodeAddress.empty);
|
||||||
const state = getState();
|
const state = getState();
|
||||||
if (state.type !== "PAGERANK_EVALUATED") {
|
if (state.type !== "PAGERANK_EVALUATED") {
|
||||||
throw new Error("Impossible");
|
throw new Error("Impossible");
|
||||||
|
@ -188,13 +188,13 @@ describe("explorer/state", () => {
|
||||||
it("immediately sets loading status", () => {
|
it("immediately sets loading status", () => {
|
||||||
const {getState, stm} = example(readyToRunPagerank());
|
const {getState, stm} = example(readyToRunPagerank());
|
||||||
expect(loading(getState())).toBe("NOT_LOADING");
|
expect(loading(getState())).toBe("NOT_LOADING");
|
||||||
stm.runPagerank(weightedTypes(), NodeAddress.empty);
|
stm.runPagerank(weightedTypes(), new Map(), NodeAddress.empty);
|
||||||
expect(loading(getState())).toBe("LOADING");
|
expect(loading(getState())).toBe("LOADING");
|
||||||
});
|
});
|
||||||
it("calls pagerank with the totalScoreNodePrefix option", async () => {
|
it("calls pagerank with the totalScoreNodePrefix option", async () => {
|
||||||
const {pagerankMock, stm} = example(readyToRunPagerank());
|
const {pagerankMock, stm} = example(readyToRunPagerank());
|
||||||
const foo = NodeAddress.fromParts(["foo"]);
|
const foo = NodeAddress.fromParts(["foo"]);
|
||||||
await stm.runPagerank(weightedTypes(), foo);
|
await stm.runPagerank(weightedTypes(), new Map(), foo);
|
||||||
const args = pagerankMock.mock.calls[0];
|
const args = pagerankMock.mock.calls[0];
|
||||||
expect(args[2].totalScoreNodePrefix).toBe(foo);
|
expect(args[2].totalScoreNodePrefix).toBe(foo);
|
||||||
});
|
});
|
||||||
|
@ -204,7 +204,7 @@ describe("explorer/state", () => {
|
||||||
// $ExpectFlowError
|
// $ExpectFlowError
|
||||||
console.error = jest.fn();
|
console.error = jest.fn();
|
||||||
pagerankMock.mockRejectedValue(error);
|
pagerankMock.mockRejectedValue(error);
|
||||||
await stm.runPagerank(weightedTypes(), NodeAddress.empty);
|
await stm.runPagerank(weightedTypes(), new Map(), NodeAddress.empty);
|
||||||
const state = getState();
|
const state = getState();
|
||||||
expect(loading(state)).toBe("FAILED");
|
expect(loading(state)).toBe("FAILED");
|
||||||
expect(state.type).toBe("READY_TO_RUN_PAGERANK");
|
expect(state.type).toBe("READY_TO_RUN_PAGERANK");
|
||||||
|
@ -222,12 +222,19 @@ describe("explorer/state", () => {
|
||||||
const assets = new Assets("/gateway/");
|
const assets = new Assets("/gateway/");
|
||||||
const adapters = new StaticExplorerAdapterSet([]);
|
const adapters = new StaticExplorerAdapterSet([]);
|
||||||
const prefix = NodeAddress.fromParts(["bar"]);
|
const prefix = NodeAddress.fromParts(["bar"]);
|
||||||
|
const manualWeights = new Map();
|
||||||
const wt = weightedTypes();
|
const wt = weightedTypes();
|
||||||
await stm.loadGraphAndRunPagerank(assets, adapters, wt, prefix);
|
await stm.loadGraphAndRunPagerank(
|
||||||
|
assets,
|
||||||
|
adapters,
|
||||||
|
wt,
|
||||||
|
manualWeights,
|
||||||
|
prefix
|
||||||
|
);
|
||||||
expect(stm.loadGraph).toHaveBeenCalledTimes(1);
|
expect(stm.loadGraph).toHaveBeenCalledTimes(1);
|
||||||
expect(stm.loadGraph).toHaveBeenCalledWith(assets, adapters);
|
expect(stm.loadGraph).toHaveBeenCalledWith(assets, adapters);
|
||||||
expect(stm.runPagerank).toHaveBeenCalledTimes(1);
|
expect(stm.runPagerank).toHaveBeenCalledTimes(1);
|
||||||
expect(stm.runPagerank).toHaveBeenCalledWith(wt, prefix);
|
expect(stm.runPagerank).toHaveBeenCalledWith(wt, manualWeights, prefix);
|
||||||
});
|
});
|
||||||
it("does not run pagerank if loadGraph did not succeed", async () => {
|
it("does not run pagerank if loadGraph did not succeed", async () => {
|
||||||
const {stm} = example(readyToLoadGraph());
|
const {stm} = example(readyToLoadGraph());
|
||||||
|
@ -241,6 +248,7 @@ describe("explorer/state", () => {
|
||||||
assets,
|
assets,
|
||||||
adapters,
|
adapters,
|
||||||
weightedTypes(),
|
weightedTypes(),
|
||||||
|
new Map(),
|
||||||
prefix
|
prefix
|
||||||
);
|
);
|
||||||
expect(stm.loadGraph).toHaveBeenCalledTimes(1);
|
expect(stm.loadGraph).toHaveBeenCalledTimes(1);
|
||||||
|
@ -252,15 +260,17 @@ describe("explorer/state", () => {
|
||||||
(stm: any).runPagerank = jest.fn();
|
(stm: any).runPagerank = jest.fn();
|
||||||
const prefix = NodeAddress.fromParts(["bar"]);
|
const prefix = NodeAddress.fromParts(["bar"]);
|
||||||
const wt = weightedTypes();
|
const wt = weightedTypes();
|
||||||
|
const manualWeights = new Map();
|
||||||
await stm.loadGraphAndRunPagerank(
|
await stm.loadGraphAndRunPagerank(
|
||||||
new Assets("/gateway/"),
|
new Assets("/gateway/"),
|
||||||
new StaticExplorerAdapterSet([]),
|
new StaticExplorerAdapterSet([]),
|
||||||
wt,
|
wt,
|
||||||
|
manualWeights,
|
||||||
prefix
|
prefix
|
||||||
);
|
);
|
||||||
expect(stm.loadGraph).toHaveBeenCalledTimes(0);
|
expect(stm.loadGraph).toHaveBeenCalledTimes(0);
|
||||||
expect(stm.runPagerank).toHaveBeenCalledTimes(1);
|
expect(stm.runPagerank).toHaveBeenCalledTimes(1);
|
||||||
expect(stm.runPagerank).toHaveBeenCalledWith(wt, prefix);
|
expect(stm.runPagerank).toHaveBeenCalledWith(wt, manualWeights, prefix);
|
||||||
});
|
});
|
||||||
it("when PAGERANK_EVALUATED, runs pagerank", async () => {
|
it("when PAGERANK_EVALUATED, runs pagerank", async () => {
|
||||||
const {stm} = example(pagerankEvaluated());
|
const {stm} = example(pagerankEvaluated());
|
||||||
|
@ -268,15 +278,17 @@ describe("explorer/state", () => {
|
||||||
(stm: any).runPagerank = jest.fn();
|
(stm: any).runPagerank = jest.fn();
|
||||||
const prefix = NodeAddress.fromParts(["bar"]);
|
const prefix = NodeAddress.fromParts(["bar"]);
|
||||||
const wt = weightedTypes();
|
const wt = weightedTypes();
|
||||||
|
const manualWeights = new Map();
|
||||||
await stm.loadGraphAndRunPagerank(
|
await stm.loadGraphAndRunPagerank(
|
||||||
new Assets("/gateway/"),
|
new Assets("/gateway/"),
|
||||||
new StaticExplorerAdapterSet([]),
|
new StaticExplorerAdapterSet([]),
|
||||||
wt,
|
wt,
|
||||||
|
manualWeights,
|
||||||
prefix
|
prefix
|
||||||
);
|
);
|
||||||
expect(stm.loadGraph).toHaveBeenCalledTimes(0);
|
expect(stm.loadGraph).toHaveBeenCalledTimes(0);
|
||||||
expect(stm.runPagerank).toHaveBeenCalledTimes(1);
|
expect(stm.runPagerank).toHaveBeenCalledTimes(1);
|
||||||
expect(stm.runPagerank).toHaveBeenCalledWith(wt, prefix);
|
expect(stm.runPagerank).toHaveBeenCalledWith(wt, manualWeights, prefix);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue