Another attempt at better bounding box collision for d3 force.
forked from emeeks's block: More bounding box collide
| license: |
Another attempt at better bounding box collision for d3 force.
forked from emeeks's block: More bounding box collide
| // https://d3js.org/d3-force/ Version 1.0.0. Copyright 2016 Mike Bostock. | |
| (function (global, factory) { | |
| typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-quadtree'), require('d3-collection'), require('d3-dispatch'), require('d3-timer')) : | |
| typeof define === 'function' && define.amd ? define(['exports', 'd3-quadtree', 'd3-collection', 'd3-dispatch', 'd3-timer'], factory) : | |
| (factory((global.d3 = global.d3 || {}),global.d3,global.d3,global.d3,global.d3)); | |
| }(this, function (exports,d3Quadtree,d3Collection,d3Dispatch,d3Timer) { 'use strict'; | |
| function center(x, y) { | |
| var nodes; | |
| if (x == null) x = 0; | |
| if (y == null) y = 0; | |
| function force() { | |
| var i, | |
| n = nodes.length, | |
| node, | |
| sx = 0, | |
| sy = 0; | |
| for (i = 0; i < n; ++i) { | |
| node = nodes[i], sx += node.x, sy += node.y; | |
| } | |
| for (sx = sx / n - x, sy = sy / n - y, i = 0; i < n; ++i) { | |
| node = nodes[i], node.x -= sx, node.y -= sy; | |
| } | |
| } | |
| force.initialize = function(_) { | |
| nodes = _; | |
| }; | |
| force.x = function(_) { | |
| return arguments.length ? (x = +_, force) : x; | |
| }; | |
| force.y = function(_) { | |
| return arguments.length ? (y = +_, force) : y; | |
| }; | |
| return force; | |
| } | |
| function constant(x) { | |
| return function() { | |
| return x; | |
| }; | |
| } | |
| function jiggle() { | |
| return (Math.random() - 0.5) * 1e-6; | |
| } | |
| function x(d) { | |
| return d.x + d.vx; | |
| } | |
| function y(d) { | |
| return d.y + d.vy; | |
| } | |
| function collide(radius) { | |
| var nodes, | |
| radii, | |
| strength = 1, | |
| iterations = 1; | |
| if (typeof radius !== "function") radius = constant(radius == null ? 1 : +radius); | |
| function force() { | |
| var i, n = nodes.length, | |
| tree, | |
| node, | |
| xi, | |
| yi, | |
| ri, | |
| ri2; | |
| for (var k = 0; k < iterations; ++k) { | |
| tree = d3Quadtree.quadtree(nodes, x, y).visitAfter(prepare); | |
| for (i = 0; i < n; ++i) { | |
| node = nodes[i]; | |
| ri = radii[i], ri2 = ri * ri; | |
| xi = node.x + node.vx; | |
| yi = node.y + node.vy; | |
| tree.visit(apply); | |
| } | |
| } | |
| function apply(quad, x0, y0, x1, y1) { | |
| var data = quad.data, rj = quad.r, r = ri + rj; | |
| if (data) { | |
| if (data.index > i) { | |
| var x = xi - data.x - data.vx, | |
| y = yi - data.y - data.vy, | |
| l = x * x + y * y; | |
| if (l < r * r) { | |
| if (x === 0) x = jiggle(), l += x * x; | |
| if (y === 0) y = jiggle(), l += y * y; | |
| l = (r - (l = Math.sqrt(l))) / l * strength; | |
| node.vx += (x *= l) * (r = (rj *= rj) / (ri2 + rj)); | |
| node.vy += (y *= l) * r; | |
| data.vx -= x * (r = 1 - r); | |
| data.vy -= y * r; | |
| } | |
| } | |
| return; | |
| } | |
| return x0 > xi + r || x1 < xi - r || y0 > yi + r || y1 < yi - r; | |
| } | |
| } | |
| function prepare(quad) { | |
| if (quad.data) return quad.r = radii[quad.data.index]; | |
| for (var i = quad.r = 0; i < 4; ++i) { | |
| if (quad[i] && quad[i].r > quad.r) { | |
| quad.r = quad[i].r; | |
| } | |
| } | |
| } | |
| force.initialize = function(_) { | |
| var i, n = (nodes = _).length; radii = new Array(n); | |
| for (i = 0; i < n; ++i) radii[i] = +radius(nodes[i], i, nodes); | |
| }; | |
| force.iterations = function(_) { | |
| return arguments.length ? (iterations = +_, force) : iterations; | |
| }; | |
| force.strength = function(_) { | |
| return arguments.length ? (strength = +_, force) : strength; | |
| }; | |
| force.radius = function(_) { | |
| return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), force) : radius; | |
| }; | |
| return force; | |
| } | |
| function x$1(d) { | |
| return d.x + d.vx; | |
| } | |
| function y$1(d) { | |
| return d.y + d.vy; | |
| } | |
| function rectCollide(bbox) { | |
| var nodes, | |
| boundingBoxes, | |
| strength = 1, | |
| iterations = 1; | |
| if (typeof bbox !== "function") bbox = constant(bbox == null ? [[0,0][1,1]] : bbox); | |
| function force() { | |
| var i, | |
| n = nodes.length, | |
| tree, | |
| node, | |
| xi, | |
| yi, | |
| bbi, | |
| nx1, | |
| ny1, | |
| nx2, | |
| ny2 | |
| for (var k = 0; k < iterations; ++k) { | |
| tree = d3Quadtree.quadtree(nodes, x$1, y$1).visitAfter(prepare); | |
| for (i = 0; i < n; ++i) { | |
| var node = nodes[i]; | |
| bbi = boundingBoxes[i], | |
| xi = node.x + node.vx; | |
| yi = node.y + node.vy; | |
| nx1 = xi + bbi[0][0] | |
| ny1 = yi + bbi[0][1] | |
| nx2 = xi + bbi[1][0] | |
| ny2 = yi + bbi[1][1] | |
| tree.visit(apply); | |
| } | |
| } | |
| function apply(quad, x0, y0, x1, y1) { | |
| var data = quad.data, | |
| bWidth = bbLength(bbi, 0), | |
| bHeight = bbLength(bbi, 1); | |
| if (data) { | |
| if (data.index > ~~(i / 4)) { | |
| var bbj = boundingBoxes[data.index], | |
| dnx1 = data.x + data.vx + bbj[0][0], | |
| dny1 = data.y + data.vy + bbj[0][1], | |
| dnx2 = data.x + data.vx + bbj[1][0], | |
| dny2 = data.y + data.vy + bbj[1][1], | |
| dWidth = bbLength(bbj, 0), | |
| dHeight = bbLength(bbj, 1) | |
| if (nx1 <= dnx2 && dnx1 <= nx2 && ny1 <= dny2 && dny1 <= ny2) { | |
| var xSize = d3.extent([dnx1, dnx2, nx1, nx2]) | |
| var ySize = d3.extent([dny1, dny2, ny1, ny2]) | |
| var xOverlap = (bWidth + dWidth) - (xSize[1] - xSize[0]) | |
| var yOverlap = (bHeight + dHeight) - (ySize[1] - ySize[0]) | |
| var xBPush = xOverlap * strength * (xOverlap / bWidth) | |
| var yBPush = yOverlap * strength * (yOverlap / bHeight) | |
| var xDPush = xOverlap * strength * (xOverlap / dWidth) | |
| var yDPush = yOverlap * strength * (yOverlap / dHeight) | |
| if (xOverlap / bWidth < xOverlap / dWidth) { | |
| node.vx -= xBPush | |
| data.vx += xDPush | |
| } | |
| else { | |
| node.vx += xBPush | |
| data.vx -= xDPush | |
| } | |
| if ( yOverlap / bHeight < yOverlap / dHeight) { | |
| node.vy -= yBPush | |
| data.vy += yDPush | |
| } | |
| else { | |
| node.vy += yBPush | |
| data.vy -= yDPush | |
| } | |
| } | |
| } | |
| return; | |
| } | |
| return x0 > nx2 || x1 < nx1 || y0 > ny2 || y1 < ny1; | |
| } | |
| } | |
| function prepare(quad) { | |
| if (quad.data) return quad.bb = boundingBoxes[quad.data.index]; | |
| for (var i = quad.bb = [[0,0],[0,0]]; i < 4; ++i) { | |
| if (quad[i] && bbArea(quad[i].bb) > bbArea(quad.bb)) { | |
| quad.bb = quad[i].bb; | |
| } | |
| } | |
| } | |
| function bbArea(bbox) { | |
| return (bbox[1][0] - bbox[0][0]) * (bbox[1][1] - bbox[0][1]) | |
| } | |
| function bbLength(bbox, heightWidth) { | |
| return (bbox[1][heightWidth] - bbox[0][heightWidth]) | |
| } | |
| force.initialize = function(_) { | |
| var i, n = (nodes = _).length; boundingBoxes = new Array(n); | |
| for (i = 0; i < n; ++i) boundingBoxes[i] = bbox(nodes[i], i, nodes); | |
| }; | |
| force.iterations = function(_) { | |
| return arguments.length ? (iterations = +_, force) : iterations; | |
| }; | |
| force.strength = function(_) { | |
| return arguments.length ? (strength = +_, force) : strength; | |
| }; | |
| force.bbox = function(_) { | |
| return arguments.length ? (bbox = typeof _ === "function" ? _ : constant(+_), force) : bbox; | |
| }; | |
| return force; | |
| } | |
| function index(d, i) { | |
| return i; | |
| } | |
| function link(links) { | |
| var id = index, | |
| strength = defaultStrength, | |
| strengths, | |
| distance = constant(30), | |
| distances, | |
| nodes, | |
| count, | |
| bias, | |
| iterations = 1; | |
| if (links == null) links = []; | |
| function defaultStrength(link) { | |
| return 1 / Math.min(count[link.source.index], count[link.target.index]); | |
| } | |
| function force(alpha) { | |
| for (var k = 0, n = links.length; k < iterations; ++k) { | |
| for (var i = 0, link, source, target, x, y, l, b; i < n; ++i) { | |
| link = links[i], source = link.source, target = link.target; | |
| x = target.x + target.vx - source.x - source.vx || jiggle(); | |
| y = target.y + target.vy - source.y - source.vy || jiggle(); | |
| l = Math.sqrt(x * x + y * y); | |
| l = (l - distances[i]) / l * alpha * strengths[i]; | |
| x *= l, y *= l; | |
| target.vx -= x * (b = bias[i]); | |
| target.vy -= y * b; | |
| source.vx += x * (b = 1 - b); | |
| source.vy += y * b; | |
| } | |
| } | |
| } | |
| function initialize() { | |
| if (!nodes) return; | |
| var i, | |
| n = nodes.length, | |
| m = links.length, | |
| nodeById = d3Collection.map(nodes, id), | |
| link; | |
| for (i = 0, count = new Array(n); i < n; ++i) { | |
| count[i] = 0; | |
| } | |
| for (i = 0; i < m; ++i) { | |
| link = links[i], link.index = i; | |
| if (typeof link.source !== "object") link.source = nodeById.get(link.source); | |
| if (typeof link.target !== "object") link.target = nodeById.get(link.target); | |
| ++count[link.source.index], ++count[link.target.index]; | |
| } | |
| for (i = 0, bias = new Array(m); i < m; ++i) { | |
| link = links[i], bias[i] = count[link.source.index] / (count[link.source.index] + count[link.target.index]); | |
| } | |
| strengths = new Array(m), initializeStrength(); | |
| distances = new Array(m), initializeDistance(); | |
| } | |
| function initializeStrength() { | |
| if (!nodes) return; | |
| for (var i = 0, n = links.length; i < n; ++i) { | |
| strengths[i] = +strength(links[i], i, links); | |
| } | |
| } | |
| function initializeDistance() { | |
| if (!nodes) return; | |
| for (var i = 0, n = links.length; i < n; ++i) { | |
| distances[i] = +distance(links[i], i, links); | |
| } | |
| } | |
| force.initialize = function(_) { | |
| nodes = _; | |
| initialize(); | |
| }; | |
| force.links = function(_) { | |
| return arguments.length ? (links = _, initialize(), force) : links; | |
| }; | |
| force.id = function(_) { | |
| return arguments.length ? (id = _, force) : id; | |
| }; | |
| force.iterations = function(_) { | |
| return arguments.length ? (iterations = +_, force) : iterations; | |
| }; | |
| force.strength = function(_) { | |
| return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initializeStrength(), force) : strength; | |
| }; | |
| force.distance = function(_) { | |
| return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), initializeDistance(), force) : distance; | |
| }; | |
| return force; | |
| } | |
| function x$2(d) { | |
| return d.x; | |
| } | |
| function y$2(d) { | |
| return d.y; | |
| } | |
| var initialRadius = 10; | |
| var initialAngle = Math.PI * (3 - Math.sqrt(5)); | |
| function simulation(nodes) { | |
| var simulation, | |
| alpha = 1, | |
| alphaMin = 0.001, | |
| alphaDecay = 1 - Math.pow(alphaMin, 1 / 300), | |
| alphaTarget = 0, | |
| velocityDecay = 0.6, | |
| forces = d3Collection.map(), | |
| stepper = d3Timer.timer(step), | |
| event = d3Dispatch.dispatch("tick", "end"); | |
| if (nodes == null) nodes = []; | |
| function step() { | |
| tick(); | |
| event.call("tick", simulation); | |
| if (alpha < alphaMin) { | |
| stepper.stop(); | |
| event.call("end", simulation); | |
| } | |
| } | |
| function tick() { | |
| var i, n = nodes.length, node; | |
| alpha += (alphaTarget - alpha) * alphaDecay; | |
| forces.each(function(force) { | |
| force(alpha); | |
| }); | |
| for (i = 0; i < n; ++i) { | |
| node = nodes[i]; | |
| if (node.fx == null) node.x += node.vx *= velocityDecay; | |
| else node.x = node.fx, node.vx = 0; | |
| if (node.fy == null) node.y += node.vy *= velocityDecay; | |
| else node.y = node.fy, node.vy = 0; | |
| } | |
| } | |
| function initializeNodes() { | |
| for (var i = 0, n = nodes.length, node; i < n; ++i) { | |
| node = nodes[i], node.index = i; | |
| if (isNaN(node.x) || isNaN(node.y)) { | |
| var radius = initialRadius * Math.sqrt(i), angle = i * initialAngle; | |
| node.x = radius * Math.cos(angle); | |
| node.y = radius * Math.sin(angle); | |
| } | |
| if (isNaN(node.vx) || isNaN(node.vy)) { | |
| node.vx = node.vy = 0; | |
| } | |
| } | |
| } | |
| function initializeForce(force) { | |
| if (force.initialize) force.initialize(nodes); | |
| return force; | |
| } | |
| initializeNodes(); | |
| return simulation = { | |
| tick: tick, | |
| restart: function() { | |
| return stepper.restart(step), simulation; | |
| }, | |
| stop: function() { | |
| return stepper.stop(), simulation; | |
| }, | |
| nodes: function(_) { | |
| return arguments.length ? (nodes = _, initializeNodes(), forces.each(initializeForce), simulation) : nodes; | |
| }, | |
| alpha: function(_) { | |
| return arguments.length ? (alpha = +_, simulation) : alpha; | |
| }, | |
| alphaMin: function(_) { | |
| return arguments.length ? (alphaMin = +_, simulation) : alphaMin; | |
| }, | |
| alphaDecay: function(_) { | |
| return arguments.length ? (alphaDecay = +_, simulation) : +alphaDecay; | |
| }, | |
| alphaTarget: function(_) { | |
| return arguments.length ? (alphaTarget = +_, simulation) : alphaTarget; | |
| }, | |
| velocityDecay: function(_) { | |
| return arguments.length ? (velocityDecay = 1 - _, simulation) : 1 - velocityDecay; | |
| }, | |
| force: function(name, _) { | |
| return arguments.length > 1 ? ((_ == null ? forces.remove(name) : forces.set(name, initializeForce(_))), simulation) : forces.get(name); | |
| }, | |
| find: function(x, y, radius) { | |
| var i = 0, | |
| n = nodes.length, | |
| dx, | |
| dy, | |
| d2, | |
| node, | |
| closest; | |
| if (radius == null) radius = Infinity; | |
| else radius *= radius; | |
| for (i = 0; i < n; ++i) { | |
| node = nodes[i]; | |
| dx = x - node.x; | |
| dy = y - node.y; | |
| d2 = dx * dx + dy * dy; | |
| if (d2 < radius) closest = node, radius = d2; | |
| } | |
| return closest; | |
| }, | |
| on: function(name, _) { | |
| return arguments.length > 1 ? (event.on(name, _), simulation) : event.on(name); | |
| } | |
| }; | |
| } | |
| function manyBody() { | |
| var nodes, | |
| node, | |
| alpha, | |
| strength = constant(-30), | |
| strengths, | |
| distanceMin2 = 1, | |
| distanceMax2 = Infinity, | |
| theta2 = 0.81; | |
| function force(_) { | |
| var i, n = nodes.length, tree = d3Quadtree.quadtree(nodes, x$2, y$2).visitAfter(accumulate); | |
| for (alpha = _, i = 0; i < n; ++i) node = nodes[i], tree.visit(apply); | |
| } | |
| function initialize() { | |
| if (!nodes) return; | |
| var i, n = nodes.length; | |
| strengths = new Array(n); | |
| for (i = 0; i < n; ++i) strengths[i] = +strength(nodes[i], i, nodes); | |
| } | |
| function accumulate(quad) { | |
| var strength = 0, q, c, x, y, i; | |
| // For internal nodes, accumulate forces from child quadrants. | |
| if (quad.length) { | |
| for (x = y = i = 0; i < 4; ++i) { | |
| if ((q = quad[i]) && (c = q.value)) { | |
| strength += c, x += c * q.x, y += c * q.y; | |
| } | |
| } | |
| quad.x = x / strength; | |
| quad.y = y / strength; | |
| } | |
| // For leaf nodes, accumulate forces from coincident quadrants. | |
| else { | |
| q = quad; | |
| q.x = q.data.x; | |
| q.y = q.data.y; | |
| do strength += strengths[q.data.index]; | |
| while (q = q.next); | |
| } | |
| quad.value = strength; | |
| } | |
| function apply(quad, x1, _, x2) { | |
| if (!quad.value) return true; | |
| var x = quad.x - node.x, | |
| y = quad.y - node.y, | |
| w = x2 - x1, | |
| l = x * x + y * y; | |
| // Apply the Barnes-Hut approximation if possible. | |
| // Limit forces for very close nodes; randomize direction if coincident. | |
| if (w * w / theta2 < l) { | |
| if (l < distanceMax2) { | |
| if (x === 0) x = jiggle(), l += x * x; | |
| if (y === 0) y = jiggle(), l += y * y; | |
| if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l); | |
| node.vx += x * quad.value * alpha / l; | |
| node.vy += y * quad.value * alpha / l; | |
| } | |
| return true; | |
| } | |
| // Otherwise, process points directly. | |
| else if (quad.length || l >= distanceMax2) return; | |
| // Limit forces for very close nodes; randomize direction if coincident. | |
| if (quad.data !== node || quad.next) { | |
| if (x === 0) x = jiggle(), l += x * x; | |
| if (y === 0) y = jiggle(), l += y * y; | |
| if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l); | |
| } | |
| do if (quad.data !== node) { | |
| w = strengths[quad.data.index] * alpha / l; | |
| node.vx += x * w; | |
| node.vy += y * w; | |
| } while (quad = quad.next); | |
| } | |
| force.initialize = function(_) { | |
| nodes = _; | |
| initialize(); | |
| }; | |
| force.strength = function(_) { | |
| return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength; | |
| }; | |
| force.distanceMin = function(_) { | |
| return arguments.length ? (distanceMin2 = _ * _, force) : Math.sqrt(distanceMin2); | |
| }; | |
| force.distanceMax = function(_) { | |
| return arguments.length ? (distanceMax2 = _ * _, force) : Math.sqrt(distanceMax2); | |
| }; | |
| force.theta = function(_) { | |
| return arguments.length ? (theta2 = _ * _, force) : Math.sqrt(theta2); | |
| }; | |
| return force; | |
| } | |
| function x$3(x) { | |
| var strength = constant(0.1), | |
| nodes, | |
| strengths, | |
| xz; | |
| if (typeof x !== "function") x = constant(x == null ? 0 : +x); | |
| function force(alpha) { | |
| for (var i = 0, n = nodes.length, node; i < n; ++i) { | |
| node = nodes[i], node.vx += (xz[i] - node.x) * strengths[i] * alpha; | |
| } | |
| } | |
| function initialize() { | |
| if (!nodes) return; | |
| var i, n = nodes.length; | |
| strengths = new Array(n); | |
| xz = new Array(n); | |
| for (i = 0; i < n; ++i) { | |
| strengths[i] = isNaN(xz[i] = +x(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes); | |
| } | |
| } | |
| force.initialize = function(_) { | |
| nodes = _; | |
| initialize(); | |
| }; | |
| force.strength = function(_) { | |
| return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength; | |
| }; | |
| force.x = function(_) { | |
| return arguments.length ? (x = typeof _ === "function" ? _ : constant(+_), initialize(), force) : x; | |
| }; | |
| return force; | |
| } | |
| function y$3(y) { | |
| var strength = constant(0.1), | |
| nodes, | |
| strengths, | |
| yz; | |
| if (typeof y !== "function") y = constant(y == null ? 0 : +y); | |
| function force(alpha) { | |
| for (var i = 0, n = nodes.length, node; i < n; ++i) { | |
| node = nodes[i], node.vy += (yz[i] - node.y) * strengths[i] * alpha; | |
| } | |
| } | |
| function initialize() { | |
| if (!nodes) return; | |
| var i, n = nodes.length; | |
| strengths = new Array(n); | |
| yz = new Array(n); | |
| for (i = 0; i < n; ++i) { | |
| strengths[i] = isNaN(yz[i] = +y(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes); | |
| } | |
| } | |
| force.initialize = function(_) { | |
| nodes = _; | |
| initialize(); | |
| }; | |
| force.strength = function(_) { | |
| return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength; | |
| }; | |
| force.y = function(_) { | |
| return arguments.length ? (y = typeof _ === "function" ? _ : constant(+_), initialize(), force) : y; | |
| }; | |
| return force; | |
| } | |
| exports.forceCenter = center; | |
| exports.forceCollide = collide; | |
| exports.forceRectCollide = rectCollide; | |
| exports.forceLink = link; | |
| exports.forceManyBody = manyBody; | |
| exports.forceSimulation = simulation; | |
| exports.forceX = x$3; | |
| exports.forceY = y$3; | |
| Object.defineProperty(exports, '__esModule', { value: true }); | |
| })); |
| <html> | |
| <head> | |
| <title>d3v4 Simple Word Cloud</title> | |
| <meta charset="utf-8" /> | |
| <script src="https://d3js.org/d3.v4.min.js"></script> | |
| <script src="d3-force.js"></script> | |
| </head> | |
| <style> | |
| svg { | |
| height: 768px; | |
| width: 768px; | |
| border: 1px solid gray; | |
| } | |
| </style> | |
| <body> | |
| <div id="viz"> | |
| <svg class="main"> | |
| <defs> | |
| <linearGradient id="green"> | |
| <stop offset="0%" stop-color="#92E6CB"/> | |
| <stop offset="100%" stop-color="#61D3A5"/> | |
| </linearGradient> | |
| <linearGradient id="yellow"> | |
| <stop offset="0%" stop-color="#FFB140"/> | |
| <stop offset="100%" stop-color="#E19930"/> | |
| </linearGradient> | |
| <linearGradient id="red"> | |
| <stop offset="0%" stop-color="#EE6767"/> | |
| <stop offset="100%" stop-color="#D14C4C"/> | |
| </linearGradient> | |
| <linearGradient id="gray"> | |
| <stop offset="0%" stop-color="#8D98A2"/> | |
| <stop offset="100%" stop-color="#76818C"/> | |
| </linearGradient> | |
| <marker id="green-end" viewBox="0 -5 10 10" refX="-30" refY="0" markerWidth="6" markerHeight="6" orient="auto"> | |
| <path d="M0,-5L10,0L0,5" fill="#8BE3C5"></path> | |
| </marker> | |
| <marker id="yellow-end" viewBox="0 -5 10 10" refX="-30" refY="0" markerWidth="6" markerHeight="6" orient="auto"> | |
| <path d="M0,-5L10,0L0,5" fill="#FFB140"></path> | |
| </marker> | |
| <marker id="red-end" viewBox="0 -5 10 10" refX="-30" refY="0" markerWidth="6" markerHeight="6" orient="auto"> | |
| <path d="M0,-5L10,0L0,5" fill="#E55B5A"></path> | |
| </marker> | |
| <marker id="green-start" viewBox="0 -5 10 10" refX="-30" refY="0" markerWidth="6" markerHeight="6" orient="auto"> | |
| <path d="M0,-5L10,0L0,5" fill="#8BE3C5"></path> | |
| </marker> | |
| <filter id="dropshadow" filterUnits="userSpaceOnUse"> | |
| <feGaussianBlur in="SourceAlpha" stdDeviation="20"> | |
| </feGaussianBlur> | |
| <feOffset dx="0" dy="20"></feOffset> | |
| <feComponentTransfer> | |
| <feFuncA type="linear" slope="0.3"></feFuncA> | |
| </feComponentTransfer> | |
| <feMerge> | |
| <feMergeNode></feMergeNode> | |
| <feMergeNode in="SourceGraphic"></feMergeNode> | |
| </feMerge> | |
| </filter> | |
| </defs> | |
| </svg> | |
| </div> | |
| </body> | |
| <footer> | |
| <script> | |
| MARKER_GREEN = '#8BE3C5'; | |
| MARKER_YELLOW = '#FFB140' | |
| MARKER_RED = '#E55B5A' | |
| function startMarkerColor(idx) { | |
| switch (idx % 4) { | |
| case 1: return 'url(#green-start)' | |
| case 2: return null | |
| case 3: return null | |
| default: return null | |
| } | |
| } | |
| function endMarkerColor(idx) { | |
| switch (idx % 4) { | |
| case 1: return null | |
| case 2: return 'url(#yellow-end)' | |
| case 3: return 'url(#red-end)' | |
| default: return null | |
| } | |
| } | |
| function lineColor(idx) { | |
| switch (idx % 4) { | |
| case 1: return MARKER_GREEN | |
| case 2: return MARKER_YELLOW | |
| case 3: return MARKER_RED | |
| default: return null | |
| } | |
| } | |
| function nodeColor(node, idx) { | |
| // console.log('node=', node) | |
| switch (idx % 4) { | |
| case 1: return 'url(#green)' | |
| case 2: return 'url(#yellow)' | |
| case 3: return 'url(#red)' | |
| default: return 'url(#gray)' | |
| } | |
| } | |
| const HEIGHT = 768; | |
| const WIDTH = 768; | |
| const STRONG = 390; | |
| const USER_CENTER_X = WIDTH / 2 | |
| const USER_CENTER_Y = 10 + STRONG / 2 | |
| const GREEN = '#61D3A5'; | |
| const GRAY = '#8D98A2'; | |
| const FONT = 'Verdana'; | |
| const USER_RADIUS = 40; | |
| const svg = d3.select("svg.main") | |
| const colors = d3.scaleOrdinal(d3.schemeCategory20) | |
| const total = 20 | |
| const nodes = d3.range(total).map(i => { | |
| let n = { index: i, r: 30, x: WIDTH / 2, y: HEIGHT, name: `N${i}` } | |
| if (i == 0) n.r = USER_RADIUS | |
| return n | |
| }); | |
| const links = d3.range(total).map(i => { | |
| let n | |
| if ((i % 4) == 1) { | |
| n = { source: i, target: 0, value: i } | |
| } else { | |
| n = { source: 0, target: i, value: i } | |
| } | |
| return n | |
| }); | |
| // scales | |
| const vScale = d3.scaleLinear() | |
| .domain([0, total]) | |
| .range([USER_RADIUS, WIDTH / 2]) | |
| // svg | |
| const outerG = svg.append('g') | |
| .attr('transform',`translate(${WIDTH/2},${HEIGHT/2})`) | |
| const outerCirle = outerG.append('circle') | |
| .attr('x', WIDTH / 2) | |
| .attr('y', HEIGHT / 2) | |
| .attr('r', WIDTH / 2) | |
| .attr('fill', 'none') | |
| .attr('stroke', '#8D98A2') | |
| .attr('stroke-opacity', '0.2') | |
| const outerText = outerG.append('text') | |
| .text('Weak') | |
| .attr('text-anchor', 'middle') | |
| .attr('y', HEIGHT / 6) | |
| .attr('fill', GRAY) | |
| .attr('style', `font-family: ${FONT}; font-size: 16px;`) | |
| // | |
| const innerG = svg.append('g') | |
| .attr('transform', `translate(${USER_CENTER_X},${USER_CENTER_Y})`) | |
| const innerCircle = innerG.append('circle') | |
| .attr('x', WIDTH / 2) | |
| .attr('y', HEIGHT / 2) | |
| .attr('r', STRONG / 2) | |
| .attr('fill', GREEN) | |
| .attr('fill-opacity', '0.1') | |
| .attr('stroke', GREEN) | |
| .attr('stroke-opacity', '0.8') | |
| const innerText = innerG.append('text') | |
| .text('Strong') | |
| .attr('text-anchor', 'middle') | |
| .attr('y', 110) | |
| .attr('fill', GREEN) | |
| .attr('style', `font-family: ${FONT}; font-size: 16px;`) | |
| // | |
| const centerForce = d3.forceCenter(WIDTH / 2, HEIGHT / 2) | |
| const forceX = d3.forceX(USER_CENTER_X).strength(0.005) | |
| const forceY = d3.forceY(USER_CENTER_Y).strength(0.005) | |
| const collideForce = d3.forceCollide().radius(70).strength(0.2).iterations(100) | |
| const bodyForce = d3.forceManyBody().strength(-0.2) | |
| const linkForce = d3.forceLink().strength(0.3).iterations(100).distance(l => vScale(l.value)) | |
| const simulation = d3.forceSimulation() | |
| .velocityDecay(0.9) | |
| .force('charge', bodyForce) | |
| .force('collide', collideForce) | |
| .force('center', centerForce) | |
| .force('links', linkForce) | |
| .force('forceX', forceX) | |
| .force('forceY', forceY) | |
| const nodeCirles = svg.append("g") | |
| .attr("class", "nodes") | |
| .selectAll('circle') | |
| .filter('.friend') | |
| .data(nodes) | |
| .enter().append('circle') | |
| .attr('r', d => d.r) | |
| .attr('class', 'friend') | |
| .attr('fill', (d, i) => nodeColor(d, i)) | |
| .attr('style', 'filter: url("#dropshadow")') | |
| const nodeLinks = svg.append("g") | |
| .attr("class", "links") | |
| .selectAll("line") | |
| .data(links) | |
| .enter().append("line") | |
| .attr("marker-start", (d, i) => startMarkerColor(i)) | |
| .attr("marker-end", (d, i) => endMarkerColor(i)) | |
| .attr("stroke-width", d => 2) | |
| // .style("stroke", (d, i) => lineColor(i)) | |
| const nodeLabels = svg.selectAll("text") | |
| .filter('.node-label') | |
| .data(nodes) | |
| .enter() | |
| .append("text") | |
| .attr('class', 'node-label') | |
| .text(function (d) { return d.name; }) | |
| .attr('fill', '#fff') | |
| .attr('style', `font-family: ${FONT}; font-size: 16px;`) | |
| simulation | |
| .nodes(nodes) | |
| .on('tick', ticked) | |
| simulation.force("links").links(links) | |
| // let mutex = true | |
| function ticked (e) { | |
| nodeLinks | |
| .attr("x1", function(d) { return d.source.x }) | |
| .attr("y1", function(d) { return d.source.y }) | |
| .attr("x2", function(d) { return d.target.x }) | |
| .attr("y2", function(d) { return d.target.y }); | |
| nodeCirles | |
| .attr('cx', d => { | |
| // if (d.index == 1 && mutex) { | |
| // console.log(d) | |
| // mutex = false; | |
| // } | |
| return d.x | |
| }) | |
| .attr('cy', d => d.y) | |
| nodeLabels | |
| .attr("x", function(d){ return d.x }) | |
| .attr("y", function (d) {return d.y + 5.5; }) | |
| .style("text-anchor", "middle") | |
| nodes[0].x = USER_CENTER_X | |
| nodes[0].y = USER_CENTER_Y | |
| } | |
| </script> | |
| </footer> | |
| </html> |
| text | frequency | |
|---|---|---|
| layout | 63 | |
| function | 61 | |
| data | 47 | |
| return | 36 | |
| attr | 29 | |
| chart | 28 | |
| array | 24 | |
| style | 24 | |
| layouts | 22 | |
| values | 22 | |
| need | 21 | |
| nodes | 21 | |
| pie | 21 | |
| use | 21 | |
| figure | 20 | |
| circle | 19 | |
| we'll | 19 | |
| zoom | 19 | |
| append | 17 | |
| elements | 17 |