d3.js radially position elements around an object - d3.js

I'm currently stuck at a problem with d3.js regarding the positioning.
This is the code I have right now
var center = {name: "sun", count: 20 }; //Will have more complex data in the future
var planets = [
{name: "Mercury", count: 2},
{name: "Venus", count: 3} ,
{name: "Earth", count: 5},
{name: "Mars", count: 4},
{name: "Jupiter", count: 11},
{name: "Saturn", count: 10},
{name: "Uranus", count: 7},
{name: "Neptune", count: 8} ];
var svg = d3.select("#planet-chart").append("svg")
.attr("width", 800)
.attr("height", 800);
var circleContainer = svg.selectAll("g mySolarText")
.data(planets);
var circleContainerEnter = circleContainer.enter()
.append("g")
.attr("transform", function(d,i){
return "translate("+ i*10 +",80)"
});
var circle = circleContainerEnter.append("circle")
.attr("r", function(d){return d.count * 5} )
.attr("cx", function(d,i){return (i+1) * 30} )
.attr("cy", function(d,i){return (i+1) * 30} )
.attr("stroke","black")
.attr("fill", "white");
circleContainerEnter.append("text")
.attr("dx", function(d){return -20})
.text(function(d){
return d.name}
);
Issues I am having right now are:
Currently, I can only pass the planets variable into the circle container, but I wish to also include the sun variable into it but I do not know how. I cannot simply include the sun variable into the planets array variable because I will need to put more data into it in the future.
I am having a hard time trying to figure out how to radially position the
planets around the sun, and add connecting lines to them which I am hoping to do. I have tried looking into arc but I am stuck.
For now, the sizes of the planets are being adjusted with their count values which I just multiply with the radius. Forgive me I am just a student and wish to learn more about d3 js. If you guys can help me, I would be so much grateful. Or if you can lead me to references I would be so much indebted.
Thank you so much in advance.

#tomshanley helped me via d3 official help forums. he posted this code for the fix to the issues I was encountering.
http://blockbuilder.org/tomshanley/840d381a9ca87ab404f63adda1ba8452
const radians = 0.0174532925
var center = {name: "sun", count: 20 };
var planets = [
{name: "Mercury", count: 2},
{name: "Venus", count: 3} ,
{name: "Earth", count: 5},
{name: "Mars", count: 4},
{name: "Jupiter", count: 11},
{name: "Saturn", count: 10},
{name: "Uranus", count: 7},
{name: "Neptune", count: 8} ];
var w = 800, h = 800;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
let orbitRadius = d3.scaleLinear()
.domain([0,8]) //number of planets
.range([90,w/2]) //you may need adjust this later
let angle = d3.scaleLinear()
.domain([0,9]) //number of planets + 1
.range([0,360]) //you may need adjust this later
svg.selectAll("line")
.data(planets)
.enter()
.append("line")
.attr("x1", function(d,i){
let a = angle(i);
let r = orbitRadius(i);
return xCoord = x(a, r) + (w/2)
})
.attr("y1", function(d,i){
let a = angle(i);
let r = orbitRadius(i);
return yCoord = y(a, r) + (h/2)
})
.attr("x2", (w/2))
.attr("y2", (h/2))
.style("stroke", "black")
.style("stroke-width", 2)
var circleContainer = svg.selectAll("g mySolarText")
.data(planets);
var circleContainerEnter = circleContainer.enter()
.append("g")
.attr("transform", function(d,i){
let a = angle(i);
let r = orbitRadius(i);
let xCoord = x(a, r) + (w/2)
let yCoord = y(a, r) + (h/2)
return "translate("+ xCoord +"," + yCoord + ")"
});
var circle = circleContainerEnter.append("circle")
.attr("r", function(d){return d.count * 5} )
.attr("cx", 0)
.attr("cy", 0)
.attr("stroke","black")
.attr("fill", "white");
circleContainerEnter.append("text")
.attr("dx", function(d){return -20})
.text(function(d){
return d.name}
);
function x (angle, radius) {
// change to clockwise
let a = 360 - angle
// start from 12 o'clock
return radius * Math.sin(a * radians)
}
function y (angle, radius) {
// change to clockwise
let a = 360 - angle
// start from 12 o'clock
return radius * Math.cos(a * radians)
}
again, thanks so much! :)

Related

