Factor out WeightSlider and DirectionalitySlider (#734)
This commit factors the weight sliders used for both node and edge weights into a shared WeightSlider component, and factors out the direction slider used for edge weights into a DirectionalitySlider. Both of these components are tested. This is a step towards #604. Test plan: The specific behaviors of the sliders are well tested. Since the weight config as a whole is not tested, I manually verified by messing with the weights that node weights, edge weights, and edge directionality all affects the cred distribution as anticipated.
This commit is contained in:
parent
1a96894220
commit
761b0f1282
|
@ -7,6 +7,8 @@ import {type EdgeEvaluator} from "../../core/attribution/pagerank";
|
||||||
import {byEdgeType, byNodeType} from "./edgeWeights";
|
import {byEdgeType, byNodeType} from "./edgeWeights";
|
||||||
import {defaultStaticAdapters} from "../adapters/defaultPlugins";
|
import {defaultStaticAdapters} from "../adapters/defaultPlugins";
|
||||||
import type {NodeType, EdgeType} from "../adapters/pluginAdapter";
|
import type {NodeType, EdgeType} from "../adapters/pluginAdapter";
|
||||||
|
import {WeightSlider} from "./weights/WeightSlider";
|
||||||
|
import {DirectionalitySlider} from "./weights/DirectionalitySlider";
|
||||||
|
|
||||||
type Props = {|
|
type Props = {|
|
||||||
+onChange: (EdgeEvaluator) => void,
|
+onChange: (EdgeEvaluator) => void,
|
||||||
|
@ -120,26 +122,23 @@ class EdgeConfig extends React.Component<{
|
||||||
this.props.edgeWeights,
|
this.props.edgeWeights,
|
||||||
({type}) => type.prefix
|
({type}) => type.prefix
|
||||||
);
|
);
|
||||||
return sortedWeights.map(({type, directionality, logWeight}) => (
|
return sortedWeights.map(({type, directionality, logWeight}) => {
|
||||||
<label style={{display: "block"}} key={type.prefix}>
|
const onChange = (value) => {
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={-10}
|
|
||||||
max={10}
|
|
||||||
step={0.1}
|
|
||||||
value={logWeight}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value: number = e.target.valueAsNumber;
|
|
||||||
const edgeWeights = this.props.edgeWeights.filter(
|
const edgeWeights = this.props.edgeWeights.filter(
|
||||||
(x) => x.type.prefix !== type.prefix
|
(x) => x.type.prefix !== type.prefix
|
||||||
);
|
);
|
||||||
edgeWeights.push({type, logWeight: value, directionality});
|
edgeWeights.push({type, logWeight: value, directionality});
|
||||||
this.props.onChange(edgeWeights);
|
this.props.onChange(edgeWeights);
|
||||||
}}
|
};
|
||||||
/>{" "}
|
return (
|
||||||
{formatNumber(logWeight)} {`${type.forwardName}/${type.backwardName}`}
|
<WeightSlider
|
||||||
</label>
|
key={type.prefix}
|
||||||
));
|
weight={logWeight}
|
||||||
|
name={`${type.forwardName} / ${type.backwardName}`}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
directionControls() {
|
directionControls() {
|
||||||
|
@ -147,26 +146,23 @@ class EdgeConfig extends React.Component<{
|
||||||
this.props.edgeWeights,
|
this.props.edgeWeights,
|
||||||
({type}) => type.prefix
|
({type}) => type.prefix
|
||||||
);
|
);
|
||||||
return sortedWeights.map(({type, directionality, logWeight}) => (
|
return sortedWeights.map(({type, directionality, logWeight}) => {
|
||||||
<label style={{display: "block"}} key={type.prefix}>
|
const onChange = (value: number) => {
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
value={directionality}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value: number = e.target.valueAsNumber;
|
|
||||||
const edgeWeights = this.props.edgeWeights.filter(
|
const edgeWeights = this.props.edgeWeights.filter(
|
||||||
(x) => x.type.prefix !== type.prefix
|
(x) => x.type.prefix !== type.prefix
|
||||||
);
|
);
|
||||||
edgeWeights.push({type, directionality: value, logWeight});
|
edgeWeights.push({type, directionality: value, logWeight});
|
||||||
this.props.onChange(edgeWeights);
|
this.props.onChange(edgeWeights);
|
||||||
}}
|
};
|
||||||
/>{" "}
|
return (
|
||||||
{directionality.toFixed(2)} {type.forwardName}
|
<DirectionalitySlider
|
||||||
</label>
|
name={type.forwardName}
|
||||||
));
|
key={type.prefix}
|
||||||
|
directionality={directionality}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
@ -190,26 +186,23 @@ class NodeConfig extends React.Component<{
|
||||||
({type}) => type.prefix
|
({type}) => type.prefix
|
||||||
);
|
);
|
||||||
|
|
||||||
const controls = sortedWeights.map(({type, logWeight}) => (
|
const controls = sortedWeights.map(({type, logWeight}) => {
|
||||||
<label style={{display: "block"}} key={type.prefix}>
|
const onChange = (value) => {
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={-10}
|
|
||||||
max={10}
|
|
||||||
step={0.1}
|
|
||||||
value={logWeight}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value: number = e.target.valueAsNumber;
|
|
||||||
const nodeWeights = this.props.nodeWeights.filter(
|
const nodeWeights = this.props.nodeWeights.filter(
|
||||||
(x) => x.type.prefix !== type.prefix
|
(x) => x.type.prefix !== type.prefix
|
||||||
);
|
);
|
||||||
nodeWeights.push({type, logWeight: value});
|
nodeWeights.push({type, logWeight: value});
|
||||||
this.props.onChange(nodeWeights);
|
this.props.onChange(nodeWeights);
|
||||||
}}
|
};
|
||||||
/>{" "}
|
return (
|
||||||
{formatNumber(logWeight)} {type.name}
|
<WeightSlider
|
||||||
</label>
|
key={type.prefix}
|
||||||
));
|
weight={logWeight}
|
||||||
|
name={type.name}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>Node weights (in log space)</h2>
|
<h2>Node weights (in log space)</h2>
|
||||||
|
@ -218,11 +211,3 @@ class NodeConfig extends React.Component<{
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatNumber(n: number) {
|
|
||||||
let x = n.toFixed(1);
|
|
||||||
if (!x.startsWith("-")) {
|
|
||||||
x = "+" + x;
|
|
||||||
}
|
|
||||||
return x.replace("-", "\u2212");
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
function assertValidDirectionality(x: number) {
|
||||||
|
if (x < 0 || x > 1) {
|
||||||
|
throw new Error(
|
||||||
|
`directionality out of bounds: ${x} must be between 0 and 1`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DirectionalitySlider extends React.Component<{|
|
||||||
|
+directionality: number,
|
||||||
|
+name: string,
|
||||||
|
+onChange: (number) => void,
|
||||||
|
|}> {
|
||||||
|
render() {
|
||||||
|
assertValidDirectionality(this.props.directionality);
|
||||||
|
return (
|
||||||
|
<label style={{display: "block"}}>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.01}
|
||||||
|
value={this.props.directionality}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.valueAsNumber;
|
||||||
|
assertValidDirectionality(value);
|
||||||
|
this.props.onChange(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>{this.props.directionality.toFixed(2)}</span>
|
||||||
|
<span>{this.props.name}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {shallow} from "enzyme";
|
||||||
|
|
||||||
|
import {DirectionalitySlider} from "./DirectionalitySlider";
|
||||||
|
|
||||||
|
require("../../testUtil").configureEnzyme();
|
||||||
|
|
||||||
|
describe("app/credExplorer/weights/DirectionalitySlider", () => {
|
||||||
|
describe("DirectionalitySlider", () => {
|
||||||
|
function example() {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const element = shallow(
|
||||||
|
<DirectionalitySlider
|
||||||
|
directionality={0.5}
|
||||||
|
name={"foo"}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return {element, onChange};
|
||||||
|
}
|
||||||
|
it("sets slider to the provided weight", () => {
|
||||||
|
const {element} = example();
|
||||||
|
expect(element.find("input").props().value).toBe(0.5);
|
||||||
|
});
|
||||||
|
it("slider min is 0", () => {
|
||||||
|
const {element} = example();
|
||||||
|
expect(element.find("input").props().min).toBe(0);
|
||||||
|
});
|
||||||
|
it("slider max is 0", () => {
|
||||||
|
const {element} = example();
|
||||||
|
expect(element.find("input").props().max).toBe(1);
|
||||||
|
});
|
||||||
|
it("prints the provided weight", () => {
|
||||||
|
const {element} = example();
|
||||||
|
expect(
|
||||||
|
element
|
||||||
|
.find("span")
|
||||||
|
.at(0)
|
||||||
|
.text()
|
||||||
|
).toBe("0.50");
|
||||||
|
});
|
||||||
|
it("displays the provided name", () => {
|
||||||
|
const {element} = example();
|
||||||
|
expect(
|
||||||
|
element
|
||||||
|
.find("span")
|
||||||
|
.at(1)
|
||||||
|
.text()
|
||||||
|
).toBe("foo");
|
||||||
|
});
|
||||||
|
it("changes to the slider trigger the onChange", () => {
|
||||||
|
const {element, onChange} = example();
|
||||||
|
const input = element.find("input");
|
||||||
|
input.simulate("change", {target: {valueAsNumber: 0.99}});
|
||||||
|
expect(onChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onChange).toHaveBeenCalledWith(0.99);
|
||||||
|
});
|
||||||
|
it("errors if provided an out-of-bound directionality", () => {
|
||||||
|
function withDirectionality(d) {
|
||||||
|
return () =>
|
||||||
|
shallow(
|
||||||
|
<DirectionalitySlider
|
||||||
|
directionality={d}
|
||||||
|
name={"foo"}
|
||||||
|
onChange={jest.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expect(withDirectionality(-0.2)).toThrowError(
|
||||||
|
"directionality out of bounds"
|
||||||
|
);
|
||||||
|
expect(withDirectionality(2)).toThrowError(
|
||||||
|
"directionality out of bounds"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("errors rather than providing an out-of-bound directionality", () => {
|
||||||
|
const {element} = example();
|
||||||
|
const input = element.find("input");
|
||||||
|
expect(() => {
|
||||||
|
input.simulate("change", {target: {valueAsNumber: -0.2}});
|
||||||
|
}).toThrowError("directionality out of bounds");
|
||||||
|
expect(() => {
|
||||||
|
input.simulate("change", {target: {valueAsNumber: 2.0}});
|
||||||
|
}).toThrowError("directionality out of bounds");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,36 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export class WeightSlider extends React.Component<{|
|
||||||
|
+weight: number,
|
||||||
|
+name: string,
|
||||||
|
+onChange: (number) => void,
|
||||||
|
|}> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<label style={{display: "block"}}>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={-10}
|
||||||
|
max={10}
|
||||||
|
step={0.1}
|
||||||
|
value={this.props.weight}
|
||||||
|
onChange={(e) => {
|
||||||
|
this.props.onChange(e.target.valueAsNumber);
|
||||||
|
}}
|
||||||
|
/>{" "}
|
||||||
|
<span>{formatWeight(this.props.weight)}</span>
|
||||||
|
<span>{this.props.name}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatWeight(n: number) {
|
||||||
|
let x = n.toFixed(1);
|
||||||
|
if (!x.startsWith("-")) {
|
||||||
|
x = "+" + x;
|
||||||
|
}
|
||||||
|
return x.replace("-", "\u2212");
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {shallow} from "enzyme";
|
||||||
|
|
||||||
|
import {WeightSlider, formatWeight} from "./WeightSlider";
|
||||||
|
|
||||||
|
require("../../testUtil").configureEnzyme();
|
||||||
|
|
||||||
|
describe("app/credExplorer/weights/WeightSlider", () => {
|
||||||
|
describe("WeightSlider", () => {
|
||||||
|
function example() {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const element = shallow(
|
||||||
|
<WeightSlider weight={3} name={"foo"} onChange={onChange} />
|
||||||
|
);
|
||||||
|
return {element, onChange};
|
||||||
|
}
|
||||||
|
it("sets slider to the provided weight", () => {
|
||||||
|
const {element} = example();
|
||||||
|
expect(element.find("input").props().value).toBe(3);
|
||||||
|
});
|
||||||
|
it("prints the provided weight", () => {
|
||||||
|
const {element} = example();
|
||||||
|
expect(
|
||||||
|
element
|
||||||
|
.find("span")
|
||||||
|
.at(0)
|
||||||
|
.text()
|
||||||
|
).toBe(formatWeight(3));
|
||||||
|
});
|
||||||
|
it("displays the provided name", () => {
|
||||||
|
const {element} = example();
|
||||||
|
expect(
|
||||||
|
element
|
||||||
|
.find("span")
|
||||||
|
.at(1)
|
||||||
|
.text()
|
||||||
|
).toBe("foo");
|
||||||
|
});
|
||||||
|
it("changes to the slider trigger the onChange", () => {
|
||||||
|
const {element, onChange} = example();
|
||||||
|
const input = element.find("input");
|
||||||
|
input.simulate("change", {target: {valueAsNumber: 7}});
|
||||||
|
expect(onChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onChange).toHaveBeenCalledWith(7);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatWeight", () => {
|
||||||
|
it("rounds to one decimal", () => {
|
||||||
|
expect(formatWeight(0.123)).toBe("+0.1");
|
||||||
|
});
|
||||||
|
it("adds a + to 0", () => {
|
||||||
|
expect(formatWeight(0)).toBe("+0.0");
|
||||||
|
});
|
||||||
|
it("adds a minus symbol to negative numbers", () => {
|
||||||
|
expect(formatWeight(-3)).toBe("\u22123.0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue