I've made a chart with multiple y-axis values that are being plotted as circles. I now want to connect each y-axis value set with lines (a line chart basically).
I think my error lies in how I am attempting to define the y-axis line value:
var line = d3.svg.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y1, d.y2, d.y3); });
Here is my dataset:
var dataset = [
{x: d3.time.hour.utc.offset(now, -5), y1: 1, y2: 3, y3: 2},
{x: d3.time.hour.utc.offset(now, -4), y1: 2, y2: 2, y3: 4},
{x: d3.time.hour.utc.offset(now, -3), y1: 3, y2: 3, y3: 1},
{x: d3.time.hour.utc.offset(now, -2), y1: 4, y2: 1, y3: 2},
{x: d3.time.hour.utc.offset(now, -1), y1: 5, y2: 5, y3: 3},
{x: now, y1: 6, y2: 4, y3: 3},
];
And here is a complete example of my graph as of now: http://jsbin.com/edikeg/1/edit
I've read the line() method api reference but am not sure what else to try. If anyone could recommend the best approach to take to accomplish this, as well as any addition d3.js starting out tips or beginner resources to look into I would greatly appreciate it.
Thanks
First, your dataset is in a non-optimal format. A better format would be to create a single object for each line/point set:
var dataset = [
[
{x: d3.time.hour.utc.offset(now, -5), y: 1},
{x: d3.time.hour.utc.offset(now, -4), y: 2},
{x: d3.time.hour.utc.offset(now, -3), y: 3},
{x: d3.time.hour.utc.offset(now, -2), y: 4},
{x: d3.time.hour.utc.offset(now, -1), y: 5},
{x: now, y: 1}
],
[
// y2-values paired with x-values
],
[
// y3-values paired with x-values
]
];
Why, you ask? Because creating objects and groups of objects with d3 will be easier. You could also just write a map function to convert your current data into this format, if you need to.
Now, with your data in an easier format, the code for creating the lines and circles won't be dependent on the number of items in your dataset. Check out a working, rewritten version here:
http://jsbin.com/edikeg/3/edit
Basic explanation of what's happening:
For each item in the dataset, we create g.line, a pathContainer to hold each path and each of its circle points.
var pathContainers = svg.selectAll('g.line')
.data(dataset);
pathContainers.enter().append('g')
.attr('class', 'line');
Then, in each pathContainer, we create a path.
pathContainers.selectAll('path')
.data(function (d) { return [d]; }) // continues the data from the pathContainer
.enter().append('path')
.attr('d', d3.svg.line()
.x(function (d) { return xScale(d.x); })
.y(function (d) { return yScale(d.y); })
.interpolate('monotone')
);
Last, we create a circle for each point in the dataset for each pathContainer
pathContainers.selectAll('circle')
.data(function (d) { return d; })
.enter().append('circle')
.attr('cx', function (d) { return xScale(d.x); })
.attr('cy', function (d) { return yScale(d.y); })
.attr('r', 5);
Hope this helps!
Related
I'm trying to learn d3 very quickly and I'm getting pretty stuck on selection and joining.
I want to be able to draw an axis with dots for each member of an array. Some of the array members have the same x value, but I still want to see as many dots as there are with that value. My array (in React with useState) looks like so:
const [data, setData] = useState(
[
{x: 2020, colour: "purple", y1: 0.001, y2: 0.63},
{x: 2027, colour: "red", y1: 0.003, y2: 0.84},
{x: 2031, colour: "yellow", y1: 0.024, y2: 0.56},
{x: 2031, colour: "green", y1: 0.054, y2: 0.22},
{x: 2040, colour: "blue", y1: 0.062, y2: 0.15},
{x: 2050, colour: "orange", y1: 0.062, y2: 0.15}
]
);
You can see there are two values for 2031 and I want to draw a yellow dot, then a purple dot below, at the x axis tick labelled "2031".
So I group my data with this reduce function (purloined from SO):
const dot = data.reduce(
(r, v, _, __, k = v.x) => ((r[k] || (r[k] = [])).push(v), r),
{}
);
...which produces this:
{ 2020: [{x: 2020, colour: "purple", y1: 0.001, y2: 0.63}],
2027: [...] }
I initiate my x axis and create a placeholder for it:
const g = d3.axisBottom( scX ).tickValues(
data.map(d => {
return d.x
})
)
svg.append( "g" )
.attr( "transform", "translate(" + 25 + "," + pxY/2 + ")")
.call( g )
.selectAll(".tick")
And then I want to call my dot variable and iterate over the nested arrays:
svg
.selectAll(".tick")
.call( dot )
.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 5)
.attr("fill", dot.colour)
What I would like this to do is draw a circle for every one of the nested arrays, with a fill the colour named in that array - this does not work?
Could anyone explain?
There is no need to group your data. You can just see data as an array, where every element will correspond to one circle. Multiple circles can exist with the same x-value, nothing enforces that they can't.
There is also no need to set the axis ticks like that, d3 will most likely do everything for you. d3-axis is an absolute convenience - you're meant to tweak the defaults, not build everything from scratch here.
You need to learn about data joins, since you apparently also don't know that you can access the data of an element using function(d, i) { ... } or (d, i) => ... to set the colour that way.
const data = [{
x: 2020,
colour: "purple",
y1: 0.001,
y2: 0.63
},
{
x: 2027,
colour: "red",
y1: 0.003,
y2: 0.84
},
{
x: 2031,
colour: "yellow",
y1: 0.024,
y2: 0.56
},
{
x: 2031,
colour: "green",
y1: 0.054,
y2: 0.22
},
{
x: 2040,
colour: "blue",
y1: 0.062,
y2: 0.15
},
{
x: 2050,
colour: "orange",
y1: 0.062,
y2: 0.15
}
];
const width = 600,
height = 300;
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
const x = d3.scaleLinear()
.domain(d3.extent(data, d => d.x))
.range([50, 550]);
const y1 = d3.scaleLinear()
.domain(d3.extent(data, d => d.y1))
.range([275, 25]);
const y2 = d3.scaleLinear()
.domain(d3.extent(data, d => d.y2))
.range([3, 10]);
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("fill", d => d.colour)
.attr("cx", d => x(d.x))
.attr("cy", d => y1(d.y1))
.attr("r", d => y2(d.y2));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
I'm trying to get coordinates of circles (cx, cy) after using d3-force collision, but I still got original positions. Are there any ways for getting translated(after collision check) coordinates of circles?
Below is just summary of code.
let data = [
{ 'x': 100, 'y': 100, 'radius': 10, cluster: 4 },
{ 'x': 100, 'y': 100, 'radius': 6, cluster: 3 },
{ 'x': 50, 'y': 200, 'radius': 10, cluster: 2 },
{ 'x': 350, 'y': 200, 'radius': 10, cluster: 2 },
{ 'x': 100, 'y': 300, 'radius': 20, cluster: 3 },
{ 'x': 100, 'y': 300, 'radius': 25, cluster: 1 },
{ 'x': 100, 'y': 300, 'radius': 33, cluster: 2 }];
var collisionForce = d3.forceCollide((d) => d.radius).strength(1).iterations(100);
var simulation = d3.forceSimulation(data).alphaDecay(0.01).force('collisionForce', collisionForce)
.force('center', d3.forceCenter(width / 2, height / 2));
var node = svg.selectAll('circle').data(data)
.enter().append('circle')
.attr('r', function (d) {
return d.radius;
}).attr('cx', function (d) {
return d.x;
}).attr('cy', function (d) {
return d.y;
})
.attr('cluster', d => d.cluster)
.attr('fill', d => that.palette[d.cluster]);
function ticked() {
node.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
});
}
simulation.on('tick', ticked);
You can listen to the end event on a simulation, and then access the final coordinates for the circles by changing the last line of your code to this:
simulation
.on('tick', ticked)
.on('end', () => {
d3.selectAll('circle').each(function () {
const thisD3 = d3.select(this)
console.log(thisD3.attr('cx'), thisD3.attr('cy'))
})
})
According to my knowledge, the array passed to d3.forceSimulation edits the array in place, so you could also access the x and y coordinates directly in that array. The key in either case is do it when the simualtion triggers the end event.
Hope this helps!
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! :)
Say I have a path I created with d3 something like:
line = d3.line()
.curve(d3.curveLinear)
.x(function(d) { return x(d.x);})
.y(function(d) { return y(d.y); });
data = [{x: 0, y: 0}, {x: 5, y: 5}, {x:10, y:10}];
myLine = svg.append("path")
.attr("fill", "none")
.attr("stroke", "steelblue")
.datum(data)
.attr("d", line);
This makes a nice diagonal line from 0 to 10. Now if I update my data to make some changes and add some points:
data = [{x: 1, y: 1}, {x:2, y:3}, {x: 6, y: 7}, {x:9, y:9}];
And update my line
myLine.datum(data).transition().duration(1000).attr("d", line);
It gives a weird transition where it slides the existing path to fit the first three points of the new path and awkwardly adds the final point to the end.
Similarly, if I update it to have fewer points, it shortens the line and then slides the remainder over, instead of just reshaping the line where it is.
I understand why this happens, but I'm wondering if there's a way to create a more smooth transition.
You should have a look at this page and this github
.attrTween('d', function () {
return d3.interpolatePath(line(data), line(data2));
});
Have a look at this fiddle
I am a total beginner to d3.js so please be kind :)
considering this jsbin example
I have the following dataset:
var dataset = [
[d3.time.hour.utc.offset(now, -5), 1, 10],
[d3.time.hour.utc.offset(now, -4), 2, 20],
[d3.time.hour.utc.offset(now, -3), 3, 30],
[d3.time.hour.utc.offset(now, -2), 4, 40],
[d3.time.hour.utc.offset(now, -1), 5, 50],
[now, 6, 60],
];
Two questions.
Does d3 provide a better approach to finding the max value for my y-axis data (all columns but the 0th, the 0th column is x-axis (time)) in my dataset array? Currently I am just looping through the entire dataset array and making a second array, excluding the first column. Perhaps there is a better datastructure other than an array I should be using for this entirely?
var data_arr = [];
for (row in dataset){
for (col=1;col < dataset[row].length; col++){
data_arr.push(dataset[row][col]);
}
}
var yScale = d3.scale.linear()
.domain([0, d3.max(data_arr)])
.range([h - padding, padding]);
Once thats resolved, I still need to determine how to graph multiple y-axis values in general! This worked fine before I needed multiple y-axis values:
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r", 2);
Please take a look at the graph w/ code here now for full context: http://jsbin.com/edatol/1/edit
Any help is appreciated!
I've made a couple of changes to your example and you can see the results at http://jsbin.com/edatol/2/edit.
First, I modified your data a little bit. This is mostly just a style thing, but I find it's easier to work with objects instead of arrays:
//Static dataset
var dataset = [
{x: d3.time.hour.utc.offset(now, -5), y1: 1, y2: 10},
{x: d3.time.hour.utc.offset(now, -4), y1: 2, y2: 20},
{x: d3.time.hour.utc.offset(now, -3), y1: 3, y2: 30},
{x: d3.time.hour.utc.offset(now, -2), y1: 4, y2: 40},
{x: d3.time.hour.utc.offset(now, -1), y1: 5, y2: 50},
{x: now, y1: 6, y2: 60},
];
Then you can find your domains and ranges like this:
var xDomain = d3.extent(dataset, function(i) { return i.x; });
var maxY = d3.max(dataset, function(i) { return Math.max(i.y1, i.y2); });
Then to add multiple y-values, you just have to append an additional circle with the appropriate values. I gave them different classes so that you can use that to select them if you want to do transitions or updates later on.
//Create circles
svg.selectAll(".y1")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) { return xScale(d.x); })
.attr("cy", function(d) { return yScale(d.y1); })
.attr("class", "y1")
.attr("r", 2);
//Create circles
svg.selectAll(".y2")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) { return xScale(d.x); })
.attr("cy", function(d) { return yScale(d.y2); })
.attr("class", "y2")
.attr("r", 2);