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:
Dandelion Mané 2018-08-30 13:43:32 -07:00 committed by GitHub
parent 1a96894220
commit 761b0f1282
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 278 additions and 68 deletions

View File

@ -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");
}

View File

@ -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>
);
}
}

View File

@ -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");
});
});
});

View File

@ -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");
}

View File

@ -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");
});
});
});