d3 animating multiple lines the line can't be completed

I want to do an animation on the lines, but the second line will draw from two parts, one from begining, and the other from close to the second last point and disappear, so I got a result like this
I was following others'code
const pathLength = path.node().getTotalLength();
const transitionPath = d3.transition().ease(d3.easeQuad).duration(3000);
path
.attrs({
"stroke-dashoffset": pathLength,
"stroke-dasharray": pathLength,
})
.transition(transitionPath)
.attr("stroke-dashoffset", 0);
if you need all the code, I can paste, but it is really just this part that works with the animation, thank you!
I think you're accidentally using the same path length twice - namely that of the first path. path.node() returns the first node, even if there are multiple nodes in the selection.
const data = [{
category: "series_1",
values: [{
name: "A",
value: 10
},
{
name: "B",
value: 21
},
{
name: "C",
value: 19
},
{
name: "D",
value: 23
},
{
name: "E",
value: 20
},
],
}, ];
let counter = 1;
const add_set = (arr) => {
let copy = JSON.parse(JSON.stringify(arr[0]));
const random = () => Math.floor(Math.random() * 20 + 1);
const add = (arr) => {
counter++;
copy.values.map((i) => (i.value = random()));
copy.category = `series_${counter}`;
arr.push(copy);
};
add(arr);
};
add_set(data);
//No.1 define the svg
let graphWidth = 600,
graphHeight = 300;
let margin = {
top: 60,
right: 10,
bottom: 30,
left: 45
};
let totalWidth = graphWidth + margin.left + margin.right,
totalHeight = graphHeight + margin.top + margin.bottom;
let svg = d3
.select("body")
.append("svg")
.attr("width", totalWidth)
.attr("height", totalHeight);
//No.2 define mainGraph
let mainGraph = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//No.3 define axises
let categoriesNames = data[0].values.map((d) => d.name);
let xScale = d3
.scalePoint()
.domain(categoriesNames)
.range([0, graphWidth]); // scalepoint make the axis starts with value compared with scaleBand
let colorScale = d3.scaleOrdinal(d3.schemeCategory10);
colorScale.domain(data.map((d) => d.category));
let yScale = d3
.scaleLinear()
.range([graphHeight, 0])
.domain([
d3.min(data, (i) => d3.min(i.values, (x) => x.value)),
d3.max(data, (i) => d3.max(i.values, (x) => x.value)),
]); //* If an arrow function is simply returning a single line of code, you can omit the statement brackets and the return keyword
//No.4 set axises
mainGraph
.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + graphHeight + ")")
.call(d3.axisBottom(xScale));
mainGraph.append("g").attr("class", "y axis").call(d3.axisLeft(yScale));
//No.5 make lines
let lineGenerator = d3
.line()
.x((d) => xScale(d.name))
.y((d) => yScale(d.value))
.curve(d3.curveMonotoneX);
var lines = mainGraph
.selectAll(".path")
.data(data.map((i) => i.values))
.enter()
.append("path")
.attr("d", lineGenerator)
.attr("fill", "none")
.attr("stroke-width", 3)
.attr("stroke", (d, i) => colorScale(i));
//No.6 append circles
let circleData = data.map((i) => i.values);
mainGraph
.selectAll(".circle-container")
.data(circleData)
.enter()
.append("g")
.attr("class", "circle-container")
.attr("fill", (d, i) => console.log(d) || colorScale(i))
.selectAll("circle")
.data((d) => d)
.enter()
.append("circle")
.attrs({
cx: (d) => xScale(d.name),
cy: (d) => yScale(d.value),
r: 3,
opacity: 1,
});
// HERE we let the lines grow
lines
.attr("stroke-dasharray", function(d) {
// Get the path length of the current element
const pathLength = this.getTotalLength();
return `0 ${pathLength}`
})
.transition()
.duration(2500)
.attr("stroke-dasharray", function(d) {
// Get the path length of the current element
const pathLength = this.getTotalLength();
return `${pathLength} ${pathLength}`
});
.line {
stroke: blue;
fill: none;
}
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>

How to create curved lines connecting nodes in D3

