i've stuck for this code. Im trying to add text behind the circle
and sample code like this
for the text:
g.selectAll(".my-text")
.data(marks)
.enter().append("text")
.attr("class", "text-desc")
.attr("x", function(d, i) {
return projection([d.long, d.lat])[0];
})
.attr("y", function(d, i) {
return projection([d.long, d.lat])[1];
})
.attr('dy', ".3em")
.text(function() { return location})
.attr("text-anchor", "middle")
.attr('color', 'white')
.attr('font-size', 15)
and for circle like this
g.selectAll(".circle")
.data(marktests)
.enter().append("circle")
.attr("class", "bubble")
.attr("cx", function(d, i) {
return projection([d.long, d.lat])[0];
})
.attr("cy", function(d, i) {
return projection([d.long, d.lat])[1];
})
.attr("r", function() {
return myRadius(locationGroup + 20);
})
.on('mouseover', tipBranch.show)
.on('mouseout', tipBranch.hide)
.on('click', function(d){
window.open('http://localhost:8000/detail/'+d.branch);
});
}
but i got result just like this
and the elements if using inspect element
Thank you if you can help to help me and explain how to solve the problem code
First of all I noticed the following issue:
g.selectAll(".my-text")
.data(marks)
.enter().append("text")
.attr("class", "text-desc")
Also the following line: .text(function() { return location}) is faulty because you are missing the data object that you iterate with. This might be changed to: .text(function(d) { return d.location})
you are selecting all elements with class .my-text but then you are attaching text-desc as class to the text elements. The correct change for this would be:
g.selectAll(".text-desc")
.data(marks)
.enter().append("text")
.attr("class", "text-desc")
considering that you want to use text-desc as a class. the same problem is with the circle as well: Either do: g.selectAll("circle") to select the circle tag elements or g.selectAll(".bubble") to select the bubbles.
You are also using different iterating objects for text and circles - usually you should iterate over a single collection.
Another issue with the sample is that location and locationGroup are not part of the collection items. I would expect that the values to be taken from the data object as such .text( d => d.location) and .attr("r", d => myRadius(d.locationGroup)). Before proceeding make sure that you populate iterating items with this properties.
Another approach would be to do the following:
const group =
g.selectAll('.mark')
.data(marks)
.enter()
.append('g')
.attr('class', 'mark')
.attr('transform', d => {
const proj = projection([d.long, d.lat])
return `translate(${proj[0]}, ${proj[1]})`;
})
group.append('text').text(d => return d.location) //apply other props to text
group.append('circle').text(d => return d.location) //apply other props to circle
Using this approach will allow you to iterate the collection with a group element and use translation property in order to move the group to the location (small improvement, projection will be executed once) and use the group to populate with other elements: text, circle.
Hope it helps.
Related
I have a horizontal bar chart that gets updated when one of the radio buttons clicked. The bars get updated fine, however, the old labels seem not to be removed every time the labels get updated. Am I missing something here? it seems that the exit function is not working. I couldn't find examples that deal with labels.
svg_bar.selectAll(".text-bar")
.data(dataSet)
.join(
enter => enter
.append("text")
.attr('text-anchor', 'middle')
.attr('font-size', '16px')
.attr('font-family', 'sans-serif')
.attr('fill', 'white')
.call(enter => enter.transition()
.duration(1000)
.attr('y', (d) => yScale_h(d.clean_test) + yScale_h.bandwidth() / 2)
.attr('x', (d) => xScale_h(d.Award) - 14)
.text(function (d) {
return `${d.Award} `;
})
),
update => update
.call(update => update.transition()
.duration(1000)
.text(function (d) {
return `${d.Award} `;
})
.attr('y', (d) => yScale_h(d.clean_test) + yScale_h.bandwidth() / 2)
.attr('x', (d) => xScale_h(d.Award) - 14)
),
exit => exit
.call(exit => exit.transition()
.duration(1000)
.remove()
)
);
If you use svg_bar.selectAll(".text-bar"), the selection is always empty and d3 will always add new elements.
If you use svg_bar.selectAll("text"), the selection will include all text elements of the svg, and it will change other text elements such as the y-axis and title.
One approach to select only the texts of the bars is to select with a class, such as svg_bar.selectAll("text.bar"). For this to work, the appended bars need to be assigned to the 'bar' class with .classed('bar', true), so they selected in the next render.
svg_bar.selectAll("text.bar") // Selects the texts of class "bar"
.data(dataSet)
.join(
enter => enter
.append("text")
.classed("bar", true) // Create texts with class "bar"
.attr('text-anchor', 'middle')
...
I have a map with a circle at the center of each country. I apply an animation that updates the circles every 3 seconds, following the general update pattern by Mike Bostock. However, my problem is that I want to apply a transition to old elements that stay in the chart (i.e. are not part of the exit()), but only for those that have a change in a particular attribute (because this attribute defines the color of the circle). I figured that I should store the old value as an attribute in the circle and then just compare this attribute value with the newly assigned value from the updated data.
However, when I use a usual js if clause it only provides me with the first element, which means when this circle's value changes, all other circles will get the transition as well.
How can I check between the old and the new value inside this pattern?
Here is a sample code:
//DATA JOIN
var countryCircles = circleCont.selectAll(".dataCircle")
.data(filData, function(d){ return d.id})
//EXIT OLD CIRCLES THAT ARE NOT INCLUDED IN NEW DATASET
countryCircles.exit()
.transition()
.ease(d3.easeLinear)
.duration(500)
.attr("r",0)
.remove()
//CHECK IF OLD CIRCLES THAT ARE INCLUDED CHANGED THE CRITICAL ATTRIBUTE VALUE (main)
if (countryCircles.attr('main') != countryCircles.data()[0].main) {
countryCircles
.attr("main", function(d) {return d.main})
.attr("id", function(d) {return "circle" + d.id})
.attr("class","dataCircle")
.transition()
.ease(d3.easeLinear)
.duration(500)
.ease(d3.easeLinear)
.attr("r",0)
.transition()
.duration(500)
.ease(d3.easeLinear)
.attr("r",10)
.style("fill", function(d) {
if (d.main === "nodata") {
return "grey"
} else {
return color_data.find(function (y) {
return y.main === d.main;
}).color;
}
})
} else {
countryCircles
.attr("main", function(d) {return d.main})
.attr("id", function(d) {return "circle" + d.id})
.attr("class","dataCircle")
}
//CREATE CIRCLES THAT ARRE NEW IN THE UPDATED DATASET
var newCircle = countryCircles.enter()
.append("circle")
.attr("class","dataCircle")
.attr("cx", getCX)
.attr("cy", getCY)
.attr("id", function(d) {return "circle" + d.id})
.attr("main", function(d) {return d.main})
.style("cursor","crosshair")
.attr("r", 0)
.transition()
.delay(500)
.duration(500)
.ease(d3.easeLinear)
.attr("r",10)
.style("fill", function(d) {
if (d.main === "nodata") {
return "grey"
} else {
return color_data.find(function (y) {
return y.main === d.main;
}).color;
}
})
OK, the solution was somewhat straight forward (and I was blind...).
Instead of having the if statement for a single element, I use the each() function and within there check for a change.
...
//CHECK IF OLD CIRCLES THAT ARE INCLUDED CHANGED THE CRITICAL ATTRIBUTE VALUE (main)
countryCircles.each(function(d) {
if (d3.select(this).attr('main') != d3.select(this).data()[0].main) {
...
I would like to reproduce the process from D3 Sankey chart using circle node instead of rectangle node, however, I would like to select only certain nodes to change from rectangles to circles.
For example, in this jsfiddle used in the example, how would you only select Node 4 and Node 7 to be converted to a circle?
I updated your fiddle.
Basically you just need some way to select the nodes that you want to make different. I used unique classname so that you can style them with CSS as well. I didn't feel like writing the code to select just 4 and 7 (I'm lazy) so I just selected all of the even nodes instead.
// add in the nodes
var node = svg.append("g").selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", function (d, i) { return i % 2 ? "node rect" : "node circle";
})
Then you can use that to select the nodes and add circles instead of rectangles.
svg.selectAll(".node.circle").append("circle")
.attr("r", sankey.nodeWidth() / 2)
.attr("cy", function(d) { return d.dy/2; })
.attr("cx", sankey.nodeWidth() / 2)
.style("fill", function (d) {
There is also another similar approach, illustrated in the following jsfiddle.
I started from this fiddle (from another SO question that you merntioned)), where all nodes had already been converted to circles:
Then I modified existing and added some new code that involves filtering during creation of circles:
// add the circles for "node4" and "node7"
node
.filter(function(d){ return (d.name == "node4") || (d.name == "node7"); })
.append("circle")
.attr("cx", sankey.nodeWidth()/2)
.attr("cy", function (d) {
return d.dy/2;
})
.attr("r", function (d) {
return Math.sqrt(d.dy);
})
.style("fill", function (d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("fill-opacity", ".9")
.style("shape-rendering", "crispEdges")
.style("stroke", function (d) {
return d3.rgb(d.color).darker(2);
})
.append("title")
.text(function (d) {
return d.name + "\n" + format(d.value);
});
// add the rectangles for the rest of the nodes
node
.filter(function(d){ return !((d.name == "node4") || (d.name == "node7")); })
.append("rect")
.attr("y", function (d) {
return d.dy/2 - Math.sqrt(d.dy)/2;
})
.attr("height", function (d) {
return Math.sqrt(d.dy);
})
.attr("width", sankey.nodeWidth())
.style("fill", function (d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("fill-opacity", ".9")
.style("shape-rendering", "crispEdges")
.style("stroke", function (d) {
return d3.rgb(d.color).darker(2);
})
.append("title")
.text(function (d) {
return d.name + "\n" + format(d.value);
});
Similar code had to be modified for accurate positioning text beside rectangles.
I believe the final result looks natural, even though it lost some of the qualities of the original Sankey (like wider connections).
//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 am new to using d3 and JavaScript and would appreciate some constructive feedback. I'm mocking up a practice example to learn d3 that involves plotting climate data (temperature anomaly) from 1880-2010 from two sources (GISS and HAD). So far I have generated a multiple line chart in d3 using this data. Code and data are here https://gist.github.com/natemiller/0c3659e0e6a0b77dabb0
In this example the data are originally plotted grey, but each line colored a different color on mouseover.
I would like to add two additional features.
I would like, on mouseover, to reorder the lines so that the moused-over line is on top, essentially reorder the lines. I've read that this requires essentially replotting the SVG and I have tried code along the lines of this
source.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", "lightgrey")
.on("mouseover", function() {
if (active) active.classed("highlight", false);
active = d3.select(this.parentNode.appendChild(this))
.classed("highlight", true);
})
.style("stroke",function(d) {return color(d.name);})
.on("mouseout", function(d) {
d3.select('#path-' + d.name)
.transition()
.duration(750)
.style("stroke", "lightgrey")
})
.attr("id", function(d, i) { return "path-" + d.name; });
where the .on("mouseover"... code is meant to highlight the current "moused-over" line. It doesn't seem to work in my example. All the lines are highlighted initially and then turn grey with the mouseover/mouseout. If someone could help me identify how to update my code so that I can reorder the lines on mouseover that would be great!
I have been playing around with labeling the lines such that when either the line or its label is moused-over the line and label colors update. I've played around a bit using id's but so far I can't get both the text and the line to change color. I've managed to 1. mouseover the line and change the color of the text, 2. mouseover the text and change the color of the line, 2. mouseover the line and change the line, but not have both the line and the text change color when either of them are moused-over. Here is a section of code that serves as a start (using ids), but doesn't quite work as it only specifies the path, but not the text and the path. I've tried adding them both to d3.select('#path-','#text-'..., and variations on this, but it doesn't seem to work.
source.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", "lightgrey")
.on("mouseover", function(d){
d3.select(this)
.style("stroke",function(d) {return color(d.name);});
})
.on("mouseout", function(d) {
d3.select('#path-' + d.name)
.transition()
.duration(750)
.style("stroke", "lightgrey")
})
.attr("id", function(d, i) { return "path-" + d.name; });
source.append("text")
.datum(function(d) { return {name: d.name, value: d.values[d.values.length - 15]}; })
.attr("transform", function(d) { return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")"; })
.attr("x", 5)
.attr("dy", ".35em")
.style("stroke", "lightgrey")
.on("mouseover", function(d){
d3.select('#path-' + d.name)
.style("stroke",function(d) {return color(d.name);});
})
.on("mouseout", function(d) {
d3.select('#path-' + d.name)
.transition()
.duration(750)
.style("stroke", "lightgrey")
})
.text(function(d) { return d.name; })
.attr("font-family","sans-serif")
.attr("font-size","11px")
.attr("id", function(d, i) { return "text-" + d.name; });
I greatly appreciate your help. I am new to d3 and this help-serve. Its a steep learning curve at the moment, but I hope this example and the code is reasonably clear. If its not let me know how I can make it better and I can repost the question.
Thanks so much,
Nate
Chris Viau provided a good answer to this question over on the d3 Google group.
https://groups.google.com/forum/?fromgroups=#!topic/d3-js/-Ra66rqHGk4
The trick is to select the path's g parent to reorder it with the others:
this.parentNode.parentNode.appendChild(this.parentNode);
This appends the current selection's container "g" on top of all the other "g".
I've found this useful in lots of other instances as well.
Thanks Chris!