I have been trying to replicate the example present in the link. So I have started out with two circles, one fixed and one rotating. But the rotating circle is drifting away from the stationary circle and some times it is falling into the stationary circle. Can someone please help me fix this. I have created a live demo of code at the following link.Below shared is the complete code.
// takes two points x1,y1,x2,y2 and return r and Θ
function coordinateToPolar(x1,y1,x2,y2){
polar={}
polar.r= Math.sqrt(Math.pow(x2-x1,2)+Math.pow(y2-y1,2))
polar.theta=Math.atan2(y2-y1,x2-x1)
return polar;
}
function degreeToRadians(degree){
return (degree/180)*Math.PI;
}
function radiansToDegrees(rad){
return (rad/Math.PI)*180
}
// takes one point h,k the center of circle and r and Θ
function polarToCoordinate(r,theta,h,k){
point={}
point.x=h+r*Math.cos(theta);
point.y=k+r*Math.sin(theta);
return point;
}
//circle ds
function Circle(r,h,k) {
this.r=r;
this.h=h;
this.k=k;
}
/// Adjusting the geometric center
bufferX=250;
bufferY=250;
svg=d3.select("body").append("svg");
startX=200;
startY=200;
startR=150;
dsCircles=[];
// intiating prevR and lastR to 0
prevR=0;
lastR=0;
for(i=0;i<2;i++){
currR=startR>>(i*1);
dsCircles[i]=new Circle(currR, i==0?bufferX+prevR+currR: bufferX+ prevR+lastR+currR,bufferY+startY)
lastR=currR;
prevR +=currR;
}
svg.selectAll("circles").data(dsCircles).enter().append("circle").attr("id",function(d,i){return "circle"+(i+1)}).attr("r",function(d){ return d.r}).attr("cy",function(d){return d.k}).attr("cx",function(d){ return d.h});
window.setInterval(function interval(){
// static variable initiated only once
if(interval["itr"]==undefined){
// initializing iteration counter to 0
interval.itr=0;
// getting the polar coordinates of the circles with respect to geometric center
interval.theta=coordinateToPolar(dsCircles[0].h,dsCircles[0].k,dsCircles[1].h,dsCircles[1].k).theta;
interval.r=coordinateToPolar(dsCircles[0].h,dsCircles[0].k,dsCircles[1].h,dsCircles[1].k).r;
}
d3.select("#circle2").attr("cx",function(d){
// assigning new center x-coordinate
return polarToCoordinate(interval.r,interval.theta,dsCircles[0].h,dsCircles[0].k).x;
}).attr("cy",function(d){
// assigning new center y-coordinate
return polarToCoordinate(interval.r,interval.theta +.03,dsCircles[0].h,dsCircles[0].k).y;
});
d3.select("#circle2").attr("style",function(d){ return interval.itr%2==0?"stroke:red":"stroke:blue"; })
interval.itr++;
interval.theta +=.003
},10)
Just remove angle shift for cy +.03 here:
attr("cy",function(d){
// assigning new center y-coordinate
return polarToCoordinate(interval.r,interval.theta +.03,
Interesting visualization.
This is not a direct answer to you question because I find your code overly confusing and troubling to debug. You seem to be doing way more math then you need; converting back from forth to different coordinate systems. Also, animating in a setTimeout is not a good practice.
Here's a quick refactor that takes advantage of d3.transition and simplifies the calculations. It also drives the addition of new circles through data.
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
var width = 500,
height = 500;
var svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform','translate(' + width/2 + ',' + height/2 + ')');
var data = [
{
r: 75,
x: 0,
y: 0,
c: 'black',
d: 0
}, {
r: 50,
x: 0,
y: 0,
c: 'steelblue',
d: 7000
},{
r: 30,
x: 0,
y: 0,
c: 'orange',
d: 5000
},{
r: 20,
x: 0,
y: 0,
c: 'red',
d: 2000
},{
r: 10,
x: 0,
y: 0,
c: 'green',
d: 500
}
];
data.forEach(function(d,i){
if (i === 0) d.pD = null;
else d.pD = data[i-1];
});
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('r', function(d){
return d.r;
})
.style('fill', 'none')
.style('stroke', function(d){
return d.c
})
.each(goRound);
function goRound(d,i){
if (!d.pD) return function(t) { }
var self = d3.select(this);
self.transition()
.ease(d3.easeLinear)
.duration(function(d){
return d.d;
})
.tween("go.round", function(){
var inter = d3.interpolate(0, Math.PI * 2);
return function(t) {
d.x = Math.cos(inter(t)) * (d.pD.r + d.r) + d.pD.x;
d.y = Math.sin(inter(t)) * (d.pD.r + d.r) + d.pD.y;
self.attr('cx', d.x);
self.attr('cy', d.y);
};
})
.on('end', goRound);
}
</script>
</body>
</html>
Related
I have some data (Nodes) that I need to draw. These nodes can overlap and thus the order in which they are drawn is important (the ones supposed to be displayed on top need to be drawn lastly).
The position and consequently the z-axis of those nodes is subject to change, that is why I tried to model this behavior by using a key, that incoporates the current index of the List the nodes are stored in.
case class Node(id: Int)
def addNodesToSVG = {
val sortedData: List[Node] = ???
val nodesSelection = d3.select("#nodes").selectAll(".node")
.data(sortedData.toJSArray, (n: Node) => {
n.id.toString +
// the position of those nodes may change over time
// that's why we need to include the position in the identifier
sortedData.indexOf(n)
}
nodesSelection.enter().append("g").attr("class", "node") // ...
nodesSelection
.attr("transform", transform) // ...
nodesSelection.exit().remove()
}
Unfortunatly, this does not seem to work as expected.
In theory this is how I thought this is going to work if I just have two nodes (n1 and n2), which are saved in a List(n1, n2)
node key
----- ---
n1 10 // node 1 at position 0
n2 21 // node 2 at position 1
Now if I change the List to List(n2, n1) and call addNodesToSVG again this is what I thought is going to happen:
node key
----- ---
n2 20 // node 1 at position 0
n1 12 // node 2 at position 1
Since these are unknown I thought it will remove (nodesSelection.exit().remove()) the old nodes and draw the 'new' ones in the correct order. This - however - is not happening. Why?
Edit after some more debugging I found out that my exit Selection is always of size 0.
I think id function should be used in a consistent manner -- just because an object changed its position, the result of the id's function on it shouldn't change (which as I see it is the whole point of using it in the first place). The approach I'd take would be making the id function to solely depend on node's id; add a field to data objects that specifies rendering order; sort selection after merging according to that new field.
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
</style>
</head>
<body>
<button onclick="sw()">Switch</button>
<script>
var d1 = [{
id: 'a',
z: 1,
fill: 'red',
y: 0
}, {
id: 'b',
z: 2,
fill: 'green',
y: 5
}];
var d2 = [{
id: 'a',
z: 2,
fill: 'red',
y: 5
}, {
id: 'b',
z: 1,
fill: 'green',
y: 0
}]
var current = 0;
var svg = d3.select("body").append("svg")
.attr("width", 100)
.attr("height", 100)
.attr('viewBox', '0 0 10 20');
function render(d) {
var r = svg.selectAll('rect')
.data(d, function(d) { return d.id; });
r.enter()
.append('rect')
.attr('width', 10)
.attr('height', 10)
.attr('fill', function(d) { return d.fill; })
.merge(r)
.sort(function(r1, r2) {
if (r1.z > r2.z) return 1;
if (r1.z < r2.z) return -1;
return 0;
})
.transition()
.attr('y', function(d) { return d.y; });
r.exit().remove();
};
function sw() {
if (current == 0) {
current = 1;
render(d2);
} else {
current = 0;
render(d1);
}
}
render(d1);
</script>
</body>
I have a tree visualisation in which I am trying to display paths between nodes that represent a distribution with multiple classes. I want to split the path lengthwise into multiple colours to represent the frequency of each distribution.
For example: say we have Class A (red) and Class B (black), that each have a frequency of 50. Then I would like a path that is half red and half black between the nodes. The idea is to represent the relative frequencies of the classes, so the frequencies would be normalised.
My current (naive) attempt is to create a separate path for each class and then use an x-offset. It looks like this.
However, as shown in the image, the lines do not maintain an equal distance for the duration of the path.
The relevant segment of code:
linkGroup.append("path").attr("class", "link")
.attr("d", diagonal)
.style("stroke", "red")
.style("stroke-width", 5)
.attr("transform", function(d) {
return "translate(" + -2.5 + "," + 0.0 + ")"; });
linkGroup.append("path").attr("class", "link")
.attr("d", diagonal)
.style("stroke", "black")
.style("stroke-width", 5)
.attr("transform", function(d) {
return "translate(" + 2.5 + "," + 0.0 + ")"; });
It would be great if anyone has some advice.
Thanks!
A possible solution is to calculate the individual paths and fill with the required color.
Using the library svg-path-properties from geoexamples.com you can calculate properties (x,y,tangent) of a path without creating it first like it is done in this SO answer (this does not calculate the tangent).
The code snippet does it for 2 colors but it can be easy generalized for more.
You specify the colors, percentage and width of the stroke with a dictionary
var duoProp = { color: ["red", "black"], percent: 0.30, width: 15 };
percent is the amount color[0] takes from the stroke width.
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
The pathPoints parameters
path that needs to be stroked, can be generated by d3.line path example from SO answer
var lineGenerator = d3.line().x(d=>d[0]).y(d=>d[1]).curve(d3.curveNatural);
var curvePoints = [[0,0],[0,10],[20,30]];
var duoPath = pathPoints(lineGenerator(curvePoints), 10, duoProp);
path length interval at which to sample (unit pixels). Every 10 pixels gives a good approximation
dictionary with the percent and width of the stroke
It returns an array with the paths to be filled, 1 for each color.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/svg-path-properties#0.4.4/build/path-properties.min.js"></script>
</head>
<body>
<svg id="chart" width="350" height="350"></svg>
<script>
var svg = d3.select("#chart");
function pathPoints(path, stepLength, duoProp) {
var props = spp.svgPathProperties(path);
var length = props.getTotalLength();
var tList = d3.range(0, length, stepLength);
tList.push(length);
var tProps = tList.map(d => props.getPropertiesAtLength(d));
var pFactor = percent => (percent - 0.5) * duoProp.width;
tProps.forEach(p => {
p.x0 = p.x - pFactor(0) * p.tangentY;
p.y0 = p.y + pFactor(0) * p.tangentX;
p.xP = p.x - pFactor(duoProp.percent) * p.tangentY;
p.yP = p.y + pFactor(duoProp.percent) * p.tangentX;
p.x1 = p.x - pFactor(1) * p.tangentY;
p.y1 = p.y + pFactor(1) * p.tangentX;
});
var format1d = d3.format(".1f");
var createPath = (forward, backward) => {
var fp = tProps.map(p => forward(p));
var bp = tProps.map(p => backward(p));
bp.reverse();
return 'M' + fp.concat(bp).map(p => `${format1d(p[0])},${format1d(p[1])}`).join(' ') + 'z';
}
return [createPath(p => [p.x0, p.y0], p => [p.xP, p.yP]), createPath(p => [p.xP, p.yP], p => [p.x1, p.y1])]
}
var duoProp = { color: ["red", "black"], percent: 0.30, width: 15 };
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
</script>
</body>
</html>
As a quick follow-up to rioV8's excellent answer, I was able to get their code working but needed to generalise it to work with more than two colours. In case someone else has a similar requirement, here is the code:
function pathPoints(path, stepLength, duoProp) {
// get the properties of the path
var props = spp.svgPathProperties(path);
var length = props.getTotalLength();
// build a list of segments to use as approximation points
var tList = d3.range(0, length, stepLength);
tList.push(length);
var tProps = tList.map(function (d) {
return props.getPropertiesAtLength(d);
});
// incorporate the percentage
var pFactor = function pFactor(percent) {
return (percent - 0.5) * duoProp.width;
};
// for each path segment, calculate offset points
tProps.forEach(function (p) {
// create array to store modified points
p.x_arr = [];
p.y_arr = [];
// calculate offset at 0%
p.x_arr.push(p.x - pFactor(0) * p.tangentY);
p.y_arr.push(p.y + pFactor(0) * p.tangentX);
// calculate offset at each specified percent
duoProp.percents.forEach(function(perc) {
p.x_arr.push(p.x - pFactor(perc) * p.tangentY);
p.y_arr.push(p.y + pFactor(perc) * p.tangentX);
});
// calculate offset at 100%
p.x_arr.push(p.x - pFactor(1) * p.tangentY);
p.y_arr.push(p.y + pFactor(1) * p.tangentX);
});
var format1d = d3.format(".1f");
var createPath = function createPath(forward, backward) {
var fp = tProps.map(function (p) {
return forward(p);
});
var bp = tProps.map(function (p) {
return backward(p);
});
bp.reverse();
return 'M' + fp.concat(bp).map(function (p) {
return format1d(p[0]) + "," + format1d(p[1]);
}).join(' ') + 'z';
};
// create a path for each projected point
var paths = [];
for(var i=0; i <= duoProp.percents.length; i++) {
paths.push(createPath(function (p) { return [p.x_arr[i], p.y_arr[i]]; }, function (p) { return [p.x_arr[i+1], p.y_arr[i+1]]; }));
}
return paths;
}
// generate the line
var duoProp = { color: ["red", "blue", "green"], percents: [0.5, 0.7], width: 15 };
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
Note that the percents array specifies the cumulative percentage of the stroke, not the individual percentages of the width. E.g. in the example above, the red stroke will span 0% to 50% width, the blue stroke 50% to 70% width and the green stroke 70% to 100% width.
I'm experimenting with D3 version 4 force directed graphs and have looked at Jim Vallandingham's tutorial and code as a starting point.
http://vallandingham.me/bubble_chart_v4/
and am attempting to produce an animation similar to the example here from Nathan Yau
https://flowingdata.com/2016/08/23/make-a-moving-bubbles-chart-to-show-clustering-and-distributions/
I've stripped the bubble chart from Jim Vallandingham's code to what I think I need and can display the individual states by changing the index value, but for some reason the code does not want to animate between the different states. I assume the redraw function isn't working. It may be an obvious error or one made through complete ignorance, but if you can help it would be great.
Here's my code:
function bubbleChart() {
var width = 940;
var height = 600;
var center = { x: width / 2, y: height / 3 };
var years = ["0","2008", "2009", "2010"];
var yearCenters = {
2008: { x: width / 3, y: 2 * height / 3 },
2009: { x: width / 2, y: 2 * height / 3 },
2010: { x: 2 * width / 3, y: 2 * height / 3 }
};
// #v4 strength to apply to the position forces
var forceStrength = 0.03;
// These will be set in create_nodes and create_vis
var svg = null;
var bubbles = null;
var nodes = [];
var index= 0;
function charge(d) {
return -Math.pow(d.radius, 2.3) * forceStrength;
}
// Here we create a force layout
var simulation = d3.forceSimulation()
.velocityDecay(0.2)
.force('x', d3.forceX().strength(forceStrength).x(center.x))
.force('y', d3.forceY().strength(forceStrength).y(center.y))
.force('charge', d3.forceManyBody().strength(charge))
.on('tick', ticked);
// #v4 Force starts up automatically, which we don't want as there aren't any nodes yet.
simulation.stop();
// Nice looking colors
var fillColor = d3.scaleOrdinal()
.domain(['low', 'medium', 'high'])
.range(['#d84b2a', '#beccae', '#7aa25c']);
function createNodes(rawData) {
var myNodes = rawData.map(function (d) {
return {
id: d.id,
radius: 5,
value: +d.total_amount,
name: d.grant_title,
org: d.organization,
group: d.group,
year: d.start_year,
x: Math.random() * 900,
y: Math.random() * 800
};
});
// sort them to prevent occlusion of smaller nodes.
myNodes.sort(function (a, b) { return b.value - a.value; });
return myNodes;
}
/*
* Main entry point to the bubble chart.
*/
var chart = function chart(selector, rawData) {
// convert raw data into nodes data
nodes = createNodes(rawData);
// Create a SVG element inside the provided selector
// with desired size.
svg = d3.select(selector)
.append('svg')
.attr('width', width)
.attr('height', height);
// Bind nodes data to what will become DOM elements to represent them.
bubbles = svg.selectAll('.bubble')
.data(nodes, function (d) { return d.id; });
// Create new circle elements each with class `bubble`.
// There will be one circle.bubble for each object in the nodes array.
// Initially, their radius (r attribute) will be 0.
// #v4 Selections are immutable, so lets capture the
// enter selection to apply our transtition to below.
var bubblesE = bubbles.enter().append('circle')
.classed('bubble', true)
.attr('r', 0)
.attr('fill', function (d) { return fillColor(d.group); })
.attr('stroke', function (d) { return d3.rgb(fillColor(d.group)).darker(); })
.attr('stroke-width', 2)
// #v4 Merge the original empty selection and the enter selection
bubbles = bubbles.merge(bubblesE);
// Fancy transition to make bubbles appear, ending with the
// correct radius
bubbles.transition()
.duration(2000)
.attr('r', function (d) { return d.radius; });
// Set the simulation's nodes to our newly created nodes array.
// #v4 Once we set the nodes, the simulation will start running automatically!
simulation.nodes(nodes);
chart.redraw();
};
// Callback function that is called after every tick of the force simulation.
// These x and y values are modified by the force simulation.
function ticked() {
bubbles
.attr('cx', function (d) { return d.x; })
.attr('cy', function (d) { return d.y; });
}
chart.redraw = function (index){
simulation.force('x', d3.forceX().strength(forceStrength).x(nodePosX));
simulation.force('y', d3.forceY().strength(forceStrength).y(nodePosY));
simulation.alpha(1).restart();
}
function nodePosX(d) {
if (+d.year <= +years[index]) {
return yearCenters[d.year].x;
} else {
return center.x;
}
}
function nodePosY(d) {
if (+d.year <= +years[index]) {
return yearCenters[d.year].y;
} else {
return center.y;
}
}
// return the chart function from closure.
return chart;
}
var myBubbleChart = bubbleChart();
myBubbleChart('#vis', data);
for (i=0;i<4;i++){
setInterval(function(){myBubbleChart.redraw(i);}, 100);
}
I misunderstood how to use setInterval to redraw the chart, so it should be as follows:
var i = 0;
setInterval(function(){myBubbleChart.redraw(i++);}, 1000);
This question is about d3 version 3.x and path movements.
Imagine there is a path and a circle element, and I want the circle to follow that path in a transition, but only up to some percentage.
I was asking this before and got a great answer from Gerardo Furtado here: My former question
Still, one question in this regards remains for me, and as I am a beginner, I couldn't find any working solution so far:
How can i trace this path, lets say, from a point at 25% to a point at 50%, and then later from a point at 50% to a point at 60%?
These numbers are just examples, any percentage value should be possible.
I need to avoid that the path movement always starts from position 0, but instead i want to start path movement from the current position the circle has reached already.
Hopefully I could express my question clear enough.
Thank you very much for any insight and help.
Well, I have to agree with LeBeau, and I see that you also do. Since I answered your last question, I just needed to do some minor changes in the function. However, keep his advise in mind for the next times: when asking a question, show us some code you've tried, even if it doesn't work, because it shows effort.
Back to the question.
For this solution, I'll wrap everything inside a function named move, which accepts two arguments, the initial position and the final position (both in percentages):
function move(initialPosition, finalPosition) {
The initial position, as the name implies, set the initial position of the circle along the path. The math is this:
var start = path.node()
.getPointAtLength(path.node().getTotalLength() * initialPosition);
Then, I slightly changed the function from my last answer to accept the initial and final positions:
function translateAlong(path) {
var l = path.getTotalLength() * (finalPosition - initialPosition);
return function() {
return function(t) {
var p = path.getPointAtLength(t * l +
(path.getTotalLength() * initialPosition));
return "translate(" + p.x + "," + p.y + ")";
};
};
}
Here is the demo. Clicking on the button calls move with 0.25 (initial position) and 0.5 (final position) as arguments:
var points = [
[240, 100],
[290, 200],
[340, 50],
[390, 150],
[90, 150],
[140, 50],
[190, 200]
];
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 300);
var path = svg.append("path")
.data([points])
.attr("d", d3.svg.line()
.tension(0) // Catmull–Rom
.interpolate("cardinal-closed"));
var color = d3.scale.category10();
var dataPositions = [{
initial: 0.25,
final: 0.5
}, {
initial: 0.5,
final: 0.6
}];
svg.selectAll(".point")
.data(points)
.enter().append("circle")
.attr("r", 4)
.attr("transform", function(d) {
return "translate(" + d + ")";
});
d3.select("button").on("click", function() {
move(0.25, 0.5);
});
function move(initialPosition, finalPosition) {
var start = path.node().getPointAtLength(path.node().getTotalLength() * initialPosition);
var circle = svg.append("circle")
.attr("r", 13)
.attr("fill", function(d, i) {
return color(i)
})
.attr("transform", "translate(" + start.x + "," + start.y + ")");
circle.transition()
.duration(1000)
.attrTween("transform", function() {
return translateAlong(path.node())()
});
function translateAlong(path) {
var l = path.getTotalLength() * (finalPosition - initialPosition);
return function() {
return function(t) {
var p = path.getPointAtLength(t * l +
(path.getTotalLength() * initialPosition));
return "translate(" + p.x + "," + p.y + ")";
};
};
}
}
path {
fill: none;
stroke: #000;
stroke-width: 3px;
}
circle {
stroke: #fff;
stroke-width: 3px;
}
<script src="//d3js.org/d3.v3.min.js"></script>
<button>Move</button>
<br>
PS: The function in this answer does not accept values bigger than 1. But you can try to change it if you need to do more than "one lap" in the path.
I have a dataset that looks like this:
var shapes = [
{
type: 'rect', // which shape to draw
size: [ 1, 100 ], // width and height (for rect) or single number for lines, triangles or squares
color: color[0],
orientation: 0 // in degrees in interval [0, 360[
},
{
type: 'triangle',
size: 55,
color: color[1],
orientation: 0
},
{
type: 'triangle',
size: 96,
color: color[0],
orientation: 0
}
// etc …
]
What I want to do is draw all of the shapes in the dataset, which is of variable length and randomly generated, as defined by the various properties in the different objects defining the shapes. The shapes should be equally distributed and not overlap each other.
The data is bound to a surrounding g-element like this:
var viewport = d3.select('body').append('svg').selectAll('g').data(shapes)
var group = viewport.append('g')
How do I approach this the d3 way? I have tried shapes.filter(shape => shape.type === 'rect').forEach(/* ... */) but it feels like I'm not doing it the d3 way. Thanks for any clues on how to approach this!
I'd go with paths, and a function that'd return the path regarding the d.type attribute.
Edit : something a little bit like that, although you'll have to specify somehow the way you want the symbols to be positioned because with this example, they'll just be drawn on top of each other.
var drawers = {
rect: function(d) {
return 'M 0 0 l '+ d.size[0] + ' 0 l 0 ' + d.size[1] + ' l -' + d.size[0] + ' 0 l 0 -' + d.size[1];
},
triangle: function(d) {},
};
var g = d3.select('#mySvg').append('g');
var symbols = g.selectAll('.symbol')
.data(shapes);
symbols.enter()
.append('path')
.classed('symbol', true)
.attr({
d: function(d) {return drawers[d.type](d);}
});
The final solution was to use the d3.svg.symbol() constructor (given that shapes is the array as described in the introductory post, with the slight difference of type being either triangle-up, circle or square:
const center = [ w / 2, h / 2 ]
const vis = d3.select(container)
.append('svg')
.attr('width', w)
.attr('height', h)
const symbols = vis.selectAll('.symbol').data(shapes)
const getPathForShape = d => d3.svg.symbol().type(d.type).size(d.size)()
const paths = symbols.enter()
.append('path')
.classed('symbol', true)
.attr('d', getPathForShape)
.attr('fill', d => d.color)
.attr('x', center[0])
.attr('y', center[1])
they were then distributed by using a force directed graph:
const force = d3.layout.force()
.size([w, h])
.charge(-100)
.nodes(shapes)
.gravity(0.1)
.on('tick', _ => paths.attr('transform', d =>
'translate(' + d.x + ',' + d.y + ')'))
// simulate a static graph:
force.start()
for (var i = 0; i < 100; i++) force.tick()
force.stop()