Appending scaled circles to a multi-line graph - javascript

I have followed an example from here https://bl.ocks.org/d3noob/4db972df5d7efc7d611255d1cc6f3c4f to create a similar graph. However, I have one additional column of data that I need to use to create circles that match the color of the line where the radius will be some scaled value of that column entry. So, col 3 has values like 873, 15, 1000, 1563, etc. I have tried to do something like
svg.selectAll('circle').data(data)
.enter().append("circle")
.attr("cx", function(d) { return x(d.date) })
.attr("cy", function(d) { return y(d.close) })
.attr("r", '5')
.attr("fill", "red");
right below the point we add the path (right after adding the value line path in the link), however, obviously this only enters circles for one line. I have to add them for both.

You actually need two circles' selections, one for open and another for close:
svg.selectAll(null).data(data)
.enter().append("circle")
.attr("cx", function(d) { return x(d.date) })
.attr("cy", function(d) { return y(d.close) })
.attr("r", '5')
.attr("fill", "steelblue");
svg.selectAll(null).data(data)
.enter().append("circle")
.attr("cx", function(d) { return x(d.date) })
.attr("cy", function(d) { return y(d.open) })
.attr("r", '5')
.attr("fill", "red");
Here is the resulting code: https://bl.ocks.org/GerardoFurtado/4179c63daf38d85a266fb11f8e8e4c17/3786e4a0594e45e6e9a41df84bae4c6a43a86c6f

Related

How can I get one circle for every three months

I'm using this link to learn D3.Js
I want to draw circles, but I want a circle for every three months
I tried to create a new data sub of the original data, but this didn't work
https://d3-graph-gallery.com/graph/area_lineDot.html
temp =[]
for (i=0; i< data.length; i=i+3) {
temp.push(data[i]);
}
I need to modify this code
svg.selectAll("myCircles")
.data(data)
.enter()
.append("circle")
.attr("fill", "red")
.attr("stroke", "none")
.attr("cx", function(d) { return x(d.date) })
.attr("cy", function(d) { return y(d.value) })
.attr("r", 3)
Try this:
svg.selectAll("myCircles")
.data(temp) // <---------- Use 'temp' instead of 'data'
.enter()
.append("circle")
.attr("fill", "red")
.attr("stroke", "none")
.attr("cx", function(d) { return x(d.date) })
.attr("cy", function(d) { return y(d.value) })
.attr("r", 3)
I got this working now the issue was the way I was reading the d.date and d.value the data has string need to be change to int
thank you Simon

Binding data to new elements in d3

I am working with a d3 scatterplot. I connect to my database and initially I begin with lets say 3 dots on the graph. Each dot represents a paper and the x axis is the year and the y axis is how many citations it has. Now when I click on a dot, papers that that paper cites appear on the graph. I have managed all of the above so far but my issue now is that although when I click on dot the relevant papers appear on the graph, when I click on THOSE dots nothing happens. So I havent managed to bind my Json data to the new dots. Here is the relevant code:
// initial connection to display papers
d3.json("connection4.php", function(error,dataJson) {
dataJson.forEach(function(d) {
d.YEAR = +d.YEAR;
d.counter = +d.counter;
console.log(d);
})
//baseData is the original data that I dont want to be replaced
baseData = dataJson;
// draw dots
var g = svg.selectAll(".dot")
.data(baseData)
.enter()
.append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", function(d) {return x(YearFn(d))})
.attr("cy", function(d) {return y(Num_citationsFn(d))})
.style("fill","blue")
.on("click", function(d, i) {
d3.json("connection2.php?paperID="+d.ID, function(error, dataJson) {
console.log(dataJson);
dataJson.forEach(function(d) {
d.YEAR = +d.YEAR;
d.counter = +d.counter;
console.log(d);
baseData.push(d);
})
var g = svg.selectAll(".dot")
.data(baseData)
.enter()
.append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", function(d) {return x(YearFn(d))})
.attr("cy", function(d) {return y(Num_citationsFn(d))})
.style("fill", "red")
})
My queries in the php file are fine as I can see they are returning the correct data, so I think my main issue is binding my Json data from my second connection to the new dots. I wonder can anyone shed some light on how I need to go about this. I am new to d3 so any feedback is appreciated! thanks in advance
I think the problem is very simply that you do not bind the "click" event to your newly created nodes.
Replace the lines
// draw dots
var g = svg.selectAll(".dot")
.data(baseData)
.enter()
.append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", function(d) {return x(YearFn(d))})
.attr("cy", function(d) {return y(Num_citationsFn(d))})
.style("fill","blue")
.on("click", function(d, i) {
...
})
by
function clickHandler (d,i){
...
}
// draw dots
var g = svg.selectAll(".dot")
.data(baseData)
.enter()
.append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", function(d) {return x(YearFn(d))})
.attr("cy", function(d) {return y(Num_citationsFn(d))})
.style("fill","blue")
.on("click", clickHandler); //clickHandler is referenced here, instead of the original anonymous function
and add also a .on("click", clickHandler); call to your newly created node, i.e. within the clickHandler function itself:
///add linked dots
var g = svg.selectAll(".dot")
.data(baseData)
.enter()
.append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", function(d) {return x(YearFn(d))})
.attr("cy", function(d) {return y(Num_citationsFn(d))})
.style("fill", "red")
.on("click", clickHandler); //click handler is *also* called here

D3.js line chart - positioning the dots

I'm having problems positioning my tooltip dots. I'm using an ordinal X scale and i just can't get it to work...not sure if i have to change my data structure or...any insight would be appreciated. I have included a JS FIDDLE link below
tooltip_container.selectAll("dot")
.data(dataset)
.enter().append("circle")
.attr("r", 5)
.attr("cx", function(d,i) { /* return x(d) ?? */})
.attr("cy", function(d,i) { /* return y(d) ?? */})
JS Fiddle link
If you want to create the tooltip for all the lines you'll need this
tooltip_container.selectAll("dot")
.data(dataset)
.enter().append("circle")
.attr("r", 5)
.attr("cx", function(d,i) { return x(d.month) })
.attr("cy", function(d,i) { return y(d.undecided) })
tooltip_container.selectAll("dot")
.data(dataset)
.enter().append("circle")
.attr("r", 5)
.attr("cx", function(d,i) { return x(d.month) })
.attr("cy", function(d,i) { return y(d.yes) })
tooltip_container.selectAll("dot")
.data(dataset)
.enter().append("circle")
.attr("r", 5)
.attr("cx", function(d,i) { return x(d.month) })
.attr("cy", function(d,i) { return y(d.no) })

Converting only certain nodes in D3 Sankey chart from rectangle to circle

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).

d3js: adding same type of elements with different data

//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.

Categories