Fairly new to d3.js, so hoping there's something obvious that I'm missing. Have been looking at this code over and over again, not sure where it's going wrong.
I have a bargraph, which displays 28 bars. I'm trying to:
Replicate this tutorial, where new data is added to graph, and oldest data is removed.
Instead of using shift to remove data, I'd like to push to the graph/array, but only display the last 28 numbers. I'd like to use the whole array for another display. This said, I can't get the above to work.
This is a jsFiddle to the troublesome code.
I have a graph located within a group (with a unique ID, #kI1data, plan on having multiple graphs later). When that group is clicked, a value from the data array is shifted, and another pushed. The graph is then redrawn. I believe it's this redraw function that is causing an issue; it removes rectangles, but doesn't add any new ones after the first click.
graphGroup.append("g")
.attr("id", "kInv1")
.append("rect")
.attr("class", "invBack")
.attr("x", 0).attr("y", 0)
.attr("width", kI1width).attr("height", kI1height)
.attr("fill", "grey")
.on("click", dataChange); // Add/remove data, and update graph
function dataChange() {
kInvd1.shift();
kInvd1.push(30); // 30 a placeholder number
updateGraph();
};
function updateGraph() {
// Select the group containing the rectangles that need updating
// (to avoid selecting other rectangles within the svg)
var kI3dataSelect = d3.select("#kI1data").selectAll("rect").data(kInvd1, function(d) { return d; });
// Enter new data
kI3dataSelect.enter().append("rect")
.attr("x", function(d, i) { return 1 + ((i+1) * ((kI1width)/28)); })
.attr("y", function(d) { return ykI1(d); })
.attr("height", function(d) { return (kI1height) - ykI1(d); })
.attr("width", (kI1width)/28)
.attr("fill", "black");
// Update positions (shift one bar left)
kI3dataSelect
.transition().duration(100)
.attr("x", function(d, i) { return 1 + (i * ((kI1width)/28)); });
kI3dataSelect.exit()
.transition().duration(100)
.attr("x", function(d, i) { return 1 + ((i-1) * ((kI1width)/28)); })
.remove();
};
For now I'm just trying to get the newly added data to display, whilst the oldest data is removed. Once that's done, if there are any pointers on how to display just the last 28 numbers in the array, it'd be very much appreciated!
The jsFiddle shows how a new bar is added on the first click, but subsequent clicks only translate and remove data, whilst new bars are not added.
Changing your dataChange function to the following gets you new insertions and limits the over all array to 28 elements.
function dataChange() {
if (kInvd1.length >= 28) kInvd1.shift()
kInvd1.push(Math.random() * 60); // 30 a placeholder number
updateGraph();
};
Related
I'm hoping someone can spot where I'm going wrong here as I've looked at it for over 24 hours and can't see the issue.
I have a fairly complex dataviz working nicely in D3 but the final step is to 'adjust' any overlapping points - primarily so that their individual tooltips are accessible (on hover) (e.g. I don't want to consider alternatives like 'growing' the points to show their combined status).
So, imagine a pinboard where every pin is a point with hit_x and hit_y coordinates. Everything is working perfectly (filtering, updating, etc) in what I understand to be a fairly standard D3 'update' pattern. Sometimes two pins might have the same coordinates.
I thought I'd use D3 forces (for the first time) to recognise the 'colliding' pins and then adjust their positions accordingly. However, whilst I can get a simple version working on Blockbuilder. I can't get the same thing working when applied to my dataviz, even when I simplify it considerably.
I think perhaps I don't 100% understand the simulation process when using from an update pattern. My (simplified) code is pasted below, and here's in effect what I think it should do:
Appropriately loaded/formatted data is passed to update()
Prepare points ready to attach svg objects (same as 'standard' update pattern).
Prepare a simulation (and initially run it via ticked()).
Visualise the data.
Rerun the simulation so that the collisions are detected...
...during which, ticked() should notice the collisions and adjust the points by adjusting d.x and d.y accordingly until there are no overlaps.
I'm sure there's something obvious I'm missing - possibly related to whether I should pass the 'points' to the simulation or the original data. If anyone can spot it then I'd be very grateful. ¯\(ツ)/¯
function ticked() {
console.log("Ticking...");
points
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
// Update visualisation
function update(data) {
// Animation transitions
var t1 = d3.transition().duration(3000);
// Add svg group for handling/styling points later
graph.select("g.points").remove();
var pointsG = graph.append("g")
.attr('class', 'points');
// Data: Points
// Join new data with old elements
points = pointsG.selectAll("circle.point")
.data(data, function(d) {
return d;
});
// Forces
collisionSimulation = d3.forceSimulation(points)
.force('charge', d3.forceManyBody().strength(10))
.force('x', d3.forceX(function(d) {
return xScale(d.point.hit_x);
}).strength(0.5))
.force('y', d3.forceY(function(d) {
return yScale(d.point.hit_y);
}).strength(0.5))
.alphaTarget(1)
.on('tick', ticked);
console.log(collisionSimulation);
// Remove old elements not present in new data
points.exit()
.transition(t1)
.attr('class', 'exit')
.remove();
// Append new elements
points.enter()
.append('circle')
.attr('class', 'point')
.attr('cx', function(d) {
return xScale(d.point.hit_x);
})
.attr('cy', function(d) {
return yScale(d.point.hit_y);
})
.attr('r', 5)
.merge(points);
collisionSimulation.nodes(points)
.force("collide", d3.forceCollide().strength(0.5).radius(function() {
return 5;
}));
collisionSimulation.alpha(0.5).restart();
}
OK, now resolved - though I would appreciate further input on the ticked() implementation.
I imagine someone will go through the same process as me at some point, so following is what I did to get things working. The crux of it is that I had been getting confused joining the forces with the visual objects. By passing the same original data to both, the forces were then able to act on the visual objects, rather than both existing as one.
I re-read the core information about data joins. I've read this a number of times over the past few months working with D3 but for some reason, reading it again made my forces / update pattern finally make (un)sense in my head.
Similarly, despite having used a few excellent Blocks examples for reference, I came across this Medium article which made things click for me. It doesn't mean the Blocks examples aren't great - just that for some reason this article helped me.
Thereafter, I updated my code and was able to get things working. I feel like I'm still missing a little bit of 'data join' magic in my ticked() function though as it seems odd to need to 'search' for the relevant items to act on rather than use a previously built reference. I'm sure I can optimise that, but if anyone else can input then great.
Hope that helps someone else out.
function ticked() {
console.log("Ticking...");
d3.select('g.points').selectAll('circle.point')
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
// Update visualisation
function update(data) {
// Animation transitions
var t1 = d3.transition().duration(3000);
// Add svg groups for organising data elements
graph.select("g.points").remove();
var pointsG = graph.append("g")
.attr('class', 'points');
// Points
// Join new data with old elements
points = pointsG.selectAll("circle.point")
.data(data, function(d){ return d; });
// Remove old elements not present in new data
points.exit()
.transition(t1)
.attr('class', 'exit')
.remove();
// Append new elements
points.enter()
.append('circle')
.attr('class', 'point')
.attr('cx', function(d){ return xScale(d.point.hit_x); })
.attr('cy', function(d){ return yScale(d.point.hit_y); })
.attr('r', 5)
.merge(points);
// Forces
collisionSimulation = d3.forceSimulation()
.nodes(data)
.force('charge', d3.forceManyBody().strength(2))
.force('x', d3.forceX(function(d) { return xScale(d.point.hit_x); }).strength(0.5))
.force('y', d3.forceY(function(d) { return yScale(d.point.hit_y); }).strength(0.5))
.force("collide", d3.forceCollide().strength(0.5).radius(function() { return 5; }))
.on('tick', ticked);
collisionSimulation.alpha(0.5).restart();
}
I have a multi-line graph and I'm trying to create an interactive legend that when you click on each name, it will disable the corresponding line. It works only for the first one. Here is my code from this example:
svg.append("text")
.attr("x", (legendSpace / 2) + i * legendSpace) // space legend
.attr("y", height + (margin.bottom / 2) + 5)
.attr("class", "legend") // style the legend
.style("fill", function() { // Add the colours dynamically
return d.color = color(d.key);
})
.on("click", function() {
// Determine if current line is visible
var active = d.active ? false : true,
newOpacity = active ? 0 : 1;
// Hide or show the elements based on the ID
d3.select("#tag" + d.key.replace(/\s+/g, ''))
.transition().duration(100)
.style("opacity", newOpacity);
// Update whether or not the elements are active
d.active = active;
})
.text(d.key);
Also, I have this plunker with my visualisation (the data is just a small part of the dataset).
Thank you in advance.
Generally appending things with a foreach loop isn't the preferable option in d3. This is why you are having difficulty.
The first country in the legend is Albania, and only Albania works in the legend. Why? Because every path in the chart has the same id (the id for Albania):
So, when clicking on Albania in the legend, the on click function searches for the line with id tagAlbania (and looks only for the first entry as IDs should be unique) and toggles it.
The key issue you need to solve is why do all your paths share the same id. Let's look at your code:
dataNest.forEach(function(d, i) {
var country = plot.selectAll(".country")
.data(countries)
.enter().append("g")
.attr("class", "country");
country.append("path")
.attr("class", "line")
.attr("id", 'tag' + d.key.replace(/\s+/g, '')) // assign ID
.attr("d", function(d) {
return line(d.values);
})
.style("stroke", function(d) {
return color(d.name);
});
On the first pass through this for each loop we add a plot for each country using selectAll().data().enter().append(). As we start with an empty selection, .enter().append() will create an element for each item in the data array: all countries' paths are drawn on the first pass.
On the second pass of the loop, the enter selection is empty - there is an element in the initial selection, selectAll('.country'), for each item in the data array. Therefore no elements are entered and appended on each iteration of the loop beyond the first.
Looking back at the first loop, we can see that d is an item in the array dataNest, and on the first loop, d is the zeroth item - Albania. So, as all paths are appended on the first loop, all paths share the same id:
.attr("id", 'tag' + d.key.replace(/\s+/g, '')) // assign ID
To fix that, we could simply change that line to access the datum of the line being appended:
.attr("id", function(datum) { return 'tag' + datum.name.replace(/\s+/g, '') }) // assign ID
As the datum is the item in the array countries, not dataNest here, I've replaced the property key with the property name
Here's an updated plunkr.
Better yet, we can make this more idiomatic with d3 by dropping the loop altogether, see this plunkr.
I am using d3 js to display two bar graphs on the same svg element. Both the graphs have different json data sources. When tested seperately, both the graphs are displayed perfectly.When the entire source code is combined, it doesnt
To append rectangles to the page, I use the following code twice (with changed values in the 'x' and 'y' of .attr)
svgd.selectAll("rect")
.data(dataset_dept_errors)
.enter()
.append("rect")
.attr("x", function (d, i) {
return i * 100 + padding_dept; // x position of rect as per i->0,1,2,3,...
})
.attr("y", function (d) {
return (h_dept + yshift_for_dept - yScale(d.NumRuleFailed)); //y position of rect as per (h-value) to prevent inverted range
})
.attr("width", "50") //depending upon domain->no of inputs - with of band is decided acc. to fit in svg
.attr("height", function (d) {
return yScale(d.NumRuleFailed); //depending upon domain->value of inputs - with of band is decided acc. to fit in svg
})
.attr("fill", function (d) { //colour based on values -> more errors - dark coloured bars
return "rgb(" + 100 + "," + 0 + "," + 200 + ")";
})
.attr("stroke", "black");
I have read that selectAll('rect') will be ignored or it may not work as intended the second time it is encountered, so it should be used only once on one svg.
How then should I append the next set of rectangles to my page?
EDIT1 : I have the same problem for selectAll('text') for placing multiple text elements on the same page
You could first append two g elements to your svg element, position them appropriately, and then create the graphs inside the g elements.
The problem is in the root node - svgd. Just use two separate root SVG nodes (svgd, svgd2):
var svgd = d3.select("#svg1"),
svgd2 = d3.select("#svg2");
and then:
svgd.selectAll("rect")
.data(dataset_dept_errors)
.enter()
...
svgd2.selectAll("rect")
.data(dataset_dept_errors)
.enter()
...
and you'll be okay,
I am working on a widget that shows several D3 bar charts with different values, one after the other, in a sliding carousel.
When the page loads, the bar chart animate as it should, but when the page goes on to the next chart - whether it be on click or by itself - I would like it to restart the animation again each time.
I have tried calling animateChart() in the console but this doesn't work.
I am looking for a function that I can call from the console or from another function, like animateChart(), that will reload the D3 bar chart animation.
Here is a link to my widget:
http://jsfiddle.net/alocdk/oa5tg1qu/1/
I've found where you could enhance your animateChart function.
In fact you were modifying only data that were enterring your graph.
By calling :
d3.select(svg)
.selectAll("rect")
.data(data)
.enter().append("rect")
[...]
Everything following this, will only apply on the new data.
You may want to read these to understand the pattern to follow with data update in D3.
General Update Pattern, I
General Update Pattern, II
General Update Pattern, III
Here is my shot now http://jsfiddle.net/uknynmqa/1/
I've removed the loop you were doing on all your svg, because I assumed you wanted to only animate the current one.
And your function is updating all of the data, and not only those enterring thanks to :
// Update the data for all
var join = d3.select(svg)
.selectAll("rect")
.data(data);
// Append new data.
join.enter().append("rect")
.attr("class", function (d, i) {
var low = ""
i == minIndex ? low = " low" : "";
return "bar" + " " + "index_" + i + low;
})
// Update everyone.
join.attr("width", barWidth)
.attr("x", function (d, i) {
return barWidth * i + barSpace * i;
})
.attr("y", chartHeight)
.attr("height", 0)
.transition()
.delay(function (d, i) {
return i * 100;
})
.attr("y", function (d, i) {
return chartHeight - y(d);
})
.attr("height", function (d) {
return y(d);
});
D3 is following a really specific data update pattern.
Depending on what you want to do, you can follow this. It's up to you what you want to animate or not.
// Link data to your graph and get the join
var join = svg.selectAll('rect')
.data(data);
// Update data already there
join.attr('x', 0);
// Append the new data
join.enter().append('rect')
.attr('x', 0);
// Remove exiting elements not linked to any data anymore
join.exit().remove();
// Update all resulting elements
join.attr('x', 0);
Working with two charts in D3. I have a pie chat displaying parent data regarding a budget. When the user mouses over a pie slice, I am trying to push that slice's array data to a bar chart.
My data is setup like so:
{"Department":"Judiciary",
"Funds1415":317432,
"Fundsb":"317.4",
"annual": [ 282,288,307,276,276,298,309,317,317 ]
},
I'm trying to use this to pass the annual array to the barchart:
path.on('mouseover', function(d) {
...
bars.selectAll('rect').transition().attr("y", function(d) { return h - d.data.annual /125; });
bars.selectAll('rect').transition().attr("height", function(d) { return d.data.annual / 125; });
});
And here's the barchart I'm trying to send it to:
var bars = svg.selectAll("rect")
.data(budget)
.enter()
.append("rect")
.attr("class", "barchart")
.attr("transform", "translate(26,109)")
.attr("fill", function(d, i) {
return color2(i);
})
.attr('class', 'barchart')
.attr("x", function(d, i) {
return i * 14;
})
.attr("width", 12)
.attr("y", 100)
.attr("height", 100);
Link to full code here:
http://jsbin.com/zayopecuto/1/edit?html,js,output
Everything 'seems' to be working, except the data either isn't passing or it isn't updating the bar chart.
I've been banging my head up against this for a couple of days, to no avail. Originally I was thinking of placing the annual data in separate arrays and just transitioning from data source to data source on mouseover, but that seems backward and unnecessary.
First, your selector is wrong. bars is already a collection of rects, so you can't re-select the rects. Second, you haven't bound "updated" data to those rects. So, with this in mind, it becomes:
bars
.data(d.data.annual)
.transition()
.attr("height", function(d) {
return d / 125;
})
.attr("y", function(d) {
return h - d /125;
});
Here's an updated example.
What I understand from your code and comment is that, you have data points for your donut chart and each data object contains a property called 'annual' which you want to use as a input data for the bar chart.
You should be calling a separate function to plot your bar chart passing it the annual data array.
Clear the existing bar chart on 'mouseout' event, so that a new bar chart can be plotted on the next 'mouseover' event. You can use jQuery empty() function for clearing out the chart container.