Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
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...
commit 9036038a263766133dd23f01378a0ec011189e49 1 parent 602f97f
@mbostock authored
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();
Please sign in to comment.
Something went wrong with that request. Please try again.