Recreate Pagerank Table (#466)
Ports #265 to the v3 branch, along with some tweaks: - Only display log score, and normalize them by adding 10 (so that most are non-negative) - Change colors to a soothing green - Improve display, e.g. make overflowing node description text wrap within the row Implements most of the tests requested in #269 Test plan: Many unit tests added Paired with @wchargin
This commit is contained in:
parent
72be58a5c0
commit
65b5babac4
|
@ -0,0 +1,252 @@
|
|||
// @flow
|
||||
|
||||
import sortBy from "lodash.sortby";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
Graph,
|
||||
NodeAddress,
|
||||
type NodeAddressT,
|
||||
Direction,
|
||||
EdgeAddress,
|
||||
} from "../../core/graph";
|
||||
import type {PagerankResult} from "../../core/attribution/pagerank";
|
||||
import type {PluginAdapter} from "../pluginAdapter";
|
||||
|
||||
const MAX_TABLE_ENTRIES = 100;
|
||||
|
||||
type Props = {
|
||||
pagerankResult: ?PagerankResult,
|
||||
graph: ?Graph,
|
||||
adapters: ?$ReadOnlyArray<PluginAdapter>,
|
||||
};
|
||||
|
||||
type State = {
|
||||
topLevelFilter: NodeAddressT,
|
||||
};
|
||||
|
||||
// TODO: Factor this out and test it (#465)
|
||||
export function nodeDescription(
|
||||
address: NodeAddressT,
|
||||
adapters: $ReadOnlyArray<PluginAdapter>
|
||||
): string {
|
||||
const adapter = adapters.find((adapter) =>
|
||||
NodeAddress.hasPrefix(address, adapter.nodePrefix())
|
||||
);
|
||||
if (adapter == null) {
|
||||
const result = NodeAddress.toString(address);
|
||||
console.warn(`No adapter for ${result}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
return adapter.renderer().nodeDescription(address);
|
||||
} catch (e) {
|
||||
const result = NodeAddress.toString(address);
|
||||
console.error(`Error getting description for ${result}: ${e.message}`);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class PagerankTable extends React.Component<Props, State> {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {topLevelFilter: NodeAddress.empty};
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.graph == null || this.props.adapters == null) {
|
||||
return <p>You must load a graph before seeing PageRank analysis.</p>;
|
||||
}
|
||||
if (this.props.pagerankResult == null) {
|
||||
return <p>Please run PageRank to see analysis.</p>;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h2>Contributions</h2>
|
||||
{this.renderFilterSelect()}
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderFilterSelect() {
|
||||
const {graph, pagerankResult, adapters} = this.props;
|
||||
if (graph == null || pagerankResult == null || adapters == null) {
|
||||
throw new Error("Impossible.");
|
||||
}
|
||||
|
||||
function optionGroup(adapter: PluginAdapter) {
|
||||
const header = (
|
||||
<option
|
||||
key={adapter.nodePrefix()}
|
||||
value={adapter.nodePrefix()}
|
||||
style={{fontWeight: "bold"}}
|
||||
>
|
||||
{adapter.name()}
|
||||
</option>
|
||||
);
|
||||
const entries = adapter.nodeTypes().map((type) => (
|
||||
<option key={type.prefix} value={type.prefix}>
|
||||
{"\u2003" + type.name}
|
||||
</option>
|
||||
));
|
||||
return [header, ...entries];
|
||||
}
|
||||
return (
|
||||
<label>
|
||||
Filter by contribution type:{" "}
|
||||
<select
|
||||
value={this.state.topLevelFilter}
|
||||
onChange={(e) => {
|
||||
this.setState({topLevelFilter: e.target.value});
|
||||
}}
|
||||
>
|
||||
<option value={NodeAddress.empty}>Show all</option>
|
||||
{sortBy(adapters, (a) => a.name()).map(optionGroup)}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
renderTable() {
|
||||
const {graph, pagerankResult, adapters} = this.props;
|
||||
if (graph == null || pagerankResult == null || adapters == null) {
|
||||
throw new Error("Impossible.");
|
||||
}
|
||||
const topLevelFilter = this.state.topLevelFilter;
|
||||
return (
|
||||
<table style={{borderCollapse: "collapse", marginTop: 10}}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{textAlign: "left"}}>Node</th>
|
||||
<th style={{textAlign: "right"}}>log(score)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<RecursiveTables
|
||||
addresses={
|
||||
topLevelFilter == null
|
||||
? Array.from(graph.nodes())
|
||||
: Array.from(graph.nodes()).filter((node) =>
|
||||
NodeAddress.hasPrefix(node, topLevelFilter)
|
||||
)
|
||||
}
|
||||
graph={graph}
|
||||
pagerankResult={pagerankResult}
|
||||
depth={0}
|
||||
adapters={adapters}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type RTState = {expanded: boolean};
|
||||
type RTProps = {|
|
||||
+address: NodeAddressT,
|
||||
+graph: Graph,
|
||||
+pagerankResult: PagerankResult,
|
||||
+depth: number,
|
||||
+adapters: $ReadOnlyArray<PluginAdapter>,
|
||||
|};
|
||||
|
||||
class RecursiveTable extends React.Component<RTProps, RTState> {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {expanded: false};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {address, adapters, depth, graph, pagerankResult} = this.props;
|
||||
const {expanded} = this.state;
|
||||
const probability = pagerankResult.get(address);
|
||||
if (probability == null) {
|
||||
throw new Error(`no PageRank value for ${NodeAddress.toString(address)}`);
|
||||
}
|
||||
const modifiedLogScore = Math.log(probability) + 10;
|
||||
return [
|
||||
<tr
|
||||
key="self"
|
||||
style={{backgroundColor: `rgba(0,143.4375,0,${1 - 0.9 ** depth})`}}
|
||||
>
|
||||
<td style={{display: "flex", alignItems: "flex-start"}}>
|
||||
<button
|
||||
style={{
|
||||
marginRight: 5,
|
||||
marginLeft: 15 * depth,
|
||||
}}
|
||||
onClick={() => {
|
||||
this.setState(({expanded}) => ({
|
||||
expanded: !expanded,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{expanded ? "\u2212" : "+"}
|
||||
</button>
|
||||
<span>{nodeDescription(address, adapters)}</span>
|
||||
</td>
|
||||
<td style={{textAlign: "right"}}>{modifiedLogScore.toFixed(2)}</td>
|
||||
</tr>,
|
||||
expanded && (
|
||||
<RecursiveTables
|
||||
key="children"
|
||||
addresses={Array.from(
|
||||
new Set( // deduplicate same node reached by several edges
|
||||
Array.from(
|
||||
graph.neighbors(address, {
|
||||
direction: Direction.ANY,
|
||||
nodePrefix: NodeAddress.empty,
|
||||
edgePrefix: EdgeAddress.empty,
|
||||
})
|
||||
).map((neighbor) => neighbor.node)
|
||||
)
|
||||
)}
|
||||
graph={graph}
|
||||
pagerankResult={pagerankResult}
|
||||
depth={depth + 1}
|
||||
adapters={adapters}
|
||||
/>
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
type RecursiveTablesProps = {|
|
||||
+addresses: $ReadOnlyArray<NodeAddressT>,
|
||||
+graph: Graph,
|
||||
+pagerankResult: PagerankResult,
|
||||
+depth: number,
|
||||
+adapters: $ReadOnlyArray<PluginAdapter>,
|
||||
|};
|
||||
|
||||
class RecursiveTables extends React.Component<RecursiveTablesProps> {
|
||||
render() {
|
||||
const {addresses, graph, pagerankResult, depth, adapters} = this.props;
|
||||
return addresses
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const x = pagerankResult.get(a);
|
||||
const y = pagerankResult.get(b);
|
||||
if (x == null) {
|
||||
throw new Error(`No pagerank result for ${NodeAddress.toString(a)}`);
|
||||
}
|
||||
if (y == null) {
|
||||
throw new Error(`No pagerank result for ${NodeAddress.toString(b)}`);
|
||||
}
|
||||
return y - x;
|
||||
})
|
||||
.slice(0, MAX_TABLE_ENTRIES)
|
||||
.map((address) => (
|
||||
<RecursiveTable
|
||||
depth={depth}
|
||||
address={address}
|
||||
graph={graph}
|
||||
pagerankResult={pagerankResult}
|
||||
key={address}
|
||||
adapters={adapters}
|
||||
/>
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,326 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import {mount, shallow} from "enzyme";
|
||||
import enzymeToJSON from "enzyme-to-json";
|
||||
|
||||
import {PagerankTable, nodeDescription} from "./PagerankTable";
|
||||
import {pagerank} from "../../core/attribution/pagerank";
|
||||
import sortBy from "lodash.sortby";
|
||||
|
||||
import {
|
||||
Graph,
|
||||
type NodeAddressT,
|
||||
NodeAddress,
|
||||
EdgeAddress,
|
||||
} from "../../core/graph";
|
||||
|
||||
require("../testUtil").configureEnzyme();
|
||||
|
||||
function example() {
|
||||
const graph = new Graph();
|
||||
const nodes = {
|
||||
fooAlpha: NodeAddress.fromParts(["foo", "a", "1"]),
|
||||
fooBeta: NodeAddress.fromParts(["foo", "b", "2"]),
|
||||
bar1: NodeAddress.fromParts(["bar", "a", "1"]),
|
||||
bar2: NodeAddress.fromParts(["bar", "2"]),
|
||||
xox: NodeAddress.fromParts(["xox"]),
|
||||
empty: NodeAddress.empty,
|
||||
};
|
||||
Object.values(nodes).forEach((n) => graph.addNode((n: any)));
|
||||
|
||||
function addEdge(parts, src, dst) {
|
||||
const edge = {address: EdgeAddress.fromParts(parts), src, dst};
|
||||
graph.addEdge(edge);
|
||||
}
|
||||
|
||||
addEdge(["a"], nodes.fooAlpha, nodes.fooBeta);
|
||||
addEdge(["b"], nodes.fooAlpha, nodes.bar1);
|
||||
addEdge(["c"], nodes.fooAlpha, nodes.xox);
|
||||
addEdge(["d"], nodes.bar1, nodes.bar1);
|
||||
addEdge(["e"], nodes.bar1, nodes.xox);
|
||||
addEdge(["e'"], nodes.bar1, nodes.xox);
|
||||
|
||||
const adapters = [
|
||||
{
|
||||
name: () => "foo",
|
||||
graph: () => {
|
||||
throw new Error("unused");
|
||||
},
|
||||
renderer: () => ({
|
||||
nodeDescription: (x) => `foo: ${NodeAddress.toString(x)}`,
|
||||
}),
|
||||
nodePrefix: () => NodeAddress.fromParts(["foo"]),
|
||||
nodeTypes: () => [
|
||||
{name: "alpha", prefix: NodeAddress.fromParts(["foo", "a"])},
|
||||
{name: "beta", prefix: NodeAddress.fromParts(["foo", "b"])},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: () => "bar",
|
||||
graph: () => {
|
||||
throw new Error("unused");
|
||||
},
|
||||
renderer: () => ({
|
||||
nodeDescription: (x) => `bar: ${NodeAddress.toString(x)}`,
|
||||
}),
|
||||
nodePrefix: () => NodeAddress.fromParts(["bar"]),
|
||||
nodeTypes: () => [
|
||||
{name: "alpha", prefix: NodeAddress.fromParts(["bar", "a"])},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: () => "xox",
|
||||
graph: () => {
|
||||
throw new Error("unused");
|
||||
},
|
||||
renderer: () => ({
|
||||
nodeDescription: (_unused_arg) => `xox node!`,
|
||||
}),
|
||||
nodePrefix: () => NodeAddress.fromParts(["xox"]),
|
||||
nodeTypes: () => [],
|
||||
},
|
||||
{
|
||||
name: () => "unused",
|
||||
graph: () => {
|
||||
throw new Error("unused");
|
||||
},
|
||||
renderer: () => {
|
||||
throw new Error("Impossible!");
|
||||
},
|
||||
nodePrefix: () => NodeAddress.fromParts(["unused"]),
|
||||
nodeTypes: () => [],
|
||||
},
|
||||
];
|
||||
|
||||
const pagerankResult = pagerank(graph, (_unused_Edge) => ({
|
||||
toWeight: 1,
|
||||
froWeight: 1,
|
||||
}));
|
||||
|
||||
return {adapters, nodes, graph, pagerankResult};
|
||||
}
|
||||
|
||||
describe("app/credExplorer/PagerankTable", () => {
|
||||
function verifyNoAdapterWarning() {
|
||||
expect(console.warn).toHaveBeenCalledWith("No adapter for NodeAddress[]");
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
// $ExpectFlowError
|
||||
console.warn = jest.fn();
|
||||
}
|
||||
beforeEach(() => {
|
||||
// $ExpectFlowError
|
||||
console.error = jest.fn();
|
||||
// $ExpectFlowError
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
afterEach(() => {
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
expect(console.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("rendering with incomplete props", () => {
|
||||
it("renders expected message with null props", () => {
|
||||
const element = shallow(
|
||||
<PagerankTable pagerankResult={null} graph={null} adapters={null} />
|
||||
);
|
||||
expect(enzymeToJSON(element)).toMatchSnapshot();
|
||||
});
|
||||
it("renders with just pagerankResult", () => {
|
||||
const {pagerankResult} = example();
|
||||
// No snapshot since this should never actually happen
|
||||
shallow(
|
||||
<PagerankTable
|
||||
pagerankResult={pagerankResult}
|
||||
graph={null}
|
||||
adapters={null}
|
||||
/>
|
||||
);
|
||||
});
|
||||
it("renders with just graph", () => {
|
||||
const {graph} = example();
|
||||
// No snapshot since this should never actually happen
|
||||
shallow(
|
||||
<PagerankTable pagerankResult={null} graph={graph} adapters={null} />
|
||||
);
|
||||
});
|
||||
it("renders with just adapters", () => {
|
||||
const {adapters} = example();
|
||||
// No snapshot since this should never actually happen
|
||||
shallow(
|
||||
<PagerankTable pagerankResult={null} graph={null} adapters={adapters} />
|
||||
);
|
||||
});
|
||||
it("renders expected message when there's no pagerank", () => {
|
||||
const {graph, adapters} = example();
|
||||
const element = shallow(
|
||||
<PagerankTable
|
||||
pagerankResult={null}
|
||||
graph={graph}
|
||||
adapters={adapters}
|
||||
/>
|
||||
);
|
||||
expect(enzymeToJSON(element)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("full rendering", () => {
|
||||
function exampleRender() {
|
||||
const {nodes, adapters, graph, pagerankResult} = example();
|
||||
const element = mount(
|
||||
<PagerankTable
|
||||
pagerankResult={pagerankResult}
|
||||
graph={graph}
|
||||
adapters={adapters}
|
||||
/>
|
||||
);
|
||||
verifyNoAdapterWarning();
|
||||
const select = element.find("select");
|
||||
expect(select).toHaveLength(1);
|
||||
return {nodes, adapters, graph, pagerankResult, element, select};
|
||||
}
|
||||
it("full render doesn't crash or error", () => {
|
||||
example();
|
||||
});
|
||||
|
||||
describe("tables ", () => {
|
||||
it("are sorted by score", () => {
|
||||
const {element, graph, pagerankResult} = exampleRender();
|
||||
const rows = element.find("RecursiveTable");
|
||||
expect(rows).toHaveLength(Array.from(graph.nodes()).length);
|
||||
const scores = rows.map((x) => pagerankResult.get(x.prop("address")));
|
||||
expect(scores).toEqual(sortBy(scores).reverse());
|
||||
});
|
||||
function expectColumnCorrect(
|
||||
element: *,
|
||||
name: string,
|
||||
tdToExpected: (x: *) => string,
|
||||
addressToExpected: (NodeAddressT) => string
|
||||
) {
|
||||
const header = element.find("th");
|
||||
const headerTexts = header.map((x) => x.text());
|
||||
const headerIndex = headerTexts.indexOf(name);
|
||||
if (headerIndex === -1) {
|
||||
throw new Error("Could not find column: " + name);
|
||||
}
|
||||
const tables = element.find("RecursiveTable");
|
||||
const actual = tables.map((x) =>
|
||||
tdToExpected(x.find("td").at(headerIndex))
|
||||
);
|
||||
const expected = tables.map((x) =>
|
||||
addressToExpected(x.prop("address"))
|
||||
);
|
||||
expect(actual).toEqual(expected);
|
||||
}
|
||||
it("has a node description column", () => {
|
||||
const {element, adapters} = exampleRender();
|
||||
expectColumnCorrect(
|
||||
element,
|
||||
"Node",
|
||||
(td) => td.find("span").text(),
|
||||
(address) => nodeDescription(address, adapters)
|
||||
);
|
||||
verifyNoAdapterWarning();
|
||||
});
|
||||
it("has a log score column", () => {
|
||||
const {element, pagerankResult} = exampleRender();
|
||||
expectColumnCorrect(
|
||||
element,
|
||||
"log(score)",
|
||||
(td) => td.text(),
|
||||
(address) => {
|
||||
const probability = pagerankResult.get(address);
|
||||
if (probability == null) {
|
||||
throw new Error(address);
|
||||
}
|
||||
const modifiedLogScore = Math.log(probability) + 10;
|
||||
return modifiedLogScore.toFixed(2);
|
||||
}
|
||||
);
|
||||
});
|
||||
it("subtables have depth-based styling", () => {
|
||||
const {element} = exampleRender();
|
||||
const getLevel = (level) => {
|
||||
const rt = element.find("RecursiveTable").at(level);
|
||||
const button = rt.find("button").first();
|
||||
return {rt, button};
|
||||
};
|
||||
getLevel(0).button.simulate("click");
|
||||
getLevel(1).button.simulate("click");
|
||||
const f = ({rt, button}) => ({
|
||||
row: rt
|
||||
.find("tr")
|
||||
.first()
|
||||
.prop("style"),
|
||||
button: button.prop("style"),
|
||||
});
|
||||
expect([0, 1, 2].map((x) => f(getLevel(x)))).toMatchSnapshot();
|
||||
});
|
||||
it("button toggles between +/- and adds sub-RecursiveTable", () => {
|
||||
const {element} = exampleRender();
|
||||
const rt = () => element.find("RecursiveTable").first();
|
||||
const button = rt().find("button");
|
||||
expect(button).toEqual(expect.anything());
|
||||
expect(button.text()).toEqual("+");
|
||||
expect(rt().find("RecursiveTables")).toHaveLength(0);
|
||||
|
||||
button.simulate("click");
|
||||
expect(button.text()).toEqual("\u2212");
|
||||
expect(rt().find("RecursiveTables")).toHaveLength(1);
|
||||
|
||||
button.simulate("click");
|
||||
expect(button.text()).toEqual("+");
|
||||
expect(rt().find("RecursiveTables")).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("filter selector", () => {
|
||||
it("has the correct options", () => {
|
||||
const {select} = exampleRender();
|
||||
const options = select.children();
|
||||
expect(options.every("option")).toBe(true);
|
||||
const results = options.map((x) => ({
|
||||
valueString: NodeAddress.toString(x.prop("value")),
|
||||
style: x.prop("style"),
|
||||
text: x.text(),
|
||||
}));
|
||||
expect(results).toMatchSnapshot();
|
||||
});
|
||||
|
||||
function selectFilterByName(select, name) {
|
||||
const option = select.children().filterWhere((x) => x.text() === name);
|
||||
if (option.length !== 1) {
|
||||
throw new Error(`ambiguous select, got ${option.length} options`);
|
||||
}
|
||||
const value = option.prop("value");
|
||||
select.simulate("change", {target: {value}});
|
||||
}
|
||||
it("plugin-level filter with no nodes works", () => {
|
||||
const {select, element} = exampleRender();
|
||||
expect(element.find("tbody tr")).not.toHaveLength(0);
|
||||
selectFilterByName(select, "unused");
|
||||
expect(element.find("tbody tr")).toHaveLength(0);
|
||||
});
|
||||
it("type-level filter with some nodes works", () => {
|
||||
const {select, element} = exampleRender();
|
||||
selectFilterByName(select, "\u2003beta");
|
||||
const rt = element.find("RecursiveTable");
|
||||
expect(rt).toHaveLength(1);
|
||||
expect(rt.prop("address")).toEqual(example().nodes.fooBeta);
|
||||
});
|
||||
it("filter doesn't apply to sub-tables", () => {
|
||||
const {select, element} = exampleRender();
|
||||
selectFilterByName(select, "\u2003beta");
|
||||
const rt = element.find("RecursiveTable");
|
||||
expect(rt).toHaveLength(1);
|
||||
const button = rt.find("button");
|
||||
expect(button).toHaveLength(1);
|
||||
button.simulate("click");
|
||||
|
||||
const rts = element.find("RecursiveTable");
|
||||
expect(rts).toHaveLength(2);
|
||||
const subRt = rts.last();
|
||||
expect(subRt.prop("address")).toEqual(example().nodes.fooAlpha);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,98 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`app/credExplorer/PagerankTable full rendering filter selector has the correct options 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"style": undefined,
|
||||
"text": "Show all",
|
||||
"valueString": "NodeAddress[]",
|
||||
},
|
||||
Object {
|
||||
"style": Object {
|
||||
"fontWeight": "bold",
|
||||
},
|
||||
"text": "bar",
|
||||
"valueString": "NodeAddress[\\"bar\\"]",
|
||||
},
|
||||
Object {
|
||||
"style": undefined,
|
||||
"text": " alpha",
|
||||
"valueString": "NodeAddress[\\"bar\\",\\"a\\"]",
|
||||
},
|
||||
Object {
|
||||
"style": Object {
|
||||
"fontWeight": "bold",
|
||||
},
|
||||
"text": "foo",
|
||||
"valueString": "NodeAddress[\\"foo\\"]",
|
||||
},
|
||||
Object {
|
||||
"style": undefined,
|
||||
"text": " alpha",
|
||||
"valueString": "NodeAddress[\\"foo\\",\\"a\\"]",
|
||||
},
|
||||
Object {
|
||||
"style": undefined,
|
||||
"text": " beta",
|
||||
"valueString": "NodeAddress[\\"foo\\",\\"b\\"]",
|
||||
},
|
||||
Object {
|
||||
"style": Object {
|
||||
"fontWeight": "bold",
|
||||
},
|
||||
"text": "unused",
|
||||
"valueString": "NodeAddress[\\"unused\\"]",
|
||||
},
|
||||
Object {
|
||||
"style": Object {
|
||||
"fontWeight": "bold",
|
||||
},
|
||||
"text": "xox",
|
||||
"valueString": "NodeAddress[\\"xox\\"]",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`app/credExplorer/PagerankTable full rendering tables subtables have depth-based styling 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"button": Object {
|
||||
"marginLeft": 0,
|
||||
"marginRight": 5,
|
||||
},
|
||||
"row": Object {
|
||||
"backgroundColor": "rgba(0,143.4375,0,0)",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"button": Object {
|
||||
"marginLeft": 15,
|
||||
"marginRight": 5,
|
||||
},
|
||||
"row": Object {
|
||||
"backgroundColor": "rgba(0,143.4375,0,0.09999999999999998)",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"button": Object {
|
||||
"marginLeft": 30,
|
||||
"marginRight": 5,
|
||||
},
|
||||
"row": Object {
|
||||
"backgroundColor": "rgba(0,143.4375,0,0.18999999999999995)",
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`app/credExplorer/PagerankTable rendering with incomplete props renders expected message when there's no pagerank 1`] = `
|
||||
<p>
|
||||
Please run PageRank to see analysis.
|
||||
</p>
|
||||
`;
|
||||
|
||||
exports[`app/credExplorer/PagerankTable rendering with incomplete props renders expected message with null props 1`] = `
|
||||
<p>
|
||||
You must load a graph before seeing PageRank analysis.
|
||||
</p>
|
||||
`;
|
|
@ -0,0 +1,18 @@
|
|||
// @flow
|
||||
|
||||
export function configureEnzyme() {
|
||||
const Enzyme = require("enzyme");
|
||||
const Adapter = require("enzyme-adapter-react-16");
|
||||
Enzyme.configure({adapter: new Adapter()});
|
||||
}
|
||||
|
||||
export function configureAphrodite() {
|
||||
const {StyleSheetTestUtils} = require("aphrodite/no-important");
|
||||
beforeEach(() => {
|
||||
StyleSheetTestUtils.suppressStyleInjection();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
StyleSheetTestUtils.clearBufferAndResumeStyleInjection();
|
||||
});
|
||||
}
|
|
@ -10,6 +10,7 @@ import {
|
|||
|
||||
import {findStationaryDistribution} from "./markovChain";
|
||||
|
||||
export type {PagerankResult} from "./graphToMarkovChain";
|
||||
export type PagerankOptions = {|
|
||||
+selfLoopWeight?: number,
|
||||
+verbose?: boolean,
|
||||
|
|
Loading…
Reference in New Issue