I would like to create a graphic in D3 that consists of nodes connected to each other with curved lines. The lines should be curved differently depending on how far apart the start and end point of the line are.
For example (A) is a longer connection and therefore is less curved than (C).
Which D3 function is best used for this calculation and how is it output as SVG path
A code example (for example on observablehq.com) would help me a lot.
Here is a code example in obserbavlehq.com
https://observablehq.com/#garciaguillermoa/circles-and-links
I will try to explain it, let me know if there is something I am not clear enough:
Lets start with our circles, we use d3.pie() to position this circles, passing the data defined above, it will return us some arcs, but as we want circles instead of arcs, we use arc.centroid to get the coordinates of our circles
Value is required for the spacing in the pie layout that we use to calculate the position, if you want more circles, you will need to reduce the value, here is the related code:
pie = d3
.pie()
.sort(null)
.value((d) => {
return d.value;
});
arc = d3.arc().outerRadius(300).innerRadius(50);
data = [
{ id: 0, value: 10 },
{ id: 1, value: 10 },
{ id: 2, value: 10 },
{ id: 3, value: 10 },
{ id: 4, value: 10 },
{ id: 5, value: 10 },
{ id: 6, value: 10 },
{ id: 7, value: 10 },
{ id: 8, value: 10 },
{ id: 9, value: 10 },
];
const circles = [];
for(let item of pieData) {
const [x, y] = arc.centroid(item);
circles.push({x, y});
}
Now we can render the circles:
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const mainGroup = svg
.append("g")
.attr("id", "main")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Insert lines and circles groups, lines first so they are behind circles
const linesGroup = mainGroup.append("g").attr("id", "lines");
const circlesGroup = mainGroup.append("g").attr("id", "circles");
circlesGroup
.selectAll("circle")
.data(circles, (_, index) => index)
.join((enter) => {
enter
.append("circle")
.attr("id", (_, index) => {
return `circle-${index}`;
})
.attr("r", 20)
.attr("cx", (d) => {
return d.x;
})
.attr("cy", (d) => {
return d.y;
})
.style("stroke-width", "2px")
.style("stroke", "#000")
.style("fill", "#963cff");
});
Now we need to declare the links, we could do this with an array specifying the id of the source and destination (from and to). we use this to search each circle, get its coordinates (the source and destination of our links) and then create the links, in order to create them, we can use a path and the d3 method quadraticCurveTo, this function requires four parameters, the first two are "the control point" which defines our curve, we use 0, 0 as it is the center of our viz (it is the center because we used a translate in the parent group).
lines = [
{
from: 1,
to: 3,
},
{
from: 8,
to: 4,
},
];
for (let line of lines) {
const fromCircle = circles[line.from];
const toCircle = circles[line.to];
const fromP = { x: fromCircle.x, y: fromCircle.y };
const toP = { x: toCircle.x, y: toCircle.y };
const path = d3.path();
path.moveTo(fromP.x, fromP.y);
path.quadraticCurveTo(0, 0, toP.x, toP.y);
linesGroup
.append("path")
.style("fill", "none")
.style("stroke-width", "2px")
.style("stroke-dasharray", "10 10")
.style("stroke", "#000")
.attr("d", path);
}

d3js how to get rotated rect's corner coordinates?

