The following toy problem illustrates my issue. I have an array of "locations", say a treasure map. Each item in the array for example monsters or treasure could exist at multiple locations on the map. e.g.
locations = [
{name:'treasure', color: 'blue', coords:[[100,100], [200,300]]},
{name:'monsters', color: 'red', coords:[[100,150], [220,420], [50,50]]}
]
Now I want to plot these using D3. The bad/naive approach (that works - see here for fiddle), would look like this:
for location in locations
for coords in location.coords
svg.append('circle')
.attr('cx', coords[0])
.attr('cy', coords[1])
.attr('r', 8)
.style('fill', location.color)
.datum(location)
However, when I modify the contents of the data, I don't want to have to run this naive code each time. It appears that using data() and enter() is the "correct" way to do it, but I can't figure out how it works with the sub-coordinates. e.g.
svg.selectAll('circle').data(locations).enter().append('circle')
.attr('cx', (d) -> d.coords[0][0])
.attr('cy', (d) -> d.coords[0][1])
.attr('r', 8)
.style('fill', (d) -> d.color)
This works great, but as you can see I am only printing the FIRST coordinate for each location, where I want to print them all. I suspect the only way to do this is to flatten my data array so there are 5 entries in total - 3 monsters and 2 treasure items.
Just wondering if there is a way to handle this better using D3.
For this, you need nested selections. The idea is that instead of appending a single element per data item, you append several. In code, it looks like this:
// append a `g` element for each data item to hold the circles
var groups = svg.selectAll("g.circle").data(locations)
.enter().append("g").attr("class", "circle");
// now select all the circles in each group and append the new ones
groups.selectAll("circle")
// the d in this function references a single data item in locations
.data(function(d) { return d.coords; })
.enter().append("circle")
.attr("cx", function(d) { return d[0]; })
.attr("cy", function(d) { return d[1]; });
It works the same for update and exit selections.
Related
I understand that merge can be used to combine enter and update selections in d3 v4, as in the simple example here: https://bl.ocks.org/mbostock/3808218.
I have a scatter plot in which multiple variables are displayed on a shared x-axis, for different groups selected by a dropdown box. When a new group is selected, the overall set of datapoints is updated, with points for each variable added like this:
.each(function(d, i) {
var min = d3.min(d.values, function(d) { return d.value; } );
var max = d3.max(d.values, function(d) { return d.value; } );
// Join new data with old elements
var points = d3.select(this).selectAll("circle")
.data(d.values, function(d) { return (d.Plot); } );
// Add new elements
points.enter().append("circle")
.attr("cy", y(d.key))
.attr("r", 10)
.style("opacity", 0.5)
.style("fill", function(d) { return elevColor(d.Elevation); })
.merge(points) //(?)
.transition()
.attr("cx", function(d) { return x((d.value-min)/(max-min)); });
// Remove old elements not present in new data
points.exit().remove();
This whole piece of code is largely duplicated for the overall enter selection and again in the overall update selection (as opposed to the individual variables), which seems less than ideal. How would merge be used to to remove this duplicated code?
The full example is here: http://plnkr.co/edit/VE0CtevC3XSCpeLtJmxq?p=preview
I'm the author of the solution for your past question, which you linked in this one. I provided that solution in a comment, not as a proper answer, because I was in a hurry and I wrote a lazy solution, full of duplication — as you say here. As I commented in the same question, the solution for reducing the duplication is using merge.
Right now, in your code, there is duplication regarding the setup of the "update" and "enter" selections:
var update = g.selectAll(".datapoints")
.data(filtered[0].values);
var enter = update.enter().append("g")
.attr("class", "datapoints");
update.each(function(d, i){
//code here
});
enter.each(function(d, i){
//same code here
});
To avoid the duplication, we merge the selections. This is how you can do it:
var enter = update.enter().append("g")
.attr("class", "datapoints")
.merge(update)
.each(function(d, i) {
//etc...
Here is the updated Plunker: http://plnkr.co/edit/MADPLmfiqpLSj9aGK8SC?p=preview
I'm working on a d3 chart (Multiple line chart).
I'm trying to represent a stock prediction, so basically the chart contains two lines: stock values line and an other one for my prediction.
The prediction is monthly, all days of month are represented in the chart.
In order to choose the month I have added a dropdown menu.
I appended a circle on each daily data, and works well for the first time. When user tries to change the month, the old circles are not updated, but the new ones are added.
Follow the code about circles:
topicEnter.append("g").selectAll("circle")
.data(function(d){return d.values})
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(dd){return x(dd.date)})
.attr("cy", function(dd){return y(dd.probability)})
.attr("fill", "none")
.attr("stroke", "black");
I have done a fiddle to understand better the situation and in order to show code.
What am I missing here? Why don't the circles update themself with the lines?
To solve the issue about circles not updating you can do the following:
function update(topics) {
// Calculate min and max values with arrow functions
const minValue = d3.min(topics, t => d3.min(t.values, v => v.probability));
const maxValue = d3.max(topics, t => d3.max(t.values, v => v.probability));
y.domain([minValue, maxValue]);
x2.domain(x.domain());
y2.domain(y.domain());
// update axes
d3.transition(svg).select('.y.axis').call(yAxis);
d3.transition(svg).select('.x.axis').call(xAxis);
// Update context
var contextUpdate = context.selectAll(".topic").data(topics);
contextUpdate.exit().remove();
contextUpdate.select('path')
.transition().duration(600)
.call(drawCtxPath);
contextUpdate.enter().append('g') // append new topics
.attr('class', 'topic')
.append('path').call(drawCtxPath);
// New data join
var focusUpdate = focus.selectAll('.topic').data(topics);
// Remove extra topics not found in data
focusUpdate.exit().remove(); //remove topics
// Update paths
focusUpdate.select('path')
.transition().duration(600)
.call(drawPath)
// Update circles
var circlesUpdate = focusUpdate
.selectAll('.topic-circle')
.data(d => d.values);
circlesUpdate.exit().remove();
circlesUpdate.transition().duration(600).call(drawCircle);
circlesUpdate.enter().append('circle').call(drawCircle);
// Add new topics
var newTopics = focusUpdate.enter().append('g') // append new topics
.attr('class', 'topic');
// Add new paths
newTopics.append('path').call(drawPath)
// Add new circles
newTopics.selectAll('.topic-circle')
.data(d => d.values)
.enter()
.append('circle')
.call(drawCircle);
}
With these helper functions to reduce code duplication:
function drawCtxPath(path) {
path.attr("d", d => line2(d.values))
.style("stroke", d => color(d.name));
}
function drawPath(path) {
path.attr("d", d => line(d.values))
.attr('clip-path', 'url(#clip)')
.style("stroke", d => color(d.name));
}
function drawCircle(circle) {
circle.attr('class', 'topic-circle')
.attr('clip-path', 'url(#clip)')
.attr("r", d => 5)
.attr("cx", d => x(d.date))
.attr("cy", d => y(d.probability))
.attr("fill", "none")
.attr("stroke", "black");
}
I think there are some additional issues in your code, when you select the same month twice you get an error, we can fix that by doing the following:
d3.select('#month_chart').on("change", function() {
// Get selected value of the select
var month = this.options[this.selectedIndex].value;
// Since you have hardcoded data we need to return a new array
// This is why if you select the same month twice your code breaks
// since parseDate will fail since the data will be already parsed
// the second time
var monthData = get_monthly_data(month).map(d => {
return {
date: parseDate(d.date),
predicted_bool: d.predicted_bool,
target: d.target
};
});
// Lets use arrow functions!
var keys = d3.keys(monthData[0]).filter(k => k !== 'date');
color.domain(keys);
// More arrow functions!
var topics = keys.map(key => {
return {
name: key,
values: monthData.map(d => {
return {
date: d.date,
probability: +d[key]
};
})
};
});
x.domain(d3.extent(monthData, d => d.date));
update(topics);
});
// A good ol' switch
function get_monthly_data(month) {
switch (month) {
case 'gennaio':
return data_1;
case 'febbraio':
return data_2;
case 'marzo':
return data_3;
default:
return data_1;
}
}
Working jsfiddle:
https://jsfiddle.net/g699scgt/37/
The problem is your update cycle, but there are a good number of examples of the enter, update, exit process in d3.
But essentially:
You append a new g element for each batch of circles, which means you have an empty selection (no circles are in that g yet) each time and each data point is appended (and none are removed). You don't need this extra append. Take a look at the DOM structure on each append in your existing code.
Your enter() selection returns new elements - not modified elements. So if your total number of elements remains the same you will have an empty enter() selection. You'll want to update existing elements separately (alternatively, remove them all and append them all every time).
You'll want something closer to this:
// set the data
circles = topic.selectAll("circle")
.data(function(d){return d.values});
// update existing circles
circles.attr("cx", function(dd){return x(dd.date)})
.attr("cy", function(dd){return y(dd.probability)});
// add new circles
circles.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(dd){return x(dd.date)})
.attr("cy", function(dd){return y(dd.probability)})
.attr("fill", "none")
.attr("stroke", "black");
// remove excess circles
circles.exit().remove();
You'll likely also want to revise the lines that append the lines to reflect the enter, update, exit cycle in d3.
Basiclly I have an Array of some Strings and I want to bind each of this strings to an circle. Then when I hover over the Circle I want to display these strings in a tooltip.
So this is my Array:
var node = [
"Hallo Hallo Hallo Hallo Hallo Hallo Hallo",
"foobarbaz",
"This is a short text",
"Changed some things on the folder structure and added some functions to the convert.c file",
];
Then there is my Tooltip which displays a html text...
var tooltip = svg.append('foreignObject')
.attr('x', 50)
.attr('y', 50)
.attr('width', 200)
.attr('height', 300)
.style("visibility", "hidden")
.append("xhtml:body")
.html('<div style="width: 150px;">Example</div>');
And now I want to create my circles over a for loop, append the data to them and let the tooltip display the right data on a mouseover:
for (var i = 0; i < 4; i++) {
svg.append("circle")
.data(node[i])
.attr("cx", 100*i+250)
.attr("cy", 100)
.attr("r", 10)
.attr("fill", "steelblue" )
.on("mouseover", function(d){
return tooltip.style("visibility","visible")
.html('<div style="width: 150px;">'+d+'</div>');
}).on("mouseout", function(){
return tooltip.style("visibility", "hidden");
});
}
But for some reason the result is not the whole string when I hover over the points it is just the fist character of the string. I am obviously missing something here...
D3 expects you to provide an array of values, such as .data([1,2,3,4]). You are passing in strings instead. So, D3 tries to match individual characters of a string (because a string, just like an array, can be iterated over) to the elements of the selection. In this case there's already one element, so it will assign to it the __data__ property equal to the first character of the string. The rest of the characters will go to the enter selection.
As Lars Kotthoff mentioned in his comment, you shouldn't be using for loops when working with D3. Instead, try something like this:
svg.selectAll('circle') // empty (yet) selection of circles
.data(node) // apply data to the empty selection, where node is your array of strings
.enter() // same as "for all entering (new) elements... "
.append('circle') // creates as many circles as there are elements in node array
.attr('cx', function (d, i) {
// d is "Hello...", "foobarbaz", "This is a short text", etc.
// i is 0, 1, 2, 3
return 100 * i + 250; // dynamic, index-dependent x
})
.attr('cy', 100) // constant y value across all elements
.on('mouseover', function (d) {
console.log(d);
});
Currently I am learning some "D3.js" and attempting to get my head around the way data is processed and selected.
I'm stuck on the following task I've created for myself.
Ideally, I want something that is functionally equivalent to:
<svg>
<circle r="20.5" cx="100" cy="200"></circle>
<circle r="20.5" cx="300" cy="10"></circle>
</svg>
What I have currently (with my logic) is:
var matrix = [ [{ "x": 100, "y": 200 }], [{ "x": 300, "y": 10 }]];
var result = d3.select("body").append("svg") // Append SVG to end of Body
.data(matrix) // select this data
.selectAll("g") //g is a svg grouping tag
.data(function (d) { return d; }) //Unwrap the first part of the array
.enter() // Grab all the data that is new to the selection in each array
.selectAll("g")
.data(function (d) { return d;}) // foreach each item inside the 1D array
.enter() // For all the data that doesn't exist already in the SVG
.append("circle") // Append Circle to the DOM with the following attributes
.attr("r", 20.5)
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; });
};
Weirdly enough the following :
var result = d3.select("body").append("svg")
.data(matrix)
.selectAll("g")
.enter()
.append("circle")
.attr("r", 20.5)
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; });
};
Seems somehow able to get the first item in the array but doesn't iterate correctly. I'm not quite sure how it's entering the array.
D3 seems to be quite a big step away from the programming paradigms I'm used to, and more difficult to debug so it would be awesome if someone could explain where I'm going wrong.
Oh, and while the example is quite useless and I could flatten it using the merge command - for the purposes of fully understanding D3 manipulation. I'd like to draw the couple of circles without the merge :)
Thanks!
Seeing you mention that you're new to d3 I'll make a few comments on the basics.
The first is that we're trying to place some svg elements on the DOM, so first we have to have a svg canvas to work on. Typically its set up early in the code and looks something like this:
var svg = d3.select("body")
.append("svg")
.attr("width", 350)
.attr("height", 250);
Note that it would be best to define variables for height and width (but I'm being lazy).
So now you have your canvas lets look at how d3 iterates. d3 iterates over an array, so you don't have to have an array within an array for your example as in:
var matrix = [ { "x": 100, "y": 200 }], [{ "x": 300, "y": 10 }];
Now you're second block of code is almost there, with just a bit of rearrangement. The first thing we need to do is t create placeholders for the circles in your svg canvas using svg.selectAll("circle"). Next we introduce the data to the empty placeholders using data(matrix) and this is bound using 'enter()`. Now all we have to do is append the circles and give them some attributes which is all the rest of the code does
svg.selectAll("circle")
.data(matrix)
.enter()
.append("circle")
.attr("r", 20.5)
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
You can see this all put together in this fiddle with some minor changes.
If you want to get to know d3 I'd really recommend get Scott Murray book on d3 it's an excellent introduction
I have two array objects that hold my d3.svg.symbol types which are circles, squares & triangles. Array #1 has multiple symbols which I plot across the canvas, whereas array #2 only holds three symbols aligned together.
My goal is to be able to click on array #2 to filter out all of the array #1 symbols that i dont want to see. e.g. Clicking a circle in array #2 would only mean circles are shown in array #1.
var array1 = svg.selectAll(a.array1)
.data(json).enter().append("a")
array1.transition().duration(1000)
.attr("transform", function(d,i) {return "translate("+d.x+","+d.y+")" ;})
array1.append('path')
.attr("d", d3.svg.symbol().type(function(d) {return shape [d.Country];}).size(120))
var array2 = svg.selectAll(g.array2)
.data(filt)
.enter().append("g")
.attr("transform", function(d,i) {return "translate("+d.x+","+d.y+")" ;})
array2.append("path")
.attr("d", d3.svg.symbol().type(function(d){return d.shape;}).size(200))
.attr("transform", "translate(-10, -5)")
So my query is how do I specify the click onto array#2 specific types as I have three. Therefore, I would like all to be clickable, but have a different outcome.
So far I have tried this just to try & select specific shapes in array#2
array2.on("click", function(){ alert('success') })
which just alerts when I click any of them, however when this is applied:
array2.on("click", function(){ if (d3.svg.symbol().type('circle') === true) { return alert('success') ;}; })
When I click the circle of array2 it doesnt alert at all.
It would be great if I could get some help - thanks. http://jsfiddle.net/Zc4z9/16/
The event listener gets the current datum and index as arguments, see the documentation. You can also access the DOM element through this. You could use this like follows.
.on("click", function(d) {
if(d.shape == "circle") { alert("success"); }
});