Better handling of log weights (#736)

This commit isolates all of the log-weight behavior in the weight
slider. That slider moves in log space, but the numbers printed and
passed around the WeightConfig code are now always in linear-space.

This should reduce confusion in the UI and for developers.

This commit contains two other improvements: (#588)
- Changes the (log space) range on the sliders from ±10 to ±5
- Change the order from slider, weight, name to name, slider, weight, so
that there is more visual separation between the name and the weight.

Test plan: Changes to the weight slider are tested. Changes to the
WeightConfig aren't (#604) so I manually tested the UI.
This commit is contained in:
Dandelion Mané 2018-08-30 19:21:59 -07:00 committed by GitHub
parent fc5c9ea589
commit d8a16a4def
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 57 additions and 47 deletions

View File

@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
- Improve weight sliders display (#736)
- Separate bots from users in the UI (#720)
- Add a feedback link to the prototype (#715)
- Support combining multiple repositories into a single graph (#711)

View File

@ -16,24 +16,24 @@ type Props = {|
type WeightedEdgeType = {|
+type: EdgeType,
+logWeight: number,
+weight: number,
+directionality: number,
|};
type EdgeWeights = WeightedEdgeType[];
const defaultEdgeWeights = (): EdgeWeights => {
const result = [];
for (const type of defaultStaticAdapters().edgeTypes()) {
result.push({type, logWeight: 0, directionality: 0.5});
result.push({type, weight: 1.0, directionality: 0.5});
}
return result;
};
type NodeWeights = WeightedNodeType[];
type WeightedNodeType = {|+type: NodeType, +logWeight: number|};
type WeightedNodeType = {|+type: NodeType, +weight: number|};
const defaultNodeWeights = (): NodeWeights => {
const result = [];
for (const type of defaultStaticAdapters().nodeTypes()) {
result.push({type, logWeight: type.defaultWeight});
result.push({type, weight: type.defaultWeight});
}
return result;
};
@ -97,16 +97,14 @@ export class WeightConfig extends React.Component<Props, State> {
fire() {
const {edgeWeights, nodeWeights} = this.state;
const edgePrefixes = edgeWeights.map(
({type, logWeight, directionality}) => ({
const edgePrefixes = edgeWeights.map(({type, weight, directionality}) => ({
prefix: type.prefix,
weight: 2 ** logWeight,
weight,
directionality,
})
);
const nodePrefixes = nodeWeights.map(({type, logWeight}) => ({
}));
const nodePrefixes = nodeWeights.map(({type, weight}) => ({
prefix: type.prefix,
weight: 2 ** logWeight,
weight,
}));
const edgeEvaluator = byNodeType(byEdgeType(edgePrefixes), nodePrefixes);
this.props.onChange(edgeEvaluator);
@ -122,18 +120,18 @@ class EdgeConfig extends React.Component<{
this.props.edgeWeights,
({type}) => type.prefix
);
return sortedWeights.map(({type, directionality, logWeight}) => {
return sortedWeights.map(({type, directionality, weight}) => {
const onChange = (value) => {
const edgeWeights = this.props.edgeWeights.filter(
(x) => x.type.prefix !== type.prefix
);
edgeWeights.push({type, logWeight: value, directionality});
edgeWeights.push({type, weight: value, directionality});
this.props.onChange(edgeWeights);
};
return (
<WeightSlider
key={type.prefix}
weight={logWeight}
weight={weight}
name={`${type.forwardName} / ${type.backwardName}`}
onChange={onChange}
/>
@ -146,12 +144,12 @@ class EdgeConfig extends React.Component<{
this.props.edgeWeights,
({type}) => type.prefix
);
return sortedWeights.map(({type, directionality, logWeight}) => {
return sortedWeights.map(({type, directionality, weight}) => {
const onChange = (value: number) => {
const edgeWeights = this.props.edgeWeights.filter(
(x) => x.type.prefix !== type.prefix
);
edgeWeights.push({type, directionality: value, logWeight});
edgeWeights.push({type, directionality: value, weight});
this.props.onChange(edgeWeights);
};
return (
@ -167,7 +165,7 @@ class EdgeConfig extends React.Component<{
render() {
return (
<div>
<h2>Edge weights (in log space)</h2>
<h2>Edge weights</h2>
{this.weightControls()}
<h2>Edge directionality</h2>
{this.directionControls()}
@ -186,18 +184,18 @@ class NodeConfig extends React.Component<{
({type}) => type.prefix
);
const controls = sortedWeights.map(({type, logWeight}) => {
const controls = sortedWeights.map(({type, weight}) => {
const onChange = (value) => {
const nodeWeights = this.props.nodeWeights.filter(
(x) => x.type.prefix !== type.prefix
);
nodeWeights.push({type, logWeight: value});
nodeWeights.push({type, weight: value});
this.props.onChange(nodeWeights);
};
return (
<WeightSlider
key={type.prefix}
weight={logWeight}
weight={weight}
name={type.name}
onChange={onChange}
/>
@ -205,7 +203,7 @@ class NodeConfig extends React.Component<{
});
return (
<div>
<h2>Node weights (in log space)</h2>
<h2>Node weights</h2>
{controls}
</div>
);

View File

@ -9,28 +9,36 @@ export class WeightSlider extends React.Component<{|
|}> {
render() {
return (
<label style={{display: "block"}}>
<label style={{display: "flex"}}>
<span style={{flexGrow: 1}}>{this.props.name}</span>
<input
type="range"
min={-10}
max={10}
step={0.1}
value={this.props.weight}
min={-5}
max={5}
step={1}
value={Math.log2(this.props.weight)}
onChange={(e) => {
this.props.onChange(e.target.valueAsNumber);
const logValue = e.target.valueAsNumber;
this.props.onChange(2 ** logValue);
}}
/>{" "}
<span>{formatWeight(this.props.weight)}</span>
<span>{this.props.name}</span>
<span
style={{minWidth: 45, display: "inline-block", textAlign: "right"}}
>
{formatWeight(this.props.weight)}
</span>
</label>
);
}
}
export function formatWeight(n: number) {
let x = n.toFixed(1);
if (!x.startsWith("-")) {
x = "+" + x;
if (n <= 0 || !isFinite(n)) {
throw new Error(`Invalid weight: ${n}`);
}
if (n >= 1) {
return n.toFixed(0) + "×";
} else {
return `1/${(1 / n).toFixed(0)}×`;
}
return x.replace("-", "\u2212");
}

View File

@ -16,16 +16,16 @@ describe("app/credExplorer/weights/WeightSlider", () => {
);
return {element, onChange};
}
it("sets slider to the provided weight", () => {
it("sets slider to the log of provided weight", () => {
const {element} = example();
expect(element.find("input").props().value).toBe(3);
expect(element.find("input").props().value).toBe(Math.log2(3));
});
it("prints the provided weight", () => {
const {element} = example();
expect(
element
.find("span")
.at(0)
.at(1)
.text()
).toBe(formatWeight(3));
});
@ -34,28 +34,31 @@ describe("app/credExplorer/weights/WeightSlider", () => {
expect(
element
.find("span")
.at(1)
.at(0)
.text()
).toBe("foo");
});
it("changes to the slider trigger the onChange", () => {
it("changes to the slider trigger the onChange with exponentiatied value", () => {
const {element, onChange} = example();
const input = element.find("input");
input.simulate("change", {target: {valueAsNumber: 7}});
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith(7);
expect(onChange).toHaveBeenCalledWith(2 ** 7);
});
});
describe("formatWeight", () => {
it("rounds to one decimal", () => {
expect(formatWeight(0.123)).toBe("+0.1");
it("shows numbers greater than 1 as a integer-rounded multiplier", () => {
expect(formatWeight(5.3)).toBe("5×");
});
it("adds a + to 0", () => {
expect(formatWeight(0)).toBe("+0.0");
it("shows numbers less than 1 (but not 0) as integer-rounded fractions", () => {
expect(formatWeight(0.249)).toBe("1/4×");
});
it("adds a minus symbol to negative numbers", () => {
expect(formatWeight(-3)).toBe("\u22123.0");
it("throws on bad values", () => {
const bads = [NaN, Infinity, -Infinity, -3, 0];
for (const bad of bads) {
expect(() => formatWeight(bad)).toThrowError("Invalid weight");
}
});
});
});