I'm pretty new to d3js and feeling a little overwhelmed here. I'm trying to figure out how to query a rotated rectangle's corner coordinates so i can place a circle on that location (eventually I'm going to use that as a starting coordinate for a line to link to other nodes).
Here is an image showing what I'm trying to do:
Currently I'm getting the circle on the left of the svg boundary below, I'm trying to place it roughly where the x is below.
Here is my code for the circle:
let rx = node.attr("x");
let ry = node.attr("y");
g.append("circle")
.attr("cx",rx)
.attr("cy",ry)
.attr("r",5);
Here is my jsFiddle: jsFiddle and a Stack Overflow snippet
let d3Root = 'd3-cpm';
let w = document.documentElement.clientWidth;
let h = document.documentElement.clientHeight;
//TODO put type any
let eData = {
width: 180,
height: 180,
padding: 80,
fill: '#E0E0E0',
stroke: '#c3c5c5',
strokeWidth: 3,
hoverFill: '#1958b5',
hoverStroke: '#0046ad',
hoverTextColor: '#fff',
rx: 18,
ry: 18,
rotate: 45,
label: 'Decision Node',
textFill: 'black',
textHoverFill: 'white'
};
let cWidth;
let cHeight = h;
d3.select(d3Root)
.append("div")
.attr("id", "d3-root")
.html(function () {
let _txt = "Hello From D3! <br/>Frame Width: ";
let _div = d3.select(this);
let _w = _div.style("width");
cWidth = parseInt(_div.style("width"));
_txt += cWidth + "<br/> ViewPort Width: " + w;
return _txt;
});
let svg = d3.select(d3Root)
.append("svg")
.attr("width", cWidth)
.attr("height", cHeight)
.call(d3.zoom()
//.scaleExtent([1 / 2, 4])
.on("zoom", zoomed));
;
let g = svg.append("g")
.on("mouseover", function (d) {
d3.select(this)
.style("cursor", "pointer");
d3.select(this).select("rect")
.style("fill", eData.hoverFill)
.style("stroke", eData.hoverStroke);
d3.select(this).select("text")
.style("fill", eData.textHoverFill);
})
.on("mouseout", function (d) {
d3.select(this)
.style("cursor", "default");
d3.select(this).select("rect")
.style("fill", eData.fill)
.style("stroke", eData.stroke);
d3.select(this).select("text")
.style("fill", eData.textFill);
});
let node = g.append("rect")
.attr("width", eData.width)
.attr("height", eData.height)
.attr("fill", eData.fill)
.attr("stroke", eData.stroke)
.attr("stroke-width", eData.strokeWidth)
.attr("rx", eData.rx)
.attr("ry", eData.ry)
.attr("y", eData.padding)
.attr('transform', function () {
let _x = calcXLoc();
console.log(_x);
return "translate(" + _x + "," + "0) rotate(45)";
})
.on("click", ()=> {
console.log("rect clicked");
d3.event.stopPropagation();
//this.nodeClicked();
});
let nText = g.append('text')
.text(eData.label)
.style('fill', eData.textFill)
.attr('x', calcXLoc() - 50)
.attr('y', eData.width + 10)
.attr("text-anchor", "middle")
.on("click", ()=> {
console.log("text clicked");
d3.event.stopPropagation();
//this.nodeClicked();
});
let rx = node.attr("x");
let ry = node.attr("y");
g.append("circle")
.attr("cx",rx)
.attr("cy",ry)
.attr("r",5);
function calcXLoc() {
return (cWidth / 2 - eData.width / 2) + eData.width;
}
function zoomed() {
g.attr("transform", d3.event.transform);
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<d3-cpm></d3-cpm>
You're applying a transform to your rect to position and rotate it. It has no x attribute, so that comes back as undefined. This gets you slightly closer:
let rx = parseInt(node.attr("x"), 10) | 0;
let ry = parseInt(node.attr("y"), 10) | 0;
let height = parseInt(node.attr("height"), 10) | 0;
let transform = node.attr("transform");
g.append("circle")
.attr("cx",rx + height)
.attr("cy",ry + height)
.attr("transform", transform)
.attr("r",5);
But note that this is going to get kind of clunky and difficult to deal with - it'd be better if your data was modeled in such a way that the circular points were handled in there as well and could be somehow derived/transformed consistently....
Updated fiddle: https://jsfiddle.net/dcw48tk6/7/
Image:

stacking axes horizontally on top of each other

Thisis an example by Mike Bostock of a "simple" hive graph (as he refers to it in this article ). It has three "axis" created by this code
svg.selectAll(".axis")
.data(d3.range(3))
.enter().append("line")
.attr("class", "axis")
.attr("transform", function(d) { return "rotate(" + degrees(angle(d)) + ")"; })
.attr("x1", radius.range()[0])
.attr("x2", radius.range()[1]);
As you can see from the first link, the three "axes" form a circle, which seems to be accomplished by the rotation in the "transform" of the code above and use of these angle and degrees functions
var angle = d3.scale.ordinal().domain(d3.range(4)).rangePoints([0, 2 * Math.PI]),
function degrees(radians) {
return radians / Math.PI * 180 - 90;
}
Question: if there were only two "axes", how would it be possible (using a "translate") to stack the "axes" on top of each other (i.e. as two horizontal lines parallel to each other?
In my attempt to do this, I tried to remove the rotation of the "axis" and then to space them vertically. To stop the rotation,I removed the call to "degrees" like this
.attr("transform", function(d) { return "rotate(" + angle(d) + ")"; })
and I also set the range of the angles to be 0,0
d3.scale.ordinal().domain(["one", "two"]).range([0,0]);
then , to space the axes, I included a "translate" like this
.attr("transform", function(d) {return "translate(" + width /2 + "," + height/d + ")"});
The result is that there is one visible horizontal axis, and it seems the other one exists but is only detectable when I run the mouse over it ( and the nodes and lines haven't been repositioned)
Not sure if this is what you are after but two "axis" stacked vertically can be achieved with:
var angle = d3.scale.ordinal()
.domain(d3.range(3)) //<-- only calculate angles for 2 [-90, 90]
.rangePoints([0, 2 * Math.PI]),
...
svg.selectAll(".axis")
.data(d3.range(2)) //<-- 2 lines
EDITS
What are you are describing is not really a hive plot and attempting to re-purpose the layout is probably more trouble then it's worth. If you just want linked points on a line, here's an off-the-cuff implementation:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.link {
fill: none;
stroke-width: 1.5px;
}
.axis, .node {
stroke: #000;
stroke-width: 1.5px;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="d3.hive.min.js"></script>
<script>
var width = 500,
height = 500;
var lineSep = 200,
lineLen = 400,
color = d3.scale.category10().domain(d3.range(20)),
margin = [50,50];
var nodes = [
{x: 0, y: .1},
{x: 0, y: .9},
{x: 0, y: .2},
{x: 1, y: .3},
{x: 1, y: .1},
{x: 1, y: .8},
{x: 1, y: .4},
{x: 1, y: .6},
{x: 1, y: .2},
{x: 1, y: .7},
{x: 1, y: .8}
];
var links = [
{source: nodes[0], target: nodes[3]},
{source: nodes[1], target: nodes[3]},
{source: nodes[2], target: nodes[4]},
{source: nodes[2], target: nodes[9]},
{source: nodes[3], target: nodes[0]},
{source: nodes[4], target: nodes[0]},
{source: nodes[5], target: nodes[1]}
];
var nodeNest = d3.nest().key(function(d){ return d.x }).entries(nodes);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + margin[0] + "," + margin[1] + ")");
var axis = svg.selectAll(".axis")
.data(nodeNest);
var g = axis
.enter().append("g")
.attr("class", "axis")
.attr("transform", function(d,i) {
var t = "translate(0," + (i * lineSep) + ")";
return t;
})
.append("line")
.attr("x1", 0)
.attr("x2", lineLen);
axis.selectAll(".node")
.data(function(d){
d.values.forEach(function(q){
q.len = d.values.length;
})
return d.values;
})
.enter().append("circle")
.attr("class", "node")
.attr("cx", function(d, i, j) {
d.cx = ((i + 0.5) * (lineLen / d.len));
d.cy = (j * lineSep);
return d.cx;
})
.attr("r", 5)
.style("fill", function(d) { return color(d.x); });
svg.selectAll(".link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", function(d){
console.log(d);
var p = "";
p += "M" + d.source.cx + "," + d.source.cy;
p += "Q" + "0," + ((margin[1] / 2) + (lineSep/2));
p += " " + d.target.cx + "," + d.target.cy;
return p;
})
.style("stroke", function(d) {
return color(d.source.x);
});
function degrees(radians) {
return radians / Math.PI * 180 - 90;
}
</script>

Bind new variables to __data__, keep existing variables

I have (x,y) data bound to a couple of circles as follows:
var svg = d3.select("body")
.append("svg")
.attr("width", 640)
.attr("height", 400)
var data = [{x: 100, y: 100}, {x: 200, y: 200}]
var circles = svg.selectAll("circle").data(data)
.enter().append("circle")
.attr("r", 20)
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
I now want to bind a new variable z to each circle, using a simple index join.
newdata = [{z: 10}, {z: 30}]
circles.data(newdata)
If I now do circles.data() I see that the z variables have been bound as expected, but that x and y are no longer there.
Is there a way to have x, y AND z bound to the circles' data?
Note that the title of this SO question makes it sound similar, but it's not asking the same thing!
This can be done fairly easily with the selection.each function:
// Add a new variable, 'z', equal 2 * x
circles.each(function(d) { d.z = d.x * 2; });
Or, for the specific case where the new data to be bound is stored in a list of objects called newdata (using simple indexing to join the data):
circles.each(function(d, i) { d.z = newdata[i].z; });

Resources