Display a node/edge type weight configuration UI (#487)

Summary:
This commit adds sliders for each node and edge type (hard-coded for
now), and hooks them up to the cred explorer so that re-running PageRank
uses the newly induced edge evaluator.

Paired with @decentralion.

Test Plan:
We will add tests later. We promise! In the meantime, the results that
appear when you drag a slider and re-run PageRank seem appropriate. For
instance, changing the “Git blob” node type from `0.0` to `-10.0`
results in the Git blobs not dominating the whole view.

wchargin-branch: configurable-weights-ui
This commit is contained in:
William Chargin 2018-07-04 18:50:50 -07:00 committed by GitHub
parent 1749676459
commit b5ddead3c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 194 additions and 10 deletions

View File

@ -10,6 +10,8 @@ import {Graph} from "../../core/graph";
import {pagerank, type PagerankResult} from "../../core/attribution/pagerank";
import {PagerankTable} from "./PagerankTable";
import type {PluginAdapter} from "../pluginAdapter";
import {type EdgeEvaluator} from "../../core/attribution/pagerank";
import {WeightConfig} from "./WeightConfig";
type Props = {};
type State = {
@ -24,6 +26,7 @@ type State = {
|},
+pagerankResult: ?PagerankResult,
|},
edgeEvaluator: ?EdgeEvaluator,
};
const REPO_OWNER_KEY = "repoOwner";
@ -36,6 +39,7 @@ export default class App extends React.Component<Props, State> {
repoOwner: "",
repoName: "",
data: {graphWithMetadata: null, pagerankResult: null},
edgeEvaluator: null,
};
}
@ -47,6 +51,7 @@ export default class App extends React.Component<Props, State> {
}
render() {
const {edgeEvaluator} = this.state;
const {graphWithMetadata, pagerankResult} = this.state.data;
return (
<div>
@ -82,20 +87,14 @@ export default class App extends React.Component<Props, State> {
<br />
<button onClick={() => this.loadData()}>Load data</button>
<button
disabled={graphWithMetadata == null}
disabled={graphWithMetadata == null || edgeEvaluator == null}
onClick={() => {
setTimeout(() => {
if (graphWithMetadata == null) {
throw new Error(
"graphWithMetadata: " + String(graphWithMetadata)
);
if (graphWithMetadata == null || edgeEvaluator == null) {
throw new Error("Unexpected null value");
}
const {graph} = graphWithMetadata;
const edgeWeight = (_unused_Edge) => ({
toWeight: 1,
froWeight: 1,
});
const pagerankResult = pagerank(graph, edgeWeight, {
const pagerankResult = pagerank(graph, edgeEvaluator, {
verbose: true,
});
const data = {graphWithMetadata, pagerankResult};
@ -120,6 +119,7 @@ export default class App extends React.Component<Props, State> {
) : (
<p>Graph not loaded.</p>
)}
<WeightConfig onChange={(ee) => this.setState({edgeEvaluator: ee})} />
<PagerankTable
graph={graphWithMetadata ? graphWithMetadata.graph : null}
adapters={graphWithMetadata ? graphWithMetadata.adapters : null}

View File

@ -0,0 +1,184 @@
// @flow
import React from "react";
import {
type EdgeAddressT,
type NodeAddressT,
EdgeAddress,
NodeAddress,
} from "../../core/graph";
import {type EdgeEvaluator} from "../../core/attribution/pagerank";
import {byEdgeType, byNodeType} from "./edgeWeights";
// Hacks...
import * as GithubNode from "../../plugins/github/nodes";
import * as GithubEdge from "../../plugins/github/edges";
import * as GitNode from "../../plugins/git/nodes";
import * as GitEdge from "../../plugins/git/edges";
type Props = {
onChange: (EdgeEvaluator) => void,
};
type UserEdgeWeight = {|+logWeight: number, +directionality: number|};
type UserNodeWeight = number /* in log space */;
const defaultEdgeWeights = (): Map<EdgeAddressT, UserEdgeWeight> =>
new Map()
.set(GithubEdge._Prefix.authors, {logWeight: 0, directionality: 0.5})
.set(GithubEdge._Prefix.mergedAs, {logWeight: 0, directionality: 0.5})
.set(GithubEdge._Prefix.references, {logWeight: 0, directionality: 0.5})
.set(GithubEdge._Prefix.hasParent, {logWeight: 0, directionality: 0.5})
.set(GitEdge._Prefix.hasTree, {logWeight: 0, directionality: 0.5})
.set(GitEdge._Prefix.hasParent, {logWeight: 0, directionality: 0.5})
.set(GitEdge._Prefix.includes, {logWeight: 0, directionality: 0.5})
.set(GitEdge._Prefix.becomes, {logWeight: 0, directionality: 0.5})
.set(GitEdge._Prefix.hasContents, {logWeight: 0, directionality: 0.5});
const defaultNodeWeights = (): Map<NodeAddressT, UserNodeWeight> =>
new Map()
.set(GithubNode._Prefix.repo, 0)
.set(GithubNode._Prefix.issue, 0)
.set(GithubNode._Prefix.pull, 0)
.set(GithubNode._Prefix.review, 0)
.set(GithubNode._Prefix.comment, 0)
.set(GithubNode._Prefix.userlike, 0)
.set(GitNode._Prefix.blob, 0)
.set(GitNode._Prefix.commit, 0)
.set(GitNode._Prefix.tree, 0)
.set(GitNode._Prefix.treeEntry, 0);
type State = {
edgeWeights: Map<EdgeAddressT, UserEdgeWeight>,
nodeWeights: Map<NodeAddressT, UserNodeWeight>,
};
export class WeightConfig extends React.Component<Props, State> {
constructor(props: Props): void {
super(props);
this.state = {
edgeWeights: defaultEdgeWeights(),
nodeWeights: defaultNodeWeights(),
};
}
render() {
return (
<div style={{display: "flex", flex: 1}}>
<EdgeConfig
edgeWeights={this.state.edgeWeights}
onChange={(ew) => this.setState({edgeWeights: ew}, () => this.fire())}
/>
<NodeConfig
nodeWeights={this.state.nodeWeights}
onChange={(nw) => this.setState({nodeWeights: nw}, () => this.fire())}
/>
</div>
);
}
componentDidMount() {
this.fire();
}
fire() {
const {edgeWeights, nodeWeights} = this.state;
const edgePrefixes = Array.from(edgeWeights.entries()).map(
([prefix, {logWeight, directionality}]) => ({
prefix,
weight: 2 ** logWeight,
directionality,
})
);
const nodePrefixes = Array.from(nodeWeights.entries()).map(
([prefix, logWeight]) => ({prefix, weight: 2 ** logWeight})
);
const edgeEvaluator = byNodeType(byEdgeType(edgePrefixes), nodePrefixes);
this.props.onChange(edgeEvaluator);
}
}
class EdgeConfig extends React.Component<{
edgeWeights: Map<EdgeAddressT, UserEdgeWeight>,
onChange: (Map<EdgeAddressT, UserEdgeWeight>) => void,
}> {
render() {
const controls = [];
for (const [key, currentValue] of this.props.edgeWeights.entries()) {
controls.push(
<label style={{display: "block"}} key={key}>
<input
type="range"
min={-10}
max={10}
step={0.1}
value={currentValue.logWeight}
onChange={(e) => {
const value: number = e.target.valueAsNumber;
const edgeWeights = new Map(this.props.edgeWeights);
const oldValue = edgeWeights.get(key);
if (oldValue == null) {
throw new Error(key);
}
edgeWeights.set(key, {...oldValue, logWeight: value});
this.props.onChange(edgeWeights);
}}
/>{" "}
{formatNumber(currentValue.logWeight)}{" "}
{JSON.stringify(EdgeAddress.toParts(key))}
</label>
);
}
return (
<div>
<h2>Edge weights (in log space)</h2>
{controls}
</div>
);
}
}
class NodeConfig extends React.Component<{
nodeWeights: Map<NodeAddressT, UserNodeWeight>,
onChange: (Map<NodeAddressT, UserNodeWeight>) => void,
}> {
render() {
const controls = [];
for (const [key, currentValue] of this.props.nodeWeights.entries()) {
controls.push(
<label style={{display: "block"}} key={key}>
<input
type="range"
min={-10}
max={10}
step={0.1}
value={currentValue}
onChange={(e) => {
const value: number = e.target.valueAsNumber;
const nodeWeights = new Map(this.props.nodeWeights);
nodeWeights.set(key, value);
this.props.onChange(nodeWeights);
}}
/>{" "}
{formatNumber(currentValue)}{" "}
{JSON.stringify(NodeAddress.toParts(key))}
</label>
);
}
return (
<div>
<h2>Node weights (in log space)</h2>
{controls}
</div>
);
}
}
function formatNumber(n: number) {
let x = n.toFixed(1);
if (!x.startsWith("-")) {
x = "+" + x;
}
return x.replace("-", "\u2212");
}