diff --git a/src/app/credExplorer/WeightConfig.js b/src/app/credExplorer/WeightConfig.js index 35e1e09..8b1ff60 100644 --- a/src/app/credExplorer/WeightConfig.js +++ b/src/app/credExplorer/WeightConfig.js @@ -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}) => ( - - )); + 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 ( + + ); + }); } directionControls() { @@ -147,26 +146,23 @@ class EdgeConfig extends React.Component<{ this.props.edgeWeights, ({type}) => type.prefix ); - return sortedWeights.map(({type, directionality, logWeight}) => ( - - )); + 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 ( + + ); + }); } render() { return ( @@ -190,26 +186,23 @@ class NodeConfig extends React.Component<{ ({type}) => type.prefix ); - const controls = sortedWeights.map(({type, logWeight}) => ( - - )); + 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 ( + + ); + }); return (

Node weights (in log space)

@@ -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"); -} diff --git a/src/app/credExplorer/weights/DirectionalitySlider.js b/src/app/credExplorer/weights/DirectionalitySlider.js new file mode 100644 index 0000000..4a45b5e --- /dev/null +++ b/src/app/credExplorer/weights/DirectionalitySlider.js @@ -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 ( + + ); + } +} diff --git a/src/app/credExplorer/weights/DirectionalitySlider.test.js b/src/app/credExplorer/weights/DirectionalitySlider.test.js new file mode 100644 index 0000000..9b9d092 --- /dev/null +++ b/src/app/credExplorer/weights/DirectionalitySlider.test.js @@ -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( + + ); + 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( + + ); + } + 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"); + }); + }); +}); diff --git a/src/app/credExplorer/weights/WeightSlider.js b/src/app/credExplorer/weights/WeightSlider.js new file mode 100644 index 0000000..2e96a2a --- /dev/null +++ b/src/app/credExplorer/weights/WeightSlider.js @@ -0,0 +1,36 @@ +// @flow + +import React from "react"; + +export class WeightSlider extends React.Component<{| + +weight: number, + +name: string, + +onChange: (number) => void, +|}> { + render() { + return ( + + ); + } +} + +export function formatWeight(n: number) { + let x = n.toFixed(1); + if (!x.startsWith("-")) { + x = "+" + x; + } + return x.replace("-", "\u2212"); +} diff --git a/src/app/credExplorer/weights/WeightSlider.test.js b/src/app/credExplorer/weights/WeightSlider.test.js new file mode 100644 index 0000000..44a24fb --- /dev/null +++ b/src/app/credExplorer/weights/WeightSlider.test.js @@ -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( + + ); + 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"); + }); + }); +});