I am trying to draw a set of lines and circle points, but I cant figure out how to get the circles to work.
The line function needs an array of points, but for the circle it needs just the x/y of each point.
How do I append a circle (to the same group as the line), for each x/y point?
// Data join
var join = svg.selectAll("g")
.data(lineData)
// Enter
var group = join.enter()
.append("g");
group.append("path")
.attr('stroke', 'blue')
.attr('stroke-width', 2)
.attr('fill', 'none');
group.append('circle')
.attr("r", 10)
.attr('fill', 'blue');
// Update
join.select("path")
.attr('d', line);
join.select("circle")
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); });
Full code is here: http://jsfiddle.net/dxxddvL4/1/
The basic pattern you need to use are nested selections -- for each line, there are multiple circles. It's easier to do the lines and circles separately, lines and g elements first:
var join = svg.selectAll("g")
.data(lineData);
// Enter
join.enter()
.append("g")
.append("path")
.attr('stroke', 'blue')
.attr('stroke-width', 2)
.attr('fill', 'none');
// Update
join.select("path")
.attr('d', line);
join.exit().remove();
The code is basically the same as yours, except that the appended g elements aren't saved in a separate selection and the exit selection is handled by removing the elements. Now the circles, along the same lines:
var circles = join.selectAll("circle")
.data(function(d) { return d; });
circles.enter()
.append('circle')
.attr("r", 10)
.attr('fill', 'blue');
circles.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); });
circles.exit().remove();
The first line here is the nested selection -- for each element in the array that denotes the line, we want a circle. Note that this is operating on the update selection of the g elements. This is ok because the elements in the enter selection are merged into the update selection when the g elements are appended. That is, even though we only handle the update selection, any newly-appended elements are included in this.
After that, we handle the selections as usual. The enter selection has elements appended, the update selection sets the coordinates, the exit selection removes elements. All the magic happens in that first line, where we tell D3 to, for each g element at the top level, bind each point from the line to any circles underneath.
Complete example here.
Related
I have some data with 2 attributes: colour and value
I use the D3 enter selection to create circle elements, and append them to the body of the page. Their fill colour is determined by the "colour" attribute.
Then, I append text elements to the page. The text contents are determined by the "value" attribute.
Here is what I am working with:
// Set up svg element
var svg = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 300)
.style("background", "lightblue");
var dataset = [
{"colour":"red", "value":"First set of text"},
{"colour":"green", "value":"Second attempt"},
{"colour":"blue", "value":"Third and final!"}
];
// Create circles from the data
// On mouseover, give them a border (remove on mouseout)
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("r", 40)
.attr("cy", function(d, i) { return i*80 + 40; })
.attr("cx", 50)
.style("fill", function(d) {return d.colour;})
// HERE
// Can I somehow show and hide the text component that is
// associated with this circle when the circle is hovered, rather
// than the text itself?
.on("mouseover", function(d) {
d3.select(this).style("stroke", "black")
.style("stroke-width", 2)
})
.on("mouseout", function(d) {d3.select(this).style("stroke", "none")});
// Now add the text for each circle
// Same thing with mouseover and mouseout
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.attr("text-anchor", "middle")
.attr("y", function(d, i) { return i*80 + 40; })
.attr("x", 50)
.style("opacity", 0)
.on("mouseover", function(d) {d3.select(this).style("opacity", 1)})
.on("mouseout", function(d) {d3.select(this).style("opacity", 0)})
.text(function(d) { return d.value;});
I would like for the text to be hidden, until the associated circle is hovered over. How can I connect the text element with a particular circle, so that I can toggle whether the text is shown by hovering over the circle?
This fiddle below is an outline of what I am trying to do, and what I have got so far. I have the text showing up only when hovered, but not when the circle is hovered.
https://jsfiddle.net/aj4zpn6z/
There are several ways for achieving this. Since both circles and texts use the same dataset, my solution uses filter.
First, let's name the variables for the texts and circles:
var circles = svg.selectAll("circle")
//etc...
var texts = svg.selectAll("text")
//etc...
Then, inside the circles mouseover function, we filter the texts that have the same colour attribute:
.on("mouseover", function(d){
d3.select(this).style("stroke", "black").style("stroke-width", 2);
var tempTexts = texts.filter(function(e){
return e.colour === d.colour
});
tempTexts.style("opacity", 1);
});
This is your updated fiddle: https://jsfiddle.net/wxh95e9u/
So I have points on my SVG map and now I would like to show text next to them. This is a jsfiddle with 2 points and showing their ID text. But as you can see there is no text somehow.
var featureCollection = topojson.feature(topology, topology.objects.testtest);
lines.append("g")
.attr("id", "lines")
.selectAll("path")
.data(featureCollection.features)
.enter().append("path")
.attr("d", path)
.append("text")
.attr("class", "nodetext")
.attr("x", 22)
.attr("y", 4)
.text(function (d) {
return d.properties.id;
});
And I checked it with some other text beside example I already have here. It's working in the same way.
So does it not work with pathes? Could that be?
A 'text' element can't be a child of a 'path' element, it should be a sibling. Group them if they are related and need to be positioned accordingly.
As #liamness says, your text can't be a child of path but needs to be a sibling. Your problem goes a little further, though, since you are using a path and you can't group and position the element conventionally. There is where path.centroid comes in handy. It allows you to find the center of you path and position your text there:
var e = lines.append("g")
.attr("id", "lines")
.selectAll("path")
.data(featureCollection.features)
.enter(); // save enter selection
e.append("path") // add path as child of lines g
.attr("d", path);
e.append("text") // add text as child of lines g, sibling of path
.attr("class", "nodetext")
.attr('x', function(d,i){
return path.centroid(d)[0]; // horizontal center of path
})
.attr('y', function(d,i){
return path.centroid(d)[1] + 13; // vertical center of path
})
.attr('text-anchor','middle')
.text(function (d) {
return d.properties.id;
});
Updated fiddle.
I'm using d3 to create a map and add some data on it in. So far, I managed to draw circles based on the data that I pull from database. What I want to do is, when I mouseover one of those circle, create a new bigger circle with some text on it. I was able to draw the bigger circle but couldn't figure out how to add the label or text on it.
This is how I add circles to the map.
for (var i = 0; i < data.length; i++) {
var coordinates = projection([data[i]["Longitude"], data[i]["Latitude"]]);
svg.append('svg:circle')
.attr('cx', coordinates[0])
.attr('cy', coordinates[1])
.attr('r', 5)
.attr('fill', 'black')
.attr('class', 'pClass')
.attr('id', data[i]["Id"])
.attr('dataId', data[i]["DataId"])
.on('mouseover', dataMouseover);
}
Here is the mouseover event
function dataMouseover() {
var id = $(this).attr('id');
var d= $.grep(data, function (e) { return e.Id == id; });
var coordinates = projection([d[0]["Longitude"], d[0]["Latitude"]]);
svg.append('svg:circle')
.attr('cx', coordinates[0])
.attr('cy', coordinates[1])
.attr('r', 120)
.attr('fill', 'darkblue')
.attr('class', 'pClass')
.attr('id', data[0]["Id"] + "popUp")
.attr('dataId', plaques[0]["DataId"])
.attr("stroke", "white")
.attr("stroke-width", "5")
.on('mouseout', function () {
d3.select(this).remove();
});
}
So, I'm also removing the bigger circle when mouse is out. What I want is to put a text in that circle from the data while drawing it in there.
UPDATE: I updated my code to change current circle's radius instead of drawing new one.
for (var i = 0; i < data.length; i++) {
var coordinates = projection([data[i]["Longitude"], data[i]["Latitude"]]);
svg.append('svg:circle')
.attr('cx', coordinates[0])
.attr('cy', coordinates[1])
.attr('r', 5)
.attr('fill', 'black')
.attr('class', 'pClass')
.attr('id', data[i]["Id"])
.attr('dataId', data[i]["DataId"])
.on('click', function () {
$("#dialog").dialog('open');
})
.on('mouseover', function (data) {
var sel = d3.select(this);
sel.moveToFront();
d3.select(this)
.transition()
.duration(200)
.attr('fill', 'darkblue')
.attr('r', 120)
.attr('stroke', 'white')
.attr('stroke-width', '5')
})
.on('mouseout', function () {
d3.select(this)
.transition()
.duration(200)
.attr('fill', 'black')
.attr('r', 5)
.attr('stroke', 'none')
.attr('stroke-width', '0')
});
}
Still could use some guidance on how to use g element to cover both circle and text in my case.
You can't actually add text inside an svg circle. I discovered this to my own chagrin as well a few weeks ago. :\
Instead, encapsulate both the circle and the text inside a g element. Here's a link to a SO post explaining exactly how to do that: d3 add text to circle
Steps to going about doing this:
On hover: Instead of appending a circle, first append a g element.
Then append a circle element to that g element
Then append a text element to that g element
??? (stylize the elements as you will...)
Profit from having it look like the text is inside the circle!
Also: instead of redrawing the entire circle on mouseover, see if you can just change the r attr of the circle on mouseover. Might save you some code & make your app update a bit faster.
//add circles with price data
svgContainer.selectAll("circle")
.data(priceData)
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y1(d); })
//add circles with difficulty data
svgContainer.selectAll("circle")
.data(difficultyData)
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y2(d); })
In the first half, circles with price data are added along the relevant line in the graph chart. Now I want to do the same with the second half to add circles with different data to a different line. However, the first circles' data are overwritten by the second circles' data, and the second circles never get drawn.
I think I have a gut feeling of what's going on here, but can someone explain what exactly is being done and how to solve the problem?
possible reference:
"The key function also determines the enter and exit selections: the
new data for which there is no corresponding key in the old data
become the enter selection, and the old data for which there is no
corresponding key in the new data become the exit selection. The
remaining data become the default update selection."
First, understand what selectAll(), data(), enter() do from this great post.
The problem is that since circle element already exists by the time we get to the second half, the newly provided data simply overwrites the circles instead of creating new circles. To prevent this from happening, you need to specify a key function in data() function of the second half. Then, the first batch of circles do not get overwritten.
//add circles with price data
svgContainer.selectAll("circle")
.data(priceData)
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y1(d); })
//add circles with difficulty data
svgContainer.selectAll("circle")
.data(difficultyData, function(d) { return d; }) // SPECIFY KEY FUNCTION
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y2(d); })
you can append the circles in two different groups, something like:
//add circles with price data
svgContainer.append("g")
.attr("id", "pricecircles")
.selectAll("circle")
.data(priceData)
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y1(d); })
//add circles with difficulty data
svgContainer.append("g")
.attr("id", "datacircles")
.selectAll("circle")
.data(difficultyData)
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y2(d); })
if the circles are in different groups they won't be overwritten
I had the same question as OP. And, I figured out a solution similar to tomtomtom above. In short: Use SVG group element to do what you want to do with different data but the same type of element. More explanation about why SVG group element is so very useful in D3.js and a good example can be found here:
https://www.dashingd3js.com/svg-group-element-and-d3js
My reply here includes a jsfiddle of an example involving 2 different datasets both visualized simultaneously on the same SVG but with different attributes. As seen below, I created two different group elements (circleGroup1 & circleGroup2) that would each deal with different datasets:
var ratData1 = [200, 300, 400, 600];
var ratData2 = [32, 57, 112, 293];
var svg1 = d3.select('body')
.append('svg')
.attr('width', 500)
.attr('height', 400);
var circleGroup1 = svg1.append("g");
var circleGroup2 = svg1.append("g");
circleGroup1.selectAll("circle")
.data(ratData1)
.enter().append("circle")
.attr("cy", 60)
.attr("cx", function(d, i) { return i * 100 + 30; })
.attr("r", function(d) { return Math.sqrt(d); });
circleGroup2.selectAll("circle")
.data(ratData2)
.enter()
.append("circle")
.attr("r", function(d, i){
return i*20 + 5;
})
.attr("cy", 100)
.attr("cx", function(d,i){ return i*100 +30;})
.style('fill', 'red')
.style('fill-opacity', '0.3');
What is happening is that you are:
In the FIRST HALF:
getting all circle elements in the svg container. This returns nothing because it is the first time you're calling it so there are no circle elements yet.
then you're joining to data (by index, the default when you do not specify a key function). This puts everything in the priceData dataset in the "enter" section.
Then you draw your circles and everything is happy.
then, In the SECOND SECTION:
You are again selecting generically ALL circle elements, of which there are (priceData.length) elements already existing in the SVG.
You are joining a totally different data set to these elements, again by index because you did not specify a key function. This is putting (priceData.length) elements into the "update section" of the data join, and either:
if priceData.length > difficultyData.length, it is putting (priceData.length - difficulty.length) elements into the "exit section"
if priceData.length < difficultyData.length, it is putting (difficulty.length - priceData.length) elements into the "enter section"
Either way, all of the existing elements from the first "priceData" half are reused and have their __data__ overwritten with the new difficultyData using an index-to-index mapping.
Solution?
I don't think a key function is what you are looking for here. A key function is way to choose a unique field in the data on which to join data instead of index, because index does not care if the data or elements are reordered, etc. I would use this when i want to make sure a single data set is correctly mapped back to itself when i do a selectAll(..).data(..).
The solution I would use for your problem is to group the circles with a style class so that you are creating two totally separate sets of circles for your different data sets. See my change below.
another option is to nest the two groups of circles each in their own "svg:g" element, and set a class or id on that element. Then use that element in your selectAll.. but generally, you need to group them in some way so you can select them by those groupings.
//add circles with price data
svgContainer.selectAll("circle.price")
.data(priceData)
.enter()
.append("svg:circle")
.attr("class", "price")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y2(d); })
//add circles with difficulty data
svgContainer.selectAll("circle.difficulty")
.data(difficultyData)
.enter()
.append("svg:circle")
.attr("class", "difficulty")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y2(d); })
Using this method, you will always be working with the correct circle elements for the separate datasets. After that, if you have a better unique value in the data than simply using the index, you can also add a custom key function to the two .data(..) calls.
I have a simple line graph that checks every 5 seconds for updates & redraws the line/scale if needed. This all works well EXCEPT: the data-point dots.
What am I missing in the redraw to move the dots? The dots are there when the graph is first rendered. But on update, they don't move when the line gets redrawn. So I selected a new data source on update, and the old data-points remained fixed.
Redraw on update
var svgAnimate = d3.select("#animateLine").transition();
svgAnimate.select(".line") // change the line
.duration(750)
.attr("d", valueline(data));
svgAnimate.selectAll(".circle") // change the circle
.duration(750)
.attr("d", valueline(data));
svgAnimate.select(".x.axis") // change the x axis
.duration(750)
.call(xAxis);
svgAnimate.select(".y.axis") // change the y axis
.duration(750)
.call(yAxis);
Initial drawing:
svgAnimate.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("class", "circle")
.attr("r", 5)
.style("fill", function(d) {
if (d.close <= 400) {return "red"}
else { return "black" }
;})
.attr("cx", function(d) { return x(d.date); })
.attr("cy", function(d) { return y(d.close); })
This is what I don't want.
Your problem is that the function valueLine is the function you use to draw the line. Thus, when calling it again with a new data you redraw the line.
For the circles the attribute d has no sense. However, if we consider that the y axis does not change, then you can do something like:
svgAnimate.selectAll(".circle") // change the circle
.data(newData)
.duration(750)
.attr("cx", function(d){return x(d.date)};
If you need to change the y coordinates, then you have to modify the cy attribute of the circle.
My code might not be as rigorous as necessary, please post a jsFiddle if you still have problems.
I had some issues with updating circles in charts too.
Here is a working fiddle and might some people for future searches if they have the same problem:
http://jsfiddle.net/noo8k17n/
The problem is this line:
var svgAnimate = d3.select("#animateLine").transition();
It needs to be removed and then in the update method you can add and remove circles.