Permalink
Browse files

Arc layout now uses properties.

This change also includes an abstract pv.Layout.Network for code-sharing among
network layouts. So far this only includes the arc layout but subsequent commits
should extend support to force-directed layout and others. Hopefully we can also
create an abstract pv.Layout.Hierarchy that extends this abstract network layout
for use with tree, cluster, partition, etc.
  • Loading branch information...
1 parent 602f97f commit 9036038a263766133dd23f01378a0ec011189e49 @mbostock committed Mar 7, 2010
Showing with 252 additions and 232 deletions.
  1. +1 −0 Makefile
  2. +93 −176 src/layout/Arc.js
  3. +123 −0 src/layout/Network.js
  4. +14 −17 tests/layout/arc-directed.html
  5. +7 −12 tests/layout/arc-radial.html
  6. +6 −12 tests/layout/arc.html
  7. +8 −15 tests/layout/arc2.html
View
1 Makefile
@@ -59,6 +59,7 @@ JS_PV_FILES = \
src/physics/PositionConstraint.js \
src/physics/BoundConstraint.js \
src/layout/Layout.js \
+ src/layout/Network.js \
src/layout/Grid.js \
src/layout/Stack.js \
src/layout/Treemap.js \
View
269 src/layout/Arc.js
@@ -1,192 +1,109 @@
-pv.Layout.arc = function(nodes, links) {
- var orient = "top",
- directed = false,
- w, // the cached parent panel width
- h, // cached parent panel height
- r; // cached Math.min(w, h) / 2
+/** @class Layout for arc diagrams. */
+pv.Layout.Arc = function() {
+ pv.Layout.Network.call(this);
+ var orient, directed, reverse;
/** @private */
- function init() {
- w = this.parent.width();
- h = this.parent.height();
- r = Math.min(w, h) / 2;
- for (var i = 0, n = nodes.length; i < n; i++) {
- nodes[i].breadth = (i + .5) / n;
- }
- }
+ this.init = function() {
+ orient = this.orient();
+ directed = this.directed();
+ reverse = orient == "right" || orient == "top";
+ if (pv.Layout.Network.prototype.init.call(this)) return;
- /** @private The layout, on which all public methods are registered. */
- var layout = {};
+ var nodes = this.nodes(),
+ w = this.parent.width(),
+ h = this.parent.height(),
+ r = Math.min(w, h) / 2;
- /**
- * Sets or gets the orientation. The default orientation is "left", which
- * means that the root node is placed on the left edge, leaf nodes appear on
- * the right edge, and internal nodes are in-between. The following
- * orientations are supported:<ul>
- *
- * <li>left - left-to-right.
- * <li>right - right-to-left.
- * <li>top - top-to-bottom.
- * <li>bottom - bottom-to-top.
- * <li>radial - radially, with the root at the center.</ul>
- *
- * @param {string} v the new orientation.
- * @function
- * @name pv.Layout.arc.prototype.orient
- * @returns {pv.Layout.arc} this, or the current orientation.
- */
- layout.orient = function(v) {
- if (arguments.length) {
- orient = String(v);
- return this;
+ /** @private Returns the angle, given the breadth. */
+ function angle(b) {
+ switch (orient) {
+ case "top": return -Math.PI / 2;
+ case "bottom": return Math.PI / 2;
+ case "left": return Math.PI;
+ case "right": return 0;
+ case "radial": return (b - .25) * 2 * Math.PI;
+ }
}
- return orient;
- };
- /**
- * Sets or gets whether this arc digram is directed (bidirectional). By
- * default, arc digrams are undirected, such that all arcs will appear on one
- * side (for non-radial orientations). If the arc digram is directed, then
- * forward links will exist on the conventional side (the same as as
- * undirected links--right, left, bottom and top for left, right, top and
- * bottom, respectively), while reverse links exist on the opposite side.
- *
- * @param {boolean} x whether or not this arc digram is directed.
- * @function
- * @name pv.Layout.arc.prototype.directed
- * @returns {pv.Layout.arc} this, or the current directedness.
- */
- layout.directed = function(x) {
- if (arguments.length) {
- directed = Boolean(x);
- return this;
+ /** @private Returns the left position, given the breadth. */
+ function left(b) {
+ switch (orient) {
+ case "top":
+ case "bottom": return b * w;
+ case "left": return 0;
+ case "right": return w;
+ case "radial": return w / 2 + r * Math.cos(angle(b));
+ }
}
- return directed;
- };
-
- /**
- * Returns the nodes associated with this layout.
- *
- * @function
- * @name pv.Layout.arc.prototype.nodes
- * @returns {array}
- */
- layout.nodes = nodes = nodes.map(function(d, i) {
- return {nodeName: i, nodeValue: d, linkDegree: 0};
- });
- /**
- * Returns the links associated with this layout. Each link is represented as
- * a two-element array; the first element is the source node, and the second
- * element is the target node.
- *
- * @function
- * @name pv.Layout.arc.prototype.links
- * @returns {array}
- */
- layout.links = links = links.map(function(d) {
- var s = nodes[d.source],
- t = nodes[d.target],
- l = [s, t],
- v = isNaN(d.value) ? 1 : d.value;
- s.linkDegree += v;
- t.linkDegree += v;
- l.linkValue = v;
- return l;
- });
+ /** @private Returns the top position, given the breadth. */
+ function top(b) {
+ switch (orient) {
+ case "top": return 0;
+ case "bottom": return h;
+ case "left":
+ case "right": return b * h;
+ case "radial": return h / 2 + r * Math.sin(angle(b));
+ }
+ }
- /** @private Returns the angle of the given node. */
- function angle(n) {
- return (n.breadth - .25) * 2 * Math.PI;
- }
+ /* Populate the left, top and angle attributes. */
+ for (var i = 0, n = nodes.length; i < n; i++) {
+ var breadth = (i + .5) / n, node = nodes[i];
+ node.left = left(breadth);
+ node.top = top(breadth);
+ node.angle = angle(breadth);
+ }
+ };
- /**
- * The node prototype. This prototype is intended to be used with a Dot mark
- * in conjunction with the link prototype.
- *
- * @type pv.Mark
- * @name pv.Layout.arc.prototype.node
- */
- layout.node = new pv.Mark()
- .def("init", init)
- .data(nodes)
- .strokeStyle("#1f77b4")
- .fillStyle("#fff")
- .left(function(n) {
- switch (orient) {
- case "top":
- case "bottom": return n.breadth * w;
- case "left": return 0;
- case "right": return w;
- case "radial": return w / 2 + r * Math.cos(angle(n));
- }
+ /* Override link properties to handle directedness and orientation. */
+ this.link
+ .data(function(p) {
+ var s = p.sourceNode, t = p.targetNode;
+ return reverse != (directed || (s.index < t.index))
+ ? [s, t] : [t, s];
})
- .top(function top(n) {
- switch (orient) {
- case "top": return 0;
- case "bottom": return h;
- case "left":
- case "right": return n.breadth * h;
- case "radial": return h / 2 + r * Math.sin(angle(n));
- }
- });
-
- /**
- * The link prototype, which renders edges between child nodes and their
- * parents. This prototype is intended to be used with a Line mark in
- * conjunction with the node prototype.
- *
- * @type pv.Mark
- * @name pv.Layout.arc.prototype.link
- */
- layout.link = new pv.Mark()
- .extend(layout.node)
.interpolate(function() {
return (orient == "radial") ? "linear" : "polar";
- })
- .data(function(p) {
- return (directed || (p[0].breadth < p[1].breadth))
- ? p // no reverse necessary, arc will be drawn as intended
- : [p[1], p[0]];
- })
- .fillStyle(null)
- .lineWidth(function(d, p) { return p.linkValue * 1.5; })
- .strokeStyle("rgba(0,0,0,.2)");
-
- /**
- * The node label prototype, which renders the node name adjacent to the node.
- * This prototype is provided as an alternative to using the anchor on the
- * node mark; it is primarily intended to be used with radial node-link
- * layouts, since it provides a convenient mechanism to set the text angle.
- *
- * @type pv.Mark
- * @name pv.Layout.arc.prototype.label
- */
- layout.label = new pv.Mark()
- .extend(layout.node)
- .textMargin(7)
- .textBaseline("middle")
- .text(function(n) { return n.nodeValue; })
- .textAngle(function(n) {
- switch (orient) {
- case "top":
- case "bottom": return -Math.PI / 2;
- case "left":
- case "right": return 0;
- }
- var a = angle(n);
- return pv.Wedge.upright(a) ? a : (a + Math.PI);
- })
- .textAlign(function(n) {
- switch (orient) {
- case "top":
- case "right": return "left";
- case "bottom":
- case "left": return "right";
- }
- return pv.Wedge.upright(angle(n)) ? "left" : "right";
});
-
- return layout;
};
+pv.Layout.Arc.prototype = pv.extend(pv.Layout.Network)
+ .property("orient", String)
+ .property("directed", Boolean);
+
+pv.Layout.Arc.prototype.defaults = new pv.Layout.Arc()
+ .extend(pv.Layout.Network.prototype.defaults)
+ .orient("bottom");
+
+/**
+ * The orientation. The default orientation is "left", which means that the root
+ * node is placed on the left edge, leaf nodes appear on the right edge, and
+ * internal nodes are in-between. The following orientations are supported:<ul>
+ *
+ * <li>left - left-to-right.
+ * <li>right - right-to-left.
+ * <li>top - top-to-bottom.
+ * <li>bottom - bottom-to-top.
+ * <li>radial - radially, with the root at the center.</ul>
+ *
+ * @param {string} v the new orientation.
+ * @function
+ * @name pv.Layout.Arc.prototype.orient
+ * @returns {pv.Layout.Arc} this, or the current orientation.
+ */
+
+/**
+ * Whether this arc digram is directed (i.e., bidirectional); only applies to
+ * non-radial orientations. By default, arc digrams are undirected, such that
+ * all arcs appear on one side. If the arc digram is directed, then forward
+ * links are drawn on the conventional side (the same as as undirected
+ * links--right, left, bottom and top for left, right, top and bottom,
+ * respectively), while reverse links are drawn on the opposite side.
+ *
+ * @param {boolean} x whether or not this arc digram is directed.
+ * @function
+ * @name pv.Layout.Arc.prototype.directed
+ * @returns {pv.Layout.Arc} this, or the current directedness.
+ */
View
123 src/layout/Network.js
@@ -0,0 +1,123 @@
+/** @class Abstract layout for networks. */
+pv.Layout.Network = function() {
+ pv.Layout.call(this);
+ var that = this;
+
+ /**
+ * The node prototype. This prototype is intended to be used with a Dot mark
+ * in conjunction with the link prototype.
+ *
+ * @type pv.Mark
+ * @name pv.Layout.Network.prototype.node
+ */
+ this.node = new pv.Mark()
+ .data(function() { return that.nodes(); })
+ .strokeStyle("#1f77b4")
+ .fillStyle("#fff")
+ .left(function(n) { return n.left; })
+ .top(function(n) { return n.top; });
+
+ /** @private Propagate layout mark references to node children. */
+ this.node.add = function(type) {
+ var mark = that.parent.add(type).extend(this);
+ mark.link = that.link;
+ mark.node = that.node;
+ mark.label = that.label;
+ return mark;
+ };
+
+ /**
+ * The link prototype, which renders edges between child nodes and their
+ * parents. This prototype is intended to be used with a Line mark in
+ * conjunction with the node prototype.
+ *
+ * @type pv.Mark
+ * @name pv.Layout.Network.prototype.link
+ */
+ this.link = new pv.Mark()
+ .extend(this.node)
+ .data(function(p) { return [p.sourceNode, p.targetNode]; })
+ .fillStyle(null)
+ .lineWidth(function(d, p) { return p.linkValue * 1.5; })
+ .strokeStyle("rgba(0,0,0,.2)");
+
+ /** @private Propagate layout mark references to link children. */
+ this.link.add = function(type) {
+ var mark = that.parent.add(pv.Panel)
+ .data(function() { return that.links(); })
+ .add(type).extend(this);
+ mark.link = that.link;
+ mark.node = that.node;
+ mark.label = that.label;
+ return mark;
+ };
+
+ /**
+ * The node label prototype, which renders the node name adjacent to the node.
+ * This prototype is provided as an alternative to using the anchor on the
+ * node mark; it is primarily intended to be used with radial node-link
+ * layouts, since it provides a convenient mechanism to set the text angle.
+ *
+ * @type pv.Mark
+ * @name pv.Layout.Network.prototype.label
+ */
+ this.label = new pv.Mark()
+ .extend(this.node)
+ .textMargin(7)
+ .textBaseline("middle")
+ .text(function(n) { return n.nodeValue; })
+ .textAngle(function(n) {
+ var a = n.angle;
+ return pv.Wedge.upright(a) ? a : (a + Math.PI);
+ })
+ .textAlign(function(n) {
+ return pv.Wedge.upright(n.angle) ? "left" : "right";
+ });
+
+ /** @private Propagate layout mark references to label children. */
+ this.label.add = this.node.add;
+};
+
+/** @private Transform nodes and links on cast. */
+pv.Layout.Network.prototype = pv.extend(pv.Layout)
+ .property("nodes", function(v) {
+ return v.map(function(d, i) {
+ if (typeof d != "object") d = {nodeValue: d};
+ d.index = i;
+ d.linkDegree = 0;
+ return d;
+ });
+ })
+ .property("links", function(v) {
+ return v.map(function(d) {
+ if (isNaN(d.linkValue)) d.linkValue = isNaN(d.value) ? 1 : d.value;
+ return d;
+ });
+ });
+
+/** @private Register an initialization hook after all properties. */
+pv.Layout.Network.prototype.bind = function() {
+ this.def("init", this.init);
+ pv.Layout.prototype.bind.call(this);
+};
+
+/** @private Locks node and links after initialization. */
+pv.Layout.Network.prototype.init = function() {
+ var nodes = this.nodes(),
+ links = this.links(),
+ locks = this.scene.defs.locked;
+
+ /* Lock nodes and links, so they're not reset on render. */
+ if (locks.nodes) return true;
+ locks.nodes = true;
+ locks.links = true;
+
+ /* Compute link degrees; map source and target indexes to nodes. */
+ links.forEach(function(d) {
+ var s = d.sourceNode || (d.sourceNode = nodes[d.source]),
+ t = d.targetNode || (d.targetNode = nodes[d.target]),
+ v = d.linkValue;
+ s.linkDegree += v;
+ t.linkDegree += v;
+ });
+};
View
31 tests/layout/arc-directed.html
@@ -6,27 +6,24 @@
<body>
<script type="text/javascript+protovis">
-var vertices = [0, 1, 2, 3],
- edges = [{source:0, target:1}, {source:3, target:2}],
- graph = pv.Layout.arc(vertices, edges).directed(true);
-
var vis = new pv.Panel()
.width(400)
- .height(10)
- .top(110)
- .left(10)
- .right(10)
- .bottom(110);
+ .height(400)
+ .top(100)
+ .left(100)
+ .right(100)
+ .bottom(100);
vis.add(pv.Panel)
- .data(graph.links)
- .add(pv.Line)
- .extend(graph.link);
-
-vis.add(pv.Dot)
- .extend(graph.node)
- .add(pv.Label)
- .extend(graph.label);
+ .data(["top", "bottom", "left", "right"])
+ .add(pv.Layout.Arc)
+ .nodes([0, 1, 2, 3])
+ .links([{source:0, target:1}, {source:3, target:2}])
+ .directed(true)
+ .orient(pv.identity)
+ .link.add(pv.Line)
+ .node.add(pv.Dot)
+ .label.add(pv.Label);
vis.render();
View
19 tests/layout/arc-radial.html
@@ -7,9 +7,6 @@
<body>
<script type="text/javascript+protovis">
-var graph = pv.Layout.arc(miserables.vertices, miserables.edges)
- .orient("radial");
-
var vis = new pv.Panel()
.width(600)
.height(600)
@@ -18,16 +15,14 @@
.right(100)
.bottom(100);
-vis.add(pv.Panel)
- .data(graph.links)
- .add(pv.Line)
- .extend(graph.link);
-
-vis.add(pv.Dot)
- .extend(graph.node)
+vis.add(pv.Layout.Arc)
+ .nodes(miserables.vertices)
+ .links(miserables.edges)
+ .orient("radial")
+ .link.add(pv.Line)
+ .node.add(pv.Dot)
.size(function(d) d.linkDegree + 4)
- .add(pv.Label)
- .extend(graph.label);
+ .label.add(pv.Label);
vis.render();
View
18 tests/layout/arc.html
@@ -7,9 +7,6 @@
<body>
<script type="text/javascript+protovis">
-var graph = pv.Layout.arc(miserables.vertices, miserables.edges)
- .orient("bottom");
-
var vis = new pv.Panel()
.width(1000)
.height(400)
@@ -18,16 +15,13 @@
.right(100)
.bottom(100);
-vis.add(pv.Panel)
- .data(graph.links)
- .add(pv.Line)
- .extend(graph.link);
-
-vis.add(pv.Dot)
- .extend(graph.node)
+vis.add(pv.Layout.Arc)
+ .nodes(miserables.vertices)
+ .links(miserables.edges)
+ .link.add(pv.Line)
+ .node.add(pv.Dot)
.size(function(d) d.linkDegree + 4)
- .add(pv.Label)
- .extend(graph.label);
+ .label.add(pv.Label);
vis.render();
View
23 tests/layout/arc2.html
@@ -7,14 +7,6 @@
<body>
<script type="text/javascript+protovis">
-var vertices = pv.dom(diamond(3, 4, 4)).nodes(),
- edges = vertices.slice(1).map(function(n) {
- return {source: n.nodeName, target: n.parentNode.nodeName || 0};
- });
-
-var graph = pv.Layout.arc(vertices, edges)
- .orient("left");
-
var vis = new pv.Panel()
.width(400)
.height(400)
@@ -23,13 +15,14 @@
.right(10)
.bottom(10);
-vis.add(pv.Panel)
- .data(graph.links)
- .add(pv.Line)
- .extend(graph.link);
-
-vis.add(pv.Dot)
- .extend(graph.node);
+vis.add(pv.Layout.Arc)
+ .nodes(pv.dom(diamond(3, 4, 4)).nodes())
+ .links(function() this.nodes()
+ .filter(function(n) n.parentNode)
+ .map(function(n) ({sourceNode: n, targetNode: n.parentNode})))
+ .orient("left")
+ .link.add(pv.Line)
+ .node.add(pv.Dot);
vis.render();

0 comments on commit 9036038

Please sign in to comment.