From dcd12d24496e38417ea4a6e53c2d11db2b979976 Mon Sep 17 00:00:00 2001 From: GerardoFurtado Date: Fri, 2 Feb 2018 12:05:26 +1100 Subject: [PATCH 1/3] Passing functions to radial.x() and radial.y() Currently, the radial force... d3.forceRadial(radius[, x][, y]) ... allows only numbers to the `x` and `y` positions: > If x is specified, sets the x-coordinate of the circle center to the specified number and returns this force. This proposed change allows passing functions to `radial.x()` and `radial.y()`, using the same pattern of other forces. Here is the working demo: https://bl.ocks.org/anonymous/0685343afe4675a022403be8728bc7c4/7dda06727201c128b9ff820b24c2c955e655436d --- src/radial.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/radial.js b/src/radial.js index 609516b..dbf2661 100644 --- a/src/radial.js +++ b/src/radial.js @@ -4,17 +4,19 @@ export default function(radius, x, y) { var nodes, strength = constant(0.1), strengths, - radiuses; + radiuses, + xs, + ys; if (typeof radius !== "function") radius = constant(+radius); - if (x == null) x = 0; - if (y == null) y = 0; + if (typeof x !== "function") x = constant(x == null ? 0 : +x); + if (typeof y !== "function") y = constant(y == null ? 0 : +y); function force(alpha) { for (var i = 0, n = nodes.length; i < n; ++i) { var node = nodes[i], - dx = node.x - x || 1e-6, - dy = node.y - y || 1e-6, + dx = node.x - xs[i] || 1e-6, + dy = node.y - ys[i] || 1e-6, r = Math.sqrt(dx * dx + dy * dy), k = (radiuses[i] - r) * strengths[i] * alpha / r; node.vx += dx * k; @@ -27,8 +29,12 @@ export default function(radius, x, y) { var i, n = nodes.length; strengths = new Array(n); radiuses = new Array(n); + xs = new Array(n); + ys = new Array(n); for (i = 0; i < n; ++i) { radiuses[i] = +radius(nodes[i], i, nodes); + xs[i] = +x(nodes[i], i, nodes); + ys[i] = +y(nodes[i], i, nodes); strengths[i] = isNaN(radiuses[i]) ? 0 : +strength(nodes[i], i, nodes); } } @@ -46,11 +52,11 @@ export default function(radius, x, y) { }; force.x = function(_) { - return arguments.length ? (x = +_, force) : x; + return arguments.length ? (x = typeof _ === "function" ? _ : constant(+_), initialize(), force) : x; }; force.y = function(_) { - return arguments.length ? (y = +_, force) : y; + return arguments.length ? (y = typeof _ === "function" ? _ : constant(+_), initialize(), force) : y; }; return force; From 795ba3163e94a2a6532188d988a8f571cfaf51e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 6 Jul 2020 18:51:53 +0200 Subject: [PATCH 2/3] document forceRadial.x() and .y() as setting accessor functions instead of numbers --- README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3e1db85..afa701b 100644 --- a/README.md +++ b/README.md @@ -451,8 +451,25 @@ The *radius* accessor is invoked for each [node](#simulation_nodes) in the simul # radial.x([x]) [<>](https://github.com/d3/d3-force/blob/master/src/radial.js "Source") -If *x* is specified, sets the *x*-coordinate of the circle center to the specified number and returns this force. If *x* is not specified, returns the current *x*-coordinate of the center, which defaults to zero. +If *x* is specified, sets the *x*-coordinate accessor to the specified number or function, re-evaluates the *x*-accessor for each node, and returns this force. If *x* is not specified, returns the current *x*-accessor, which defaults to: + +```js +function x() { + return 0; +} +``` + +The *x*-accessor is invoked for each [node](#simulation_nodes) in the simulation, being passed the *node* and its zero-based *index*. The resulting number is then stored internally, such that the target *x*-coordinate of each node is only recomputed when the force is initialized or when this method is called with a new *x*, and not on every application of the force. # radial.y([y]) [<>](https://github.com/d3/d3-force/blob/master/src/radial.js "Source") -If *y* is specified, sets the *y*-coordinate of the circle center to the specified number and returns this force. If *y* is not specified, returns the current *y*-coordinate of the center, which defaults to zero. +If *y* is specified, sets the *y*-coordinate accessor to the specified number or function, re-evaluates the *y*-accessor for each node, and returns this force. If *y* is not specified, returns the current *y*-accessor, which defaults to: + +```js +function y() { + return 0; +} +``` + +The *y*-accessor is invoked for each [node](#simulation_nodes) in the simulation, being passed the *node* and its zero-based *index*. The resulting number is then stored internally, such that the target *y*-coordinate of each node is only recomputed when the force is initialized or when this method is called with a new *y*, and not on every application of the force. + From ba02525a2a6f258933b09949b71e940a79c3e048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 6 Jul 2020 18:58:05 +0200 Subject: [PATCH 3/3] forceRadial().angle() sets the preferred angle as in https://observablehq.com/@fil/forcepolar closes #152 --- README.md | 12 +++++++--- src/math.js | 2 ++ src/radial.js | 66 ++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 60 insertions(+), 20 deletions(-) create mode 100644 src/math.js diff --git a/README.md b/README.md index afa701b..3f08a46 100644 --- a/README.md +++ b/README.md @@ -423,11 +423,11 @@ function y() { The *y*-accessor is invoked for each [node](#simulation_nodes) in the simulation, being passed the *node* and its zero-based *index*. The resulting number is then stored internally, such that the target *y*-coordinate of each node is only recomputed when the force is initialized or when this method is called with a new *y*, and not on every application of the force. -# d3.forceRadial(radius[, x][, y]) [<>](https://github.com/d3/d3-force/blob/master/src/radial.js "Source") +# d3.forceRadial(radius[, x][, y][, angle]) [<>](https://github.com/d3/d3-force/blob/master/src/radial.js "Source") [Radial Force](https://bl.ocks.org/mbostock/cd98bf52e9067e26945edd95e8cf6ef9) -Creates a new positioning force towards a circle of the specified [*radius*](#radial_radius) centered at ⟨[*x*](#radial_x),[*y*](#radial_y)⟩. If *x* and *y* are not specified, they default to ⟨0,0⟩. +Creates a new positioning force towards a circle of the specified [*radius*](#radial_radius) centered at ⟨[*x*](#radial_x),[*y*](#radial_y)⟩, and with a preferred [*angle*](#radial_angle). If *x* and *y* are not specified, they default to ⟨0,0⟩. If *radius* or *angle* are not specified (or null), they are ignored. # radial.strength([strength]) [<>](https://github.com/d3/d3-force/blob/master/src/radial.js "Source") @@ -445,7 +445,7 @@ The strength accessor is invoked for each [node](#simulation_nodes) in the simul # radial.radius([radius]) [<>](https://github.com/d3/d3-force/blob/master/src/radial.js "Source") -If *radius* is specified, sets the circle *radius* to the specified number or function, re-evaluates the *radius* accessor for each node, and returns this force. If *radius* is not specified, returns the current *radius* accessor. +If *radius* is specified, sets the circle *radius* to the specified number or function, re-evaluates the *radius* accessor for each node, and returns this force. If *radius* is not specified, returns the current *radius* accessor. If *angle* is null, the force ignores the radius (see [*radial*.angle](#radial_angle)). The *radius* accessor is invoked for each [node](#simulation_nodes) in the simulation, being passed the *node* and its zero-based *index*. The resulting number is then stored internally, such that the target radius of each node is only recomputed when the force is initialized or when this method is called with a new *radius*, and not on every application of the force. @@ -473,3 +473,9 @@ function y() { The *y*-accessor is invoked for each [node](#simulation_nodes) in the simulation, being passed the *node* and its zero-based *index*. The resulting number is then stored internally, such that the target *y*-coordinate of each node is only recomputed when the force is initialized or when this method is called with a new *y*, and not on every application of the force. +# radial.angle([angle]) [<>](https://github.com/d3/d3-force/blob/master/src/radial.js "Source") + +If *angle* is specified, sets the preferred *angle* to the specified number or function, re-evaluates the *angle* accessor for each node, and returns this force. If *angle* is not specified, returns the current *angle* accessor. If *angle* is null, the force ignores the preferred angle. + +The *angle* accessor is invoked for each [node](#simulation_nodes) in the simulation, being passed the *node* and its zero-based *index*. The resulting number is then stored internally, such that the target angle of each node is only recomputed when the force is initialized or when this method is called with a new *angle*, and not on every application of the force. + diff --git a/src/math.js b/src/math.js new file mode 100644 index 0000000..d38afca --- /dev/null +++ b/src/math.js @@ -0,0 +1,2 @@ +export var pi = Math.PI; +export var radians = pi / 180; diff --git a/src/radial.js b/src/radial.js index dbf2661..3fe3ff1 100644 --- a/src/radial.js +++ b/src/radial.js @@ -1,26 +1,52 @@ import constant from "./constant.js"; +import {radians} from "./math.js"; -export default function(radius, x, y) { +function value(x) { + if (typeof x === "function") return x; + if (x === null || x === undefined || isNaN(x = +x)) return; + return constant(x); +} + +export default function(radius, x, y, angle) { var nodes, strength = constant(0.1), strengths, - radiuses, + radii, xs, - ys; + ys, + angles; - if (typeof radius !== "function") radius = constant(+radius); - if (typeof x !== "function") x = constant(x == null ? 0 : +x); - if (typeof y !== "function") y = constant(y == null ? 0 : +y); + radius = value(radius); + x = value(x) || constant(0); + y = value(y) || constant(0); + angle = value(angle); function force(alpha) { for (var i = 0, n = nodes.length; i < n; ++i) { var node = nodes[i], dx = node.x - xs[i] || 1e-6, dy = node.y - ys[i] || 1e-6, - r = Math.sqrt(dx * dx + dy * dy), - k = (radiuses[i] - r) * strengths[i] * alpha / r; - node.vx += dx * k; - node.vy += dy * k; + r = Math.sqrt(dx * dx + dy * dy); + + if (radius) { + var k = ((radii[i] - r) * strengths[i] * alpha) / r; + node.vx += dx * k; + node.vy += dy * k; + } + + if (angle) { + var a = Math.atan2(dy, dx), + diff = angles[i] - a, + q = r * Math.sin(diff) * (strengths[i] * alpha); + + // the factor below augments the "unease" for points that are opposite + // the correct direction: in that case, though sin(diff) is small, + // tan(diff/2) is very high + q *= Math.hypot(1, Math.tan(diff / 2)); + + node.vx += -q * Math.sin(a); + node.vy += q * Math.cos(a); + } } } @@ -28,14 +54,16 @@ export default function(radius, x, y) { if (!nodes) return; var i, n = nodes.length; strengths = new Array(n); - radiuses = new Array(n); + radii = new Array(n); xs = new Array(n); ys = new Array(n); + angles = new Array(n); for (i = 0; i < n; ++i) { - radiuses[i] = +radius(nodes[i], i, nodes); + if (radius) radii[i] = +radius(nodes[i], i, nodes); xs[i] = +x(nodes[i], i, nodes); ys[i] = +y(nodes[i], i, nodes); - strengths[i] = isNaN(radiuses[i]) ? 0 : +strength(nodes[i], i, nodes); + if (angle) angles[i] = +angle(nodes[i], i, nodes) * radians; + strengths[i] = isNaN(radii[i]) ? 0 : +strength(nodes[i], i, nodes); } } @@ -44,19 +72,23 @@ export default function(radius, x, y) { }; force.strength = function(_) { - return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength; + return arguments.length ? (strength = value(_) || constant(1), initialize(), force) : strength; }; force.radius = function(_) { - return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), initialize(), force) : radius; + return arguments.length ? (radius = value(_), initialize(), force) : radius; }; force.x = function(_) { - return arguments.length ? (x = typeof _ === "function" ? _ : constant(+_), initialize(), force) : x; + return arguments.length ? (x = value(_) || constant(0), initialize(), force) : x; }; force.y = function(_) { - return arguments.length ? (y = typeof _ === "function" ? _ : constant(+_), initialize(), force) : y; + return arguments.length ? (y = value(_) || constant(0), initialize(), force) : y; + }; + +force.angle = function(_) { + return arguments.length ? (angle = value(_), initialize(), force) : y; }; return force;