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]
|
||||
|
||||
- Allow tweaking weights on a per-node basis (#1143)
|
||||
- Add the `pagerank` command (#1114)
|
||||
- Add the `clear` command (#1111)
|
||||
- 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>,
|
||||
|};
|
||||
|
||||
export type ManualWeights = Map<NodeAddressT, number>;
|
||||
|
||||
export function defaultWeightedNodeType(type: NodeType): WeightedNodeType {
|
||||
return {
|
||||
type,
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
// @flow
|
||||
|
||||
import type {Edge} from "../core/graph";
|
||||
import type {WeightedTypes} from "./weights";
|
||||
import * as NullUtil from "../util/null";
|
||||
import type {Edge, NodeAddressT} from "../core/graph";
|
||||
import type {WeightedTypes, ManualWeights} from "./weights";
|
||||
import type {EdgeEvaluator} from "./pagerank";
|
||||
import {NodeTrie, EdgeTrie} from "../core/trie";
|
||||
|
||||
export function weightsToEdgeEvaluator(weights: WeightedTypes): EdgeEvaluator {
|
||||
export function weightsToEdgeEvaluator(
|
||||
weights: WeightedTypes,
|
||||
manualWeights: ManualWeights
|
||||
): EdgeEvaluator {
|
||||
const nodeTrie = new NodeTrie();
|
||||
for (const {type, weight} of weights.nodes.values()) {
|
||||
nodeTrie.add(type.prefix, weight);
|
||||
|
@ -15,9 +19,15 @@ export function weightsToEdgeEvaluator(weights: WeightedTypes): EdgeEvaluator {
|
|||
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) {
|
||||
const srcWeight = nodeTrie.getLast(edge.src);
|
||||
const dstWeight = nodeTrie.getLast(edge.dst);
|
||||
const srcWeight = nodeWeight(edge.src);
|
||||
const dstWeight = nodeWeight(edge.dst);
|
||||
const {forwardWeight, backwardWeight} = edgeTrie.getLast(edge.address);
|
||||
return {
|
||||
toWeight: dstWeight * forwardWeight,
|
||||
|
|
|
@ -7,7 +7,11 @@ import {
|
|||
machineNodeType,
|
||||
assemblesEdgeType,
|
||||
} 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";
|
||||
|
||||
describe("analysis/weightsToEdgeEvaluator", () => {
|
||||
|
@ -20,6 +24,7 @@ describe("analysis/weightsToEdgeEvaluator", () => {
|
|||
+inserter?: number,
|
||||
+machine?: number,
|
||||
+baseNode?: number,
|
||||
+manualWeights?: ManualWeights,
|
||||
|};
|
||||
function weights({
|
||||
assemblesForward,
|
||||
|
@ -53,7 +58,8 @@ describe("analysis/weightsToEdgeEvaluator", () => {
|
|||
}
|
||||
function exampleEdgeWeights(weightArgs: 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
|
||||
return ee(factorioEdges.assembles1);
|
||||
}
|
||||
|
@ -91,5 +97,22 @@ describe("analysis/weightsToEdgeEvaluator", () => {
|
|||
})
|
||||
).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 {
|
||||
type WeightedTypes,
|
||||
type ManualWeights,
|
||||
combineWeights,
|
||||
defaultWeightsForDeclaration,
|
||||
} from "../analysis/weights";
|
||||
|
@ -144,10 +145,11 @@ export function makePagerankCommand(
|
|||
}
|
||||
|
||||
export async function runPagerank(
|
||||
weights: WeightedTypes,
|
||||
typeWeights: WeightedTypes,
|
||||
manualWeights: ManualWeights,
|
||||
graph: Graph
|
||||
): Promise<PagerankGraph> {
|
||||
const evaluator = weightsToEdgeEvaluator(weights);
|
||||
const evaluator = weightsToEdgeEvaluator(typeWeights, manualWeights);
|
||||
const pagerankGraph = new PagerankGraph(
|
||||
graph,
|
||||
evaluator,
|
||||
|
@ -187,7 +189,8 @@ export const defaultAdapters = () => [
|
|||
const defaultLoader = (r: RepoId) =>
|
||||
loadGraph(Common.sourcecredDirectory(), defaultAdapters(), r);
|
||||
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) =>
|
||||
savePagerankGraph(Common.sourcecredDirectory(), r, pg);
|
||||
|
||||
|
|
|
@ -230,11 +230,17 @@ describe("cli/pagerank", () => {
|
|||
fallbackWeightedTypes,
|
||||
]);
|
||||
|
||||
const manualWeights = new Map();
|
||||
manualWeights.set(advancedGraph().nodes.src(), 4);
|
||||
const graph = advancedGraph().graph1();
|
||||
const actualPagerankGraph = await runPagerank(weightedTypes, graph);
|
||||
const actualPagerankGraph = await runPagerank(
|
||||
weightedTypes,
|
||||
manualWeights,
|
||||
graph
|
||||
);
|
||||
const expectedPagerankGraph = new PagerankGraph(
|
||||
graph,
|
||||
weightsToEdgeEvaluator(weightedTypes),
|
||||
weightsToEdgeEvaluator(weightedTypes, manualWeights),
|
||||
DEFAULT_SYNTHETIC_LOOP_WEIGHT
|
||||
);
|
||||
await expectedPagerankGraph.runPagerank({
|
||||
|
|
|
@ -8,6 +8,7 @@ import CheckedLocalStore from "../webutil/checkedLocalStore";
|
|||
import BrowserLocalStore from "../webutil/browserLocalStore";
|
||||
import Link from "../webutil/Link";
|
||||
import type {RepoId} from "../core/repoId";
|
||||
import {type NodeAddressT} from "../core/graph";
|
||||
|
||||
import {PagerankTable} from "./pagerankTable/Table";
|
||||
import type {WeightedTypes} from "../analysis/weights";
|
||||
|
@ -61,6 +62,7 @@ type Props = {|
|
|||
type State = {|
|
||||
appState: AppState,
|
||||
weightedTypes: WeightedTypes,
|
||||
manualWeights: Map<NodeAddressT, number>,
|
||||
|};
|
||||
|
||||
export function createApp(
|
||||
|
@ -77,6 +79,7 @@ export function createApp(
|
|||
this.state = {
|
||||
appState: initialState(this.props.repoId),
|
||||
weightedTypes: defaultWeightsForAdapterSet(props.adapters),
|
||||
manualWeights: new Map(),
|
||||
};
|
||||
this.stateTransitionMachine = createSTM(
|
||||
() => this.state.appState,
|
||||
|
@ -98,6 +101,13 @@ export function createApp(
|
|||
onWeightedTypesChange={(weightedTypes) =>
|
||||
this.setState({weightedTypes})
|
||||
}
|
||||
manualWeights={this.state.manualWeights}
|
||||
onManualWeightsChange={(addr: NodeAddressT, weight: number) =>
|
||||
this.setState(({manualWeights}) => {
|
||||
manualWeights.set(addr, weight);
|
||||
return {manualWeights};
|
||||
})
|
||||
}
|
||||
pnd={pnd}
|
||||
maxEntriesPerList={100}
|
||||
/>
|
||||
|
@ -124,6 +134,7 @@ export function createApp(
|
|||
this.props.assets,
|
||||
this.props.adapters,
|
||||
this.state.weightedTypes,
|
||||
this.state.manualWeights,
|
||||
GithubPrefix.user
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import React from "react";
|
||||
import {shallow} from "enzyme";
|
||||
|
||||
import {Graph} from "../core/graph";
|
||||
import {Graph, NodeAddress} from "../core/graph";
|
||||
import {makeRepoId} from "../core/repoId";
|
||||
import {Assets} from "../webutil/assets";
|
||||
import testLocalStore from "../webutil/testLocalStore";
|
||||
|
@ -153,6 +153,7 @@ describe("explorer/App", () => {
|
|||
el.instance().props.assets,
|
||||
el.instance().props.adapters,
|
||||
el.instance().state.weightedTypes,
|
||||
el.instance().state.manualWeights,
|
||||
GithubPrefix.user
|
||||
);
|
||||
}
|
||||
|
@ -184,6 +185,10 @@ describe("explorer/App", () => {
|
|||
);
|
||||
prtWeightedTypesChange(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 {
|
||||
expect(prt).toHaveLength(0);
|
||||
}
|
||||
|
|
|
@ -59,13 +59,14 @@ export class AggregationRow extends React.PureComponent<AggregationRowProps> {
|
|||
const score = aggregation.summary.score;
|
||||
const {score: targetScore} = NullUtil.get(pnd.get(target));
|
||||
const connectionProportion = score / targetScore;
|
||||
const connectionPercent = (connectionProportion * 100).toFixed(2) + "%";
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
depth={depth}
|
||||
indent={1}
|
||||
showPadding={false}
|
||||
connectionProportion={connectionProportion}
|
||||
multiuseColumn={connectionPercent}
|
||||
cred={score}
|
||||
description={<AggregationView aggregation={aggregation} />}
|
||||
>
|
||||
|
|
|
@ -23,11 +23,9 @@ require("../../webutil/testUtil").configureEnzyme();
|
|||
describe("explorer/pagerankTable/Aggregation", () => {
|
||||
describe("AggregationRowList", () => {
|
||||
it("instantiates AggregationRows for each aggregation", async () => {
|
||||
const {adapters, pnd} = await example();
|
||||
const {adapters, pnd, sharedProps} = await example();
|
||||
const node = factorioNodes.inserter1;
|
||||
const depth = 20;
|
||||
const maxEntriesPerList = 50;
|
||||
const sharedProps = {adapters, pnd, maxEntriesPerList};
|
||||
const connections = NullUtil.get(pnd.get(node)).scoredConnections;
|
||||
const aggregations = aggregateFlat(
|
||||
connections,
|
||||
|
@ -58,8 +56,7 @@ describe("explorer/pagerankTable/Aggregation", () => {
|
|||
|
||||
describe("AggregationRow", () => {
|
||||
async function setup() {
|
||||
const {pnd, adapters} = await example();
|
||||
const sharedProps = {adapters, pnd, maxEntriesPerList: 123};
|
||||
const {pnd, adapters, sharedProps} = await example();
|
||||
const target = factorioNodes.inserter1;
|
||||
const {scoredConnections} = NullUtil.get(pnd.get(target));
|
||||
const aggregations = aggregateFlat(
|
||||
|
@ -105,12 +102,12 @@ describe("explorer/pagerankTable/Aggregation", () => {
|
|||
const {row, aggregation} = await setup();
|
||||
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 targetScore = NullUtil.get(sharedProps.pnd.get(target)).score;
|
||||
expect(row.props().connectionProportion).toBe(
|
||||
aggregation.summary.score / targetScore
|
||||
);
|
||||
const expectedPercent =
|
||||
((aggregation.summary.score * 100) / targetScore).toFixed(2) + "%";
|
||||
expect(row.props().multiuseColumn).toBe(expectedPercent);
|
||||
});
|
||||
it("with a AggregationView as description", async () => {
|
||||
const {row, aggregation} = await setup();
|
||||
|
|
|
@ -61,6 +61,7 @@ export class ConnectionRow extends React.PureComponent<ConnectionRowProps> {
|
|||
const {pnd, adapters} = sharedProps;
|
||||
const {score: targetScore} = NullUtil.get(pnd.get(target));
|
||||
const connectionProportion = connectionScore / targetScore;
|
||||
const connectionPercent = (connectionProportion * 100).toFixed(2) + "%";
|
||||
|
||||
const connectionView = (
|
||||
<ConnectionView connection={connection} adapters={adapters} />
|
||||
|
@ -70,7 +71,7 @@ export class ConnectionRow extends React.PureComponent<ConnectionRowProps> {
|
|||
indent={2}
|
||||
depth={depth}
|
||||
description={connectionView}
|
||||
connectionProportion={connectionProportion}
|
||||
multiuseColumn={connectionPercent}
|
||||
showPadding={false}
|
||||
cred={connectionScore}
|
||||
>
|
||||
|
|
|
@ -15,11 +15,11 @@ require("../../webutil/testUtil").configureEnzyme();
|
|||
|
||||
describe("explorer/pagerankTable/Connection", () => {
|
||||
describe("ConnectionRowList", () => {
|
||||
async function setup(maxEntriesPerList: number = 100000) {
|
||||
const {adapters, pnd} = await example();
|
||||
async function setup(maxEntriesPerList: number = 123) {
|
||||
let {sharedProps} = await example();
|
||||
sharedProps = {...sharedProps, maxEntriesPerList};
|
||||
const depth = 2;
|
||||
const node = factorioNodes.inserter1;
|
||||
const sharedProps = {adapters, pnd, maxEntriesPerList};
|
||||
const connections = NullUtil.get(sharedProps.pnd.get(node))
|
||||
.scoredConnections;
|
||||
const component = (
|
||||
|
@ -66,8 +66,7 @@ describe("explorer/pagerankTable/Connection", () => {
|
|||
|
||||
describe("ConnectionRow", () => {
|
||||
async function setup() {
|
||||
const {pnd, adapters} = await example();
|
||||
const sharedProps = {adapters, pnd, maxEntriesPerList: 123};
|
||||
const {pnd, sharedProps} = await example();
|
||||
const target = factorioNodes.inserter1;
|
||||
const {scoredConnections} = NullUtil.get(pnd.get(target));
|
||||
const scoredConnection = scoredConnections[0];
|
||||
|
@ -100,12 +99,13 @@ describe("explorer/pagerankTable/Connection", () => {
|
|||
const {row, scoredConnection} = await setup();
|
||||
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 targetScore = NullUtil.get(sharedProps.pnd.get(target)).score;
|
||||
expect(row.props().connectionProportion).toBe(
|
||||
scoredConnection.connectionScore / targetScore
|
||||
);
|
||||
const expectedPercent =
|
||||
((scoredConnection.connectionScore * 100) / targetScore).toFixed(2) +
|
||||
"%";
|
||||
expect(row.props().multiuseColumn).toBe(expectedPercent);
|
||||
});
|
||||
it("with a ConnectionView as description", async () => {
|
||||
const {row, sharedProps, scoredConnection} = await setup();
|
||||
|
|
|
@ -6,6 +6,13 @@ import * as NullUtil from "../../util/null";
|
|||
|
||||
import {type NodeAddressT} from "../../core/graph";
|
||||
import {TableRow} from "./TableRow";
|
||||
import {
|
||||
MIN_SLIDER,
|
||||
MAX_SLIDER,
|
||||
formatWeight,
|
||||
sliderToWeight,
|
||||
weightToSlider,
|
||||
} from "../weights/WeightSlider";
|
||||
|
||||
import {nodeDescription, type SharedProps} from "./shared";
|
||||
|
||||
|
@ -48,8 +55,25 @@ export type NodeRowProps = {|
|
|||
export class NodeRow extends React.PureComponent<NodeRowProps> {
|
||||
render() {
|
||||
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 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>;
|
||||
return (
|
||||
<TableRow
|
||||
|
@ -57,7 +81,7 @@ export class NodeRow extends React.PureComponent<NodeRowProps> {
|
|||
indent={0}
|
||||
showPadding={showPadding}
|
||||
description={description}
|
||||
connectionProportion={null}
|
||||
multiuseColumn={slider}
|
||||
cred={score}
|
||||
>
|
||||
<AggregationRowList
|
||||
|
|
|
@ -6,6 +6,12 @@ import sortBy from "lodash.sortby";
|
|||
import * as NullUtil from "../../util/null";
|
||||
import {TableRow} from "./TableRow";
|
||||
import {AggregationRowList} from "./Aggregation";
|
||||
import {
|
||||
MIN_SLIDER,
|
||||
MAX_SLIDER,
|
||||
sliderToWeight,
|
||||
weightToSlider,
|
||||
} from "../weights/WeightSlider";
|
||||
|
||||
import type {NodeAddressT} from "../../core/graph";
|
||||
|
||||
|
@ -21,11 +27,11 @@ describe("explorer/pagerankTable/Node", () => {
|
|||
function sortedByScore(nodes: $ReadOnlyArray<NodeAddressT>, pnd) {
|
||||
return sortBy(nodes, (node) => -NullUtil.get(pnd.get(node)).score);
|
||||
}
|
||||
async function setup(maxEntriesPerList: number = 100000) {
|
||||
const {adapters, pnd} = await example();
|
||||
async function setup(maxEntriesPerList: number = 123) {
|
||||
const {adapters, pnd, sharedProps: changeEntries} = await example();
|
||||
const sharedProps = {...changeEntries, maxEntriesPerList};
|
||||
const nodes = Array.from(pnd.keys());
|
||||
expect(nodes).not.toHaveLength(0);
|
||||
const sharedProps = {adapters, pnd, maxEntriesPerList};
|
||||
const component = <NodeRowList sharedProps={sharedProps} nodes={nodes} />;
|
||||
const element = shallow(component);
|
||||
return {element, adapters, sharedProps, nodes};
|
||||
|
@ -71,15 +77,17 @@ describe("explorer/pagerankTable/Node", () => {
|
|||
describe("NodeRow", () => {
|
||||
async function setup(props: $Shape<{...NodeRowProps}>) {
|
||||
props = props || {};
|
||||
const {pnd, adapters} = await example();
|
||||
const sharedProps = {adapters, pnd, maxEntriesPerList: 123};
|
||||
let {sharedProps} = await example();
|
||||
if (props.sharedProps !== null) {
|
||||
sharedProps = {...sharedProps, ...props.sharedProps};
|
||||
}
|
||||
const node = factorioNodes.inserter1;
|
||||
const component = shallow(
|
||||
<NodeRow
|
||||
node={NullUtil.orElse(props.node, node)}
|
||||
showPadding={NullUtil.orElse(props.showPadding, false)}
|
||||
depth={NullUtil.orElse(props.depth, 0)}
|
||||
sharedProps={NullUtil.orElse(props.sharedProps, sharedProps)}
|
||||
sharedProps={sharedProps}
|
||||
/>
|
||||
);
|
||||
const row = component.find(TableRow);
|
||||
|
@ -103,9 +111,58 @@ describe("explorer/pagerankTable/Node", () => {
|
|||
const score = NullUtil.get(sharedProps.pnd.get(node)).score;
|
||||
expect(row.props().cred).toBe(score);
|
||||
});
|
||||
it("with no connectionProportion", async () => {
|
||||
const {row} = await setup();
|
||||
expect(row.props().connectionProportion).not.toEqual(expect.anything());
|
||||
describe("with a weight slider", () => {
|
||||
async function setupSlider(initialWeight: number = 0) {
|
||||
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 () => {
|
||||
const {row, sharedProps, node} = await setup();
|
||||
|
|
|
@ -4,7 +4,7 @@ import React from "react";
|
|||
import sortBy from "lodash.sortby";
|
||||
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 {DynamicExplorerAdapterSet} from "../adapters/explorerAdapterSet";
|
||||
import type {DynamicExplorerAdapter} from "../adapters/explorerAdapter";
|
||||
|
@ -22,6 +22,8 @@ type PagerankTableProps = {|
|
|||
+onWeightedTypesChange: (WeightedTypes) => void,
|
||||
+maxEntriesPerList: number,
|
||||
+defaultNodeType: ?NodeType,
|
||||
+manualWeights: Map<NodeAddressT, number>,
|
||||
+onManualWeightsChange: (NodeAddressT, number) => void,
|
||||
|};
|
||||
type PagerankTableState = {|
|
||||
selectedNodeType: NodeType,
|
||||
|
@ -128,12 +130,24 @@ export class PagerankTable extends React.PureComponent<
|
|||
}
|
||||
|
||||
renderTable() {
|
||||
const {pnd, adapters, maxEntriesPerList} = this.props;
|
||||
const {
|
||||
pnd,
|
||||
adapters,
|
||||
maxEntriesPerList,
|
||||
manualWeights,
|
||||
onManualWeightsChange,
|
||||
} = this.props;
|
||||
if (pnd == null || adapters == null || maxEntriesPerList == null) {
|
||||
throw new Error("Impossible.");
|
||||
}
|
||||
const selectedNodeTypePrefix = this.state.selectedNodeType.prefix;
|
||||
const sharedProps = {pnd, adapters, maxEntriesPerList};
|
||||
const sharedProps = {
|
||||
pnd,
|
||||
adapters,
|
||||
maxEntriesPerList,
|
||||
manualWeights,
|
||||
onManualWeightsChange,
|
||||
};
|
||||
return (
|
||||
<table
|
||||
style={{
|
||||
|
|
|
@ -18,9 +18,16 @@ require("../../webutil/testUtil").configureEnzyme();
|
|||
describe("explorer/pagerankTable/Table", () => {
|
||||
describe("PagerankTable", () => {
|
||||
async function setup(defaultNodeType?: NodeType) {
|
||||
const {pnd, adapters, weightedTypes} = await example();
|
||||
const onWeightedTypesChange = jest.fn();
|
||||
const maxEntriesPerList = 321;
|
||||
const {
|
||||
pnd,
|
||||
adapters,
|
||||
weightedTypes,
|
||||
sharedProps,
|
||||
manualWeights,
|
||||
onManualWeightsChange,
|
||||
onWeightedTypesChange,
|
||||
maxEntriesPerList,
|
||||
} = await example();
|
||||
const element = shallow(
|
||||
<PagerankTable
|
||||
defaultNodeType={defaultNodeType}
|
||||
|
@ -29,9 +36,20 @@ describe("explorer/pagerankTable/Table", () => {
|
|||
pnd={pnd}
|
||||
adapters={adapters}
|
||||
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 () => {
|
||||
const {element} = await setup();
|
||||
|
@ -145,10 +163,9 @@ describe("explorer/pagerankTable/Table", () => {
|
|||
|
||||
describe("creates a NodeRowList", () => {
|
||||
it("with the correct SharedProps", async () => {
|
||||
const {element, adapters, pnd, maxEntriesPerList} = await setup();
|
||||
const {element, sharedProps} = await setup();
|
||||
const nrl = element.find(NodeRowList);
|
||||
const expectedSharedProps = {adapters, pnd, maxEntriesPerList};
|
||||
expect(nrl.props().sharedProps).toEqual(expectedSharedProps);
|
||||
expect(nrl.props().sharedProps).toEqual(sharedProps);
|
||||
});
|
||||
it("including all nodes by default", async () => {
|
||||
const {element, pnd} = await setup();
|
||||
|
|
|
@ -10,8 +10,10 @@ type TableRowProps = {|
|
|||
+indent: number,
|
||||
// The node that goes in the Description column
|
||||
+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
|
||||
+cred: number,
|
||||
// Children to show when the row is expanded
|
||||
|
@ -39,16 +41,12 @@ export class TableRow extends React.PureComponent<
|
|||
depth,
|
||||
indent,
|
||||
description,
|
||||
connectionProportion,
|
||||
cred,
|
||||
children,
|
||||
showPadding,
|
||||
multiuseColumn,
|
||||
} = this.props;
|
||||
const {expanded} = this.state;
|
||||
const percent =
|
||||
connectionProportion == null
|
||||
? ""
|
||||
: (connectionProportion * 100).toFixed(2) + "%";
|
||||
const backgroundColor = `hsla(150,100%,28%,${1 - 0.9 ** depth})`;
|
||||
const makeGradient = (color) =>
|
||||
`linear-gradient(to top, ${color}, ${color})`;
|
||||
|
@ -80,7 +78,7 @@ export class TableRow extends React.PureComponent<
|
|||
</button>
|
||||
{description}
|
||||
</td>
|
||||
<td style={{textAlign: "right"}}>{percent}</td>
|
||||
<td style={{textAlign: "right"}}>{multiuseColumn}</td>
|
||||
<td style={{textAlign: "right"}}>
|
||||
<span style={{marginRight: 5}}>{credDisplay(cred)}</span>
|
||||
</td>
|
||||
|
|
|
@ -15,7 +15,7 @@ describe("explorer/pagerankTable/TableRow", () => {
|
|||
depth={1}
|
||||
indent={1}
|
||||
description={<span data-test-description={true} />}
|
||||
connectionProportion={0.5}
|
||||
multiuseColumn={"50.00%"}
|
||||
cred={133.7}
|
||||
children={<div data-test-children={true} />}
|
||||
showPadding={false}
|
||||
|
@ -30,7 +30,7 @@ describe("explorer/pagerankTable/TableRow", () => {
|
|||
indent={1}
|
||||
showPadding={false}
|
||||
description={<span data-test-description={true} />}
|
||||
connectionProportion={0.5}
|
||||
multiuseColumn={"50.00%"}
|
||||
cred={133.7}
|
||||
children={<div data-test-children={true} />}
|
||||
/>
|
||||
|
@ -50,7 +50,7 @@ describe("explorer/pagerankTable/TableRow", () => {
|
|||
indent={indent}
|
||||
showPadding={false}
|
||||
description={<span data-test-description={true} />}
|
||||
connectionProportion={0.5}
|
||||
multiuseColumn={"50.00%"}
|
||||
cred={133.7}
|
||||
children={<div data-test-children={true} />}
|
||||
/>
|
||||
|
@ -92,7 +92,7 @@ describe("explorer/pagerankTable/TableRow", () => {
|
|||
const el = example();
|
||||
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("");
|
||||
expect(index).not.toEqual(-1);
|
||||
const td = example()
|
||||
|
@ -100,13 +100,14 @@ describe("explorer/pagerankTable/TableRow", () => {
|
|||
.at(index);
|
||||
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("");
|
||||
expect(index).not.toEqual(-1);
|
||||
const el = example();
|
||||
el.setProps({connectionProportion: null});
|
||||
const multiuseColumn = <span data-test-multiuse={true} />;
|
||||
el.setProps({multiuseColumn});
|
||||
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", () => {
|
||||
const index = COLUMNS().indexOf("Cred");
|
||||
|
@ -135,7 +136,7 @@ describe("explorer/pagerankTable/TableRow", () => {
|
|||
depth={2}
|
||||
indent={1}
|
||||
description={<span data-test-description={true} />}
|
||||
connectionProportion={0.5}
|
||||
multiuseColumn={"50.00%"}
|
||||
cred={133.7}
|
||||
children={<div data-test-children={true} />}
|
||||
showPadding={true}
|
||||
|
|
|
@ -38,6 +38,8 @@ export type SharedProps = {|
|
|||
+pnd: PagerankNodeDecomposition,
|
||||
+adapters: DynamicExplorerAdapterSet,
|
||||
+maxEntriesPerList: number,
|
||||
+manualWeights: Map<NodeAddressT, number>,
|
||||
+onManualWeightsChange: (NodeAddressT, number) => void,
|
||||
|};
|
||||
|
||||
export function Badge({children}: {children: ReactNode}): ReactNode {
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
// @flow
|
||||
|
||||
import {type NodeAddressT} from "../../core/graph";
|
||||
import {pagerank} from "../../analysis/pagerank";
|
||||
import type {WeightedTypes} from "../../analysis/weights";
|
||||
import {defaultWeightsForAdapterSet} from "../weights/weights";
|
||||
import {dynamicExplorerAdapterSet} from "../../plugins/demo/explorerAdapter";
|
||||
import type {SharedProps} from "./shared";
|
||||
|
||||
export const COLUMNS = () => ["Description", "", "Cred"];
|
||||
|
||||
|
@ -14,6 +17,27 @@ export async function example() {
|
|||
toWeight: 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,
|
||||
|};
|
||||
|
||||
type ManualWeights = Map<NodeAddressT, number>;
|
||||
|
||||
export function initialState(repoId: RepoId): ReadyToLoadGraph {
|
||||
return {type: "READY_TO_LOAD_GRAPH", repoId, loading: "NOT_LOADING"};
|
||||
}
|
||||
|
@ -71,11 +73,12 @@ export function createStateTransitionMachine(
|
|||
// Exported for testing purposes.
|
||||
export interface StateTransitionMachineInterface {
|
||||
+loadGraph: (Assets, StaticExplorerAdapterSet) => Promise<boolean>;
|
||||
+runPagerank: (WeightedTypes, NodeAddressT) => Promise<void>;
|
||||
+runPagerank: (WeightedTypes, ManualWeights, NodeAddressT) => Promise<void>;
|
||||
+loadGraphAndRunPagerank: (
|
||||
Assets,
|
||||
StaticExplorerAdapterSet,
|
||||
WeightedTypes,
|
||||
ManualWeights,
|
||||
NodeAddressT
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
@ -157,6 +160,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
|
|||
|
||||
async runPagerank(
|
||||
weightedTypes: WeightedTypes,
|
||||
manualWeights: ManualWeights,
|
||||
totalScoreNodePrefix: NodeAddressT
|
||||
) {
|
||||
const state = this.getState();
|
||||
|
@ -177,7 +181,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
|
|||
try {
|
||||
const pagerankNodeDecomposition = await this.pagerank(
|
||||
graph,
|
||||
weightsToEdgeEvaluator(weightedTypes),
|
||||
weightsToEdgeEvaluator(weightedTypes, manualWeights),
|
||||
{
|
||||
verbose: true,
|
||||
totalScoreNodePrefix: totalScoreNodePrefix,
|
||||
|
@ -207,6 +211,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
|
|||
assets: Assets,
|
||||
adapters: StaticExplorerAdapterSet,
|
||||
weightedTypes: WeightedTypes,
|
||||
manualWeights: ManualWeights,
|
||||
totalScoreNodePrefix: NodeAddressT
|
||||
) {
|
||||
const state = this.getState();
|
||||
|
@ -215,12 +220,20 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
|
|||
case "READY_TO_LOAD_GRAPH":
|
||||
const loadedGraph = await this.loadGraph(assets, adapters);
|
||||
if (loadedGraph) {
|
||||
await this.runPagerank(weightedTypes, totalScoreNodePrefix);
|
||||
await this.runPagerank(
|
||||
weightedTypes,
|
||||
manualWeights,
|
||||
totalScoreNodePrefix
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "READY_TO_RUN_PAGERANK":
|
||||
case "PAGERANK_EVALUATED":
|
||||
await this.runPagerank(weightedTypes, totalScoreNodePrefix);
|
||||
await this.runPagerank(
|
||||
weightedTypes,
|
||||
manualWeights,
|
||||
totalScoreNodePrefix
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error((type: empty));
|
||||
|
|
|
@ -167,7 +167,7 @@ describe("explorer/state", () => {
|
|||
const badState = readyToLoadGraph();
|
||||
const {stm} = example(badState);
|
||||
await expect(
|
||||
stm.runPagerank(weightedTypes(), NodeAddress.empty)
|
||||
stm.runPagerank(weightedTypes(), new Map(), NodeAddress.empty)
|
||||
).rejects.toThrow("incorrect state");
|
||||
});
|
||||
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 pnd = pagerankNodeDecomposition();
|
||||
pagerankMock.mockResolvedValue(pnd);
|
||||
await stm.runPagerank(weightedTypes(), NodeAddress.empty);
|
||||
await stm.runPagerank(weightedTypes(), new Map(), NodeAddress.empty);
|
||||
const state = getState();
|
||||
if (state.type !== "PAGERANK_EVALUATED") {
|
||||
throw new Error("Impossible");
|
||||
|
@ -188,13 +188,13 @@ describe("explorer/state", () => {
|
|||
it("immediately sets loading status", () => {
|
||||
const {getState, stm} = example(readyToRunPagerank());
|
||||
expect(loading(getState())).toBe("NOT_LOADING");
|
||||
stm.runPagerank(weightedTypes(), NodeAddress.empty);
|
||||
stm.runPagerank(weightedTypes(), new Map(), NodeAddress.empty);
|
||||
expect(loading(getState())).toBe("LOADING");
|
||||
});
|
||||
it("calls pagerank with the totalScoreNodePrefix option", async () => {
|
||||
const {pagerankMock, stm} = example(readyToRunPagerank());
|
||||
const foo = NodeAddress.fromParts(["foo"]);
|
||||
await stm.runPagerank(weightedTypes(), foo);
|
||||
await stm.runPagerank(weightedTypes(), new Map(), foo);
|
||||
const args = pagerankMock.mock.calls[0];
|
||||
expect(args[2].totalScoreNodePrefix).toBe(foo);
|
||||
});
|
||||
|
@ -204,7 +204,7 @@ describe("explorer/state", () => {
|
|||
// $ExpectFlowError
|
||||
console.error = jest.fn();
|
||||
pagerankMock.mockRejectedValue(error);
|
||||
await stm.runPagerank(weightedTypes(), NodeAddress.empty);
|
||||
await stm.runPagerank(weightedTypes(), new Map(), NodeAddress.empty);
|
||||
const state = getState();
|
||||
expect(loading(state)).toBe("FAILED");
|
||||
expect(state.type).toBe("READY_TO_RUN_PAGERANK");
|
||||
|
@ -222,12 +222,19 @@ describe("explorer/state", () => {
|
|||
const assets = new Assets("/gateway/");
|
||||
const adapters = new StaticExplorerAdapterSet([]);
|
||||
const prefix = NodeAddress.fromParts(["bar"]);
|
||||
const manualWeights = new Map();
|
||||
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).toHaveBeenCalledWith(assets, adapters);
|
||||
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 () => {
|
||||
const {stm} = example(readyToLoadGraph());
|
||||
|
@ -241,6 +248,7 @@ describe("explorer/state", () => {
|
|||
assets,
|
||||
adapters,
|
||||
weightedTypes(),
|
||||
new Map(),
|
||||
prefix
|
||||
);
|
||||
expect(stm.loadGraph).toHaveBeenCalledTimes(1);
|
||||
|
@ -252,15 +260,17 @@ describe("explorer/state", () => {
|
|||
(stm: any).runPagerank = jest.fn();
|
||||
const prefix = NodeAddress.fromParts(["bar"]);
|
||||
const wt = weightedTypes();
|
||||
const manualWeights = new Map();
|
||||
await stm.loadGraphAndRunPagerank(
|
||||
new Assets("/gateway/"),
|
||||
new StaticExplorerAdapterSet([]),
|
||||
wt,
|
||||
manualWeights,
|
||||
prefix
|
||||
);
|
||||
expect(stm.loadGraph).toHaveBeenCalledTimes(0);
|
||||
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 () => {
|
||||
const {stm} = example(pagerankEvaluated());
|
||||
|
@ -268,15 +278,17 @@ describe("explorer/state", () => {
|
|||
(stm: any).runPagerank = jest.fn();
|
||||
const prefix = NodeAddress.fromParts(["bar"]);
|
||||
const wt = weightedTypes();
|
||||
const manualWeights = new Map();
|
||||
await stm.loadGraphAndRunPagerank(
|
||||
new Assets("/gateway/"),
|
||||
new StaticExplorerAdapterSet([]),
|
||||
wt,
|
||||
manualWeights,
|
||||
prefix
|
||||
);
|
||||
expect(stm.loadGraph).toHaveBeenCalledTimes(0);
|
||||
expect(stm.runPagerank).toHaveBeenCalledTimes(1);
|
||||
expect(stm.runPagerank).toHaveBeenCalledWith(wt, prefix);
|
||||
expect(stm.runPagerank).toHaveBeenCalledWith(wt, manualWeights, prefix);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue