Skip to content

Commit

Permalink
Add simplex layering method
Browse files Browse the repository at this point in the history
  • Loading branch information
erikbrinkman committed Jul 18, 2018
1 parent c3e154c commit dee9f99
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 25 deletions.
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { default as dagCoordsSpread } from "./src/layout/coords/spread";
export { default as dagHierarchy } from "./src/dag/hierarchy";
export { default as dagLayerLongestPath } from "./src/layout/layering/longestPath";
export { default as dagLayerSimplex } from "./src/layout/layering/simplex";
export { default as dagLayout } from "./src/layout/index";
export { default as dagStratify } from "./src/dag/stratify";
export { default as topologicalSort } from "./src/topologicalSort";
25 changes: 6 additions & 19 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@
},
"devDependencies": {
"eslint": "^5.0.1",
"javascript-lp-solver": "^0.4.5",
"package-preamble": "^0.1.0",
"rollup": "^0.62.0",
"rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-node-resolve": "^3.3.0",
"tape": "^4.9.1",
"uglify-es": "^3.3.9"
}
Expand Down
7 changes: 7 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import resolve from "rollup-plugin-node-resolve";
import commonjs from "rollup-plugin-commonjs";

export default {
input: "index.js",
output: {
Expand All @@ -6,4 +9,8 @@ export default {
name: "d3",
extend: true,
},
plugins: [
resolve(),
commonjs(),
],
};
1 change: 1 addition & 0 deletions src/dag/dag.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// FIXME Change each depth to each as its the best default if you don't care
import Node from "./node";
import count from "./count";
import depth from "./depth";
Expand Down
2 changes: 2 additions & 0 deletions src/dag/eachDepth.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// FIXME Add integers to function for eaches
// FIXME Do something about guarantees for queuing children vs calling function, seems like function should be called first
export default function(nodes, func) {
const queue = nodes.slice();
const seen = {};
Expand Down
3 changes: 3 additions & 0 deletions src/dag/node.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// FIXME Change eachDepth to each as its the best default if you don't care
// FIXME Add other arraylike methods, map, reduce, or just say you should use nodes?
// FIXME Same for links?
import ancestors from "./ancestors";
import count from "./count";
import depth from "./depth";
Expand Down
34 changes: 34 additions & 0 deletions src/layout/layering/simplex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import solver from "javascript-lp-solver";

export default function(dag) {
// use null prefixes to prevent clash
const variables = {};
const ints = {};
const constraints = {};
dag.eachDepth(node => {
const nid = `\0${node.id}`;
ints[nid] = 1;
constraints[nid] = {"min": 0};
const variable = variables[nid] = {
opt: node.parents.length - node.children.length,
[nid]: 1,
};
node.parents.forEach(parent => {
variable[`${parent.id}\0${node.id}`] = 1;
});
node.children.forEach(child => {
const edge = `${node.id}\0${child.id}`;
constraints[edge] = {"min": 1};
variable[edge] = -1;
});
});
const assignment = solver.Solve({
optimize: "opt",
opType: "min",
constraints: constraints,
variables: variables,
ints: ints,
});
// lp solver doesn't assign some zeros
dag.eachDepth(n => n.layer = assignment[`\0${n.id}`] || 0);
}
26 changes: 20 additions & 6 deletions test/layout/layering/longestPath-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,32 @@ const tape = require("tape"),
fs = require("fs"),
d3_dag = require("../../../");

const [grafo] = [
const [square, grafo] = [
"test/data/square.json",
"test/data/grafo.json",
].map(file => d3_dag.dagStratify()(JSON.parse(fs.readFileSync(file))));

function toLayers(dag) {
const layers = [];
dag.eachDepth(n => (layers[n.layer] || (layers[n.layer] = [])).push(parseInt(n.id)));
layers.forEach(l => l.sort((a, b) => a - b));
return layers;
}

tape("longestPath() works for square", test => {
d3_dag.dagLayerLongestPath(square);
const layers = toLayers(square);
test.equals(layers.length, 3);
test.deepEquals(layers, [[0], [1, 2], [3]]);
test.end();
});

tape("longestPath() works for grafo", test => {
d3_dag.dagLayerLongestPath(grafo);
const maxLayer = grafo.nodes().reduce((l, n) => Math.max(l, n.layer), 0);
test.equals(maxLayer, 5);
const layers = new Array(6).fill(null).map(() => []);
grafo.eachDepth(n => layers[n.layer].push(parseInt(n.id)));
const layers = toLayers(grafo);
test.equals(layers.length, 6);
test.deepEquals(
layers.map(l => l.sort((a, b) => a - b)),
layers,
[[21], [12], [2, 4, 8], [0, 9, 11, 13, 19], [1, 3, 15, 16, 17, 18, 20], [5, 6, 7, 10, 14]]);
test.end();
});
36 changes: 36 additions & 0 deletions test/layout/layering/simplex-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const tape = require("tape"),
fs = require("fs"),
d3_dag = require("../../../");

const [square, grafo] = [
"test/data/square.json",
"test/data/grafo.json",
].map(file => d3_dag.dagStratify()(JSON.parse(fs.readFileSync(file))));

function toLayers(dag) {
const layers = [];
dag.eachDepth(n => (layers[n.layer] || (layers[n.layer] = [])).push(parseInt(n.id)));
layers.forEach(l => l.sort((a, b) => a - b));
return layers;
}

tape("simplex() works for square", test => {
d3_dag.dagLayerLongestPath(square);
const layers = toLayers(square);
test.equals(layers.length, 3);
test.deepEquals(layers, [[0], [1, 2], [3]]);
test.end();
});

tape("simplex() works for grafo", test => {
d3_dag.dagLayerSimplex(grafo);
const layers = toLayers(grafo);
test.equals(layers.length, 8);
const cost = grafo.links().reduce((s, l) => s + l.target.layer - l.source.layer, 0);
test.equals(cost, 30);
// XXX There are two possible configurations
test.deepEquals(
layers,
[[1, 8], [0, 14], [16, 21], [10, 12], [2, 4, 19], [9, 11, 13, 15, 17], [3, 6, 18, 20], [5, 7]]);
test.end();
});

0 comments on commit dee9f99

Please sign in to comment.