From f98fecb920f8b7eab80e49ca1401f17d0d1d2c99 Mon Sep 17 00:00:00 2001 From: Collin Green Date: Thu, 27 Jan 2022 15:57:18 -0800 Subject: [PATCH] [Ordering] add bias constraints to graph opts Add ability to pass in a list of graph constraints (list of high node vs low node relationships) to influence the ordering of the graph. Useful for picking which way edges go and for making a layout more 'stable' (the onesignal use case is making all branch edges of the same type go the same way). This is essentially this commit by izhan https://github.com/izhan/dagre/commit/4b40097490f1a83698cf5684484e89710af95ca0 in this PR https://github.com/dagrejs/dagre/pull/302 with a couple changes - high/low over left/right plus some type overrides (alternatively could fork definitelytyped and put the changes there) --- index.d.ts | 176 +++++++++++++++++++++++++++++++++++++++ lib/layout.js | 9 +- lib/order/index.js | 14 +++- test/order/order-test.js | 6 +- 4 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 index.d.ts diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 00000000..db25765f --- /dev/null +++ b/index.d.ts @@ -0,0 +1,176 @@ +// Type definitions for dagre 0.7 +// Project: https://github.com/dagrejs/dagre +// Definitions by: Qinfeng Chen +// Pete Vilter +// David Newell +// Graham Lea +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.2 + +// export as namespace dagre; + +declare module "dagre" { + export namespace graphlib { + class Graph { + constructor(opt?: { + directed?: boolean | undefined; + multigraph?: boolean | undefined; + compound?: boolean | undefined; + }); + + graph(): GraphLabel; + isDirected(): boolean; + isMultiGraph(): boolean; + setGraph(label: GraphLabel): Graph; + + edge(edgeObj: Edge): GraphEdge; + edge(outNodeName: string, inNodeName: string, name?: string): GraphEdge; + edgeCount(): number; + edges(): Edge[]; + hasEdge(edgeObj: Edge): boolean; + hasEdge(outNodeName: string, inNodeName: string, name?: string): boolean; + inEdges(inNodeName: string, outNodeName?: string): Edge[] | undefined; + outEdges(outNodeName: string, inNodeName?: string): Edge[] | undefined; + removeEdge(outNodeName: string, inNodeName: string): Graph; + setDefaultEdgeLabel( + callback: + | string + | ((v: string, w: string, name?: string) => string | Label) + ): Graph; + setEdge(params: Edge, value?: string | { [key: string]: any }): Graph; + setEdge( + sourceId: string, + targetId: string, + value?: string | Label, + name?: string + ): Graph; + + children(parentName: string): string | undefined; + hasNode(name: string): boolean; + neighbors(name: string): Array> | undefined; + node(id: string | Label): Node; + nodeCount(): number; + nodes(): string[]; + parent(childName: string): string | undefined; + predecessors(name: string): Array> | undefined; + removeNode(name: string): Graph; + filterNodes(callback: (nodeId: string) => boolean): Graph; + setDefaultNodeLabel( + callback: string | ((nodeId: string) => string | Label) + ): Graph; + setNode(name: string, label: string | Label): Graph; + setParent(childName: string, parentName: string): void; + sinks(): Array>; + sources(): Array>; + successors(name: string): Array> | undefined; + } + + namespace json { + function read(graph: any): Graph; + function write(graph: Graph): any; + } + + namespace alg { + function components(graph: Graph): string[][]; + function dijkstra( + graph: Graph, + source: string, + weightFn?: WeightFn, + edgeFn?: EdgeFn + ): any; + function dijkstraAll( + graph: Graph, + weightFn?: WeightFn, + edgeFn?: EdgeFn + ): any; + function findCycles(graph: Graph): string[][]; + function floydWarchall( + graph: Graph, + weightFn?: WeightFn, + edgeFn?: EdgeFn + ): any; + function isAcyclic(graph: Graph): boolean; + function postorder(graph: Graph, nodeNames: string | string[]): string[]; + function preorder(graph: Graph, nodeNames: string | string[]): string[]; + function prim(graph: Graph, weightFn?: WeightFn): Graph; + function tarjam(graph: Graph): string[][]; + function topsort(graph: Graph): string[]; + } + } + + export interface Label { + [key: string]: any; + } + export type WeightFn = (edge: Edge) => number; + export type EdgeFn = (outNodeName: string) => GraphEdge[]; + + export interface GraphLabel { + width?: number | undefined; + height?: number | undefined; + compound?: boolean | undefined; + rankdir?: string | undefined; + align?: string | undefined; + nodesep?: number | undefined; + edgesep?: number | undefined; + ranksep?: number | undefined; + marginx?: number | undefined; + marginy?: number | undefined; + acyclicer?: string | undefined; + ranker?: string | undefined; + } + + export interface NodeConfig { + width?: number | undefined; + height?: number | undefined; + } + + export interface EdgeConfig { + minlen?: number | undefined; + weight?: number | undefined; + width?: number | undefined; + height?: number | undefined; + lablepos?: "l" | "c" | "r" | undefined; + labeloffest?: number | undefined; + } + + interface Constraint { + high: string; + low: string; + } + + interface LayoutOptions { + constraints?: Constraint[]; + } + + export function layout( + graph: graphlib.Graph, + // layout?: GraphLabel & NodeConfig & EdgeConfig + options: LayoutOptions + ): void; + + export interface Edge { + v: string; + w: string; + name?: string | undefined; + } + + export interface GraphEdge { + points: Array<{ x: number; y: number }>; + [key: string]: any; + } + + export type Node = T & { + x: number; + y: number; + width: number; + height: number; + class?: string | undefined; + label?: string | undefined; + padding?: number | undefined; + paddingX?: number | undefined; + paddingY?: number | undefined; + rx?: number | undefined; + ry?: number | undefined; + shape?: string | undefined; + }; +} diff --git a/lib/layout.js b/lib/layout.js index 26b07304..20e6eab7 100644 --- a/lib/layout.js +++ b/lib/layout.js @@ -17,17 +17,18 @@ var Graph = require("./graphlib").Graph; module.exports = layout; -function layout(g, opts) { +function layout(g, inputOpts) { + var opts = inputOpts || {}; var time = opts && opts.debugTiming ? util.time : util.notime; time("layout", function() { var layoutGraph = time(" buildLayoutGraph", function() { return buildLayoutGraph(g); }); - time(" runLayout", function() { runLayout(layoutGraph, time); }); + time(" runLayout", function() { runLayout(layoutGraph, time, opts); }); time(" updateInputGraph", function() { updateInputGraph(g, layoutGraph); }); }); } -function runLayout(g, time) { +function runLayout(g, time, opts) { time(" makeSpaceForEdgeLabels", function() { makeSpaceForEdgeLabels(g); }); time(" removeSelfEdges", function() { removeSelfEdges(g); }); time(" acyclic", function() { acyclic.run(g); }); @@ -42,7 +43,7 @@ function runLayout(g, time) { time(" normalize.run", function() { normalize.run(g); }); time(" parentDummyChains", function() { parentDummyChains(g); }); time(" addBorderSegments", function() { addBorderSegments(g); }); - time(" order", function() { order(g); }); + time(" order", function() { order(g, opts); }); time(" insertSelfEdges", function() { insertSelfEdges(g); }); time(" adjustCoordinateSystem", function() { coordinateSystem.adjust(g); }); time(" position", function() { position(g); }); diff --git a/lib/order/index.js b/lib/order/index.js index 4ac2d9fa..fa4702a8 100644 --- a/lib/order/index.js +++ b/lib/order/index.js @@ -26,7 +26,7 @@ module.exports = order; * 1. Graph nodes will have an "order" attribute based on the results of the * algorithm. */ -function order(g) { +function order(g, opts) { var maxRank = util.maxRank(g), downLayerGraphs = buildLayerGraphs(g, _.range(1, maxRank + 1), "inEdges"), upLayerGraphs = buildLayerGraphs(g, _.range(maxRank - 1, -1, -1), "outEdges"); @@ -37,8 +37,9 @@ function order(g) { var bestCC = Number.POSITIVE_INFINITY, best; + var constraints = opts.constraints || []; for (var i = 0, lastBest = 0; lastBest < 4; ++i, ++lastBest) { - sweepLayerGraphs(i % 2 ? downLayerGraphs : upLayerGraphs, i % 4 >= 2); + sweepLayerGraphs(i % 2 ? downLayerGraphs : upLayerGraphs, i % 4 >= 2, constraints); layering = util.buildLayerMatrix(g); var cc = crossCount(g, layering); @@ -46,6 +47,8 @@ function order(g) { lastBest = 0; best = _.cloneDeep(layering); bestCC = cc; + } else if (cc === bestCC) { + best = _.cloneDeep(layering); } } @@ -58,8 +61,13 @@ function buildLayerGraphs(g, ranks, relationship) { }); } -function sweepLayerGraphs(layerGraphs, biasRight) { +function sweepLayerGraphs(layerGraphs, biasRight, constraints) { var cg = new Graph(); + + _.forEach(constraints, function(constraint) { + cg.setEdge(constraint.high, constaint.low); + }); + _.forEach(layerGraphs, function(lg) { var root = lg.graph().root; var sorted = sortSubgraph(lg, root, cg, biasRight); diff --git a/test/order/order-test.js b/test/order/order-test.js index 9b209f34..60e1896c 100644 --- a/test/order/order-test.js +++ b/test/order/order-test.js @@ -20,7 +20,7 @@ describe("order", function() { g.setPath(["a", "b", "c"]); g.setEdge("b", "d"); g.setPath(["a", "e", "f"]); - order(g); + order(g, {}); var layering = util.buildLayerMatrix(g); expect(crossCount(g, layering)).to.equal(0); }); @@ -30,7 +30,7 @@ describe("order", function() { _.forEach(["a", "d"], function(v) { g.setNode(v, { rank: 1 }); }); _.forEach(["b", "f", "e"], function(v) { g.setNode(v, { rank: 2 }); }); _.forEach(["c", "g"], function(v) { g.setNode(v, { rank: 3 }); }); - order(g); + order(g, {}); var layering = util.buildLayerMatrix(g); expect(crossCount(g, layering)).to.equal(0); }); @@ -40,7 +40,7 @@ describe("order", function() { _.forEach(["b", "e", "g"], function(v) { g.setNode(v, { rank: 2 }); }); _.forEach(["c", "f", "h"], function(v) { g.setNode(v, { rank: 3 }); }); g.setNode("d", { rank: 4 }); - order(g); + order(g, {}); var layering = util.buildLayerMatrix(g); expect(crossCount(g, layering)).to.be.lte(1); });