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 {defaultStaticAdapters} from "../adapters/defaultPlugins";
|
||||
import type {NodeType, EdgeType} from "../adapters/pluginAdapter";
|
||||
import {WeightSlider} from "./weights/WeightSlider";
|
||||
import {DirectionalitySlider} from "./weights/DirectionalitySlider";
|
||||
|
||||
type Props = {|
|
||||
+onChange: (EdgeEvaluator) => void,
|
||||
|
@ -120,26 +122,23 @@ class EdgeConfig extends React.Component<{
|
|||
this.props.edgeWeights,
|
||||
({type}) => type.prefix
|
||||
);
|
||||
return sortedWeights.map(({type, directionality, logWeight}) => (
|
||||
<label style={{display: "block"}} key={type.prefix}>
|
||||
<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(
|
||||
(x) => x.type.prefix !== type.prefix
|
||||
);
|
||||
edgeWeights.push({type, logWeight: value, directionality});
|
||||
this.props.onChange(edgeWeights);
|
||||
}}
|
||||
/>{" "}
|
||||
{formatNumber(logWeight)} {`${type.forwardName}/${type.backwardName}`}
|
||||
</label>
|
||||
));
|
||||
return sortedWeights.map(({type, directionality, logWeight}) => {
|
||||
const onChange = (value) => {
|
||||
const edgeWeights = this.props.edgeWeights.filter(
|
||||
(x) => x.type.prefix !== type.prefix
|
||||
);
|
||||
edgeWeights.push({type, logWeight: value, directionality});
|
||||
this.props.onChange(edgeWeights);
|
||||
};
|
||||
return (
|
||||
<WeightSlider
|
||||
key={type.prefix}
|
||||
weight={logWeight}
|
||||
name={`${type.forwardName} / ${type.backwardName}`}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
directionControls() {
|
||||
|
@ -147,26 +146,23 @@ class EdgeConfig extends React.Component<{
|
|||
this.props.edgeWeights,
|
||||
({type}) => type.prefix
|
||||
);
|
||||
return sortedWeights.map(({type, directionality, logWeight}) => (
|
||||
<label style={{display: "block"}} key={type.prefix}>
|
||||
<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(
|
||||
(x) => x.type.prefix !== type.prefix
|
||||
);
|
||||
edgeWeights.push({type, directionality: value, logWeight});
|
||||
this.props.onChange(edgeWeights);
|
||||
}}
|
||||
/>{" "}
|
||||
{directionality.toFixed(2)} {type.forwardName}
|
||||
</label>
|
||||
));
|
||||
return sortedWeights.map(({type, directionality, logWeight}) => {
|
||||
const onChange = (value: number) => {
|
||||
const edgeWeights = this.props.edgeWeights.filter(
|
||||
(x) => x.type.prefix !== type.prefix
|
||||
);
|
||||
edgeWeights.push({type, directionality: value, logWeight});
|
||||
this.props.onChange(edgeWeights);
|
||||
};
|
||||
return (
|
||||
<DirectionalitySlider
|
||||
name={type.forwardName}
|
||||
key={type.prefix}
|
||||
directionality={directionality}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
|
@ -190,26 +186,23 @@ class NodeConfig extends React.Component<{
|
|||
({type}) => type.prefix
|
||||
);
|
||||
|
||||
const controls = sortedWeights.map(({type, logWeight}) => (
|
||||
<label style={{display: "block"}} key={type.prefix}>
|
||||
<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(
|
||||
(x) => x.type.prefix !== type.prefix
|
||||
);
|
||||
nodeWeights.push({type, logWeight: value});
|
||||
this.props.onChange(nodeWeights);
|
||||
}}
|
||||
/>{" "}
|
||||
{formatNumber(logWeight)} {type.name}
|
||||
</label>
|
||||
));
|
||||
const controls = sortedWeights.map(({type, logWeight}) => {
|
||||
const onChange = (value) => {
|
||||
const nodeWeights = this.props.nodeWeights.filter(
|
||||
(x) => x.type.prefix !== type.prefix
|
||||
);
|
||||
nodeWeights.push({type, logWeight: value});
|
||||
this.props.onChange(nodeWeights);
|
||||
};
|
||||
return (
|
||||
<WeightSlider
|
||||
key={type.prefix}
|
||||
weight={logWeight}
|
||||
name={type.name}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<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