I thought I'd learn a little about ES6 classes while doing some d3 work, and so I made an ordinal bar chart class (fiddle here). It displays multiple series of data (eg:
[
[{"label":"apple", "value" :25},
{"label":"orange", "value": 16},
{"label":"pear", "value":19}],
[{"label":"banana", "value" :12},
{"label":"grape", "value": 6},
{"label":"peach", "value":5}]
];
I'm trying to get the update part working (where you provide new data and the bars/axis transition nicely). Unfortunately much of the example code is for v3, which doesn't seem to work with v4 like I'm using. The specific method is:
updateData(data){
//get an array of the ordinal labels out of the data
let labels = function(data){
let result = [];
for(let i=0; i<data.length; i++){
for (let j=0; j<data[i].length; j++){
result.push(data[i][j].label);
}
}
return result;
}(data);
//loop through the (potentially multiple) series in the data array
for(let i=0; i<data.length; i++){
let series = data[i],
bars = this.svg.selectAll(".series" + i)
bars
.data(series)
.enter().append("rect")
.attr("class", ("series" + i))
.classed("bar", true)
.merge(bars)
.attr("x", 0)
.attr("height", this.y.bandwidth())
.attr("y", (d) => { return this.y(d.label); })
.transition().duration(500) //grow bars horizontally on load
.attr("width", (d) => { return this.x(d.value); });
bars.exit().remove();
}
//update domain with new labels
this.y.domain(labels);
//change the y axis
this.svg.select(".yaxis")
.transition().duration(500)
.call(this.yAxis)
}
I'm trying to base the update pattern on Mike Bostock's code.
I'm getting an internal d3 error from the .call(this.yAxis), the transition doesn't animate, and the yaxis doesn't update. Additionally, the bars don't transition either. What's going wrong?
Several problems:
Update a scaleOrdinal/scaleBand axis BEFORE updating data join, otherwise the bars won't be able to find their y scale attribute (y(d.yourOrdinalLabel). The axis code hence needs to go before the bars code.
bars (or whatever element you join to the data) should be declared as the result of that join operation, so that it can be chained with .attr() for the visual attributes. let bars = svg.selectAll(".yourClass").data(yourData);
It's more sensible to have a simple update method in the class if you're only going to be toggling exclusion of existing data series, not adding new data. See updated fiddle.
Working jsfiddle.
Related
I have a stacked bar chart that can be updated and filtered using a dropdown menu. When the chart first renders I don't have issues but when I uncheck an option, the rect does not get removed.
I checked the array after the removal and it does get removed from the array. The issue here is that the rect gets shifted to the far left. The x-axis does get updated and the text associated with that objects gets removed.
My feeling is that I have a problem with the selection, but I am confused because I have rect and g that represents the series for each object. I'm not sure if I am selecting the right object when I am removing elements.
My code: Stacked bar chart with filter
Here is how the chart looks like after removing one item:
As you can see the rect is still there, but shifted to the left.
I have included the important part of my code. Any feedback even about how to make my code work on Plunker is appreciated
d3.selectAll("input").on("change", function() {
var paragraphID = d3.select(this).attr("id");
paragraphID = String(paragraphID);
if (this.checked) {
if (paragraphID === 'A') {
data.push({
production_company: "A",
Pass: 50,
Fail: 65,
total: 11
});
}
} else {
data = data.filter(d => d.production_company !== paragraphID);
}
xScale_production.domain(data.map(function(d) {
return d.production_company;
}));
yScale_production.domain([0, d3.max(series_production, d => d3.max(d, d => d[1]))]);
var group = svg_stack_production.selectAll("g").data(series_production);
var mbars = group.selectAll("rect").data(d => d);
mbars.enter()
.append("rect")
.attr("x", width)
.attr("y", d => yScale_production(d[1]))
.attr("width", xScale_production.bandwidth())
.merge(mbars)
.attr("x", (d, i) => xScale_production(d.data.production_company))
.attr("y", d => yScale_production(d[1]))
.attr("width", xScale_production.bandwidth())
.attr("height", d => yScale_production(d[0]) - yScale_production(d[1]));
group.selectAll("g")
.exit()
.remove();
mbars.selectAll("rect").exit().remove()
});
No need to select elements before calling exit, they are already in selection. Just call:
group.exit().remove();
mbars.exit().remove();
instead of:
group.selectAll("g").exit().remove();
mbars.selectAll("rect").exit().remove();
I solved the issue, there were two issues here. First, I needed to clone the original data and then apply the filtering on it as #Michael Rovinsky suggested.
Second, series_production needed to be updated after filtering the data.
here is the link for the working code.
Working code
I am using this kind of scatterplot matrix and a histogram as two views, in d3. Both of them get the data from the same csv file. This is how the histogram looks like (x axis):
To brush the histogram I use the code below, which is similar to this snippet:
svg.append("g")
.attr("class", "brush")
.call(d3.brushX()
.on("end", brushed));
function brushed() {
if (!d3.event.sourceEvent) return;
if (!d3.event.selection) return;
var d0 = d3.event.selection.map(x.invert),
d1 = [Math.floor(d0[0]*10)/10, Math.ceil(d0[1]*10)/10];
if (d1[0] >= d1[1]) {
d1[0] = Math.floor(d0[0]);
d1[1] = d1[0]+0.1;
}
d3.select(this).transition().call(d3.event.target.move, d1.map(x));
}
How can I link the two views, so that when I brush the histogram, the scatterplot matrix will show the brushed points as colored in red, and the other points as, lets say, grey?
This can get you started:
3 html files:
2 for the visuals (histogram.html and scatter.html)
1 to hold them in iframes (both.html):
Dependency:
jQuery (add to all 3 files)
Create table with 2 cells in both.html:
Add iframes to each cell:
<iframe id='histo_frame' width='100%' height='600px' src='histo.html'></iframe>
<iframe id='scatter_frame' width='100%' height='600px' src='scatter.html'></iframe>
I am using this histogram, and this scatterplot.
Add the linky_dink function to call the function inside your scatter.html (see below...):
function linky_dink(linked_data) {
document.getElementById('scatter_frame').contentWindow.color_by_value(linked_data);
}
In your scatter.html change your cell.selectAll function to this:
cell.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) { return x(d[p.x]); })
.attr("cy", function(d) { return y(d[p.y]); })
.attr("r", 4)
.attr('data-x', function(d) { return d.frequency }) // get x value being plotted
.attr('data-y', function(d) { return d.year }) // get y value being plotted
.attr("class", "all_circles") // add custom class
.style("fill", function(d) { return color(d.species); });
}
Note the added lines in bold:
Now our histogram circle elements retain the x and y values, along with a custom class we can use for targeting.
Create a color_by_value function:
function color_by_value(passed_value) {
$('.all_circles').each(function(d, val) {
if(Number($(this).attr('data-x')) == passed_value) {
$(this).css({ fill: "#ff0000" })
}
});
}
We know from above this function will be called from the linky_dink function of the parent html file. If the passed value matches that of the circle it will be recolored to #ff0000.
Finally, look for the brushend() function inside your histogram.html file. Find where it says: d3.selectAll("rect.bar").style("opacity", function(d, i) { .... and change to:
d3.selectAll("rect.bar").style("opacity", function(d, i) {
if(d.x >= localBrushYearStart && d.x <= localBrushYearEnd || brush.empty()) {
parent.linky_dink(d.y)
return(1)
} else {
return(.4)
}
});
Now, in addition to controlling the rect opacity on brushing, we are also calling our linky_dink function in our both.html file, thus passing any brushed histogram value onto the scatterplot matrix for recoloring.
Result:
Not the greatest solution for obvious reasons. It only recolors the scatterplot when the brushing ends. It targets circles by sweeping over all classes which is horribly inefficient. The colored circles are not uncolored when the brushing leaves those values since this overwhelms the linky_dink function. And I imagine you'd rather not use iframes, let alone 3 independent files. Finally, jQuery isn't really needed as D3 provides the needed functionality. But there was also no posted solution, so perhaps this will help you or someone else come up with a better answer.
I am using d3 to make a line chart that has to support up to 100 points on it, making it very crowded. The problem is that some of the labels overlap.
The method I was trying involved drawing all the points, then separately drawing all the labels and running a force collision on the labels to stop them overlapping, then after the force collision drawing a line between each of the labels and their associated point.
I can't make the forces work, let alone the drawing of lines after.
Any suggestions for a better way to do this are heartily welcomed also.
Here is my code:
$.each(data.responseJSON.responsedata, function(k, v) {
var thispoint = svg.append("g").attr("transform", "translate("+pointx+","+pointy+")");
thispoint.append("circle").attr("r", 10).style("fill","darkBlue").style("stroke","black");
var label = svg.append("text").text(v.conceptName).style("text-anchor", "end").attr("font-family", "Calibri");
label.attr("transform", "translate("+(pointx)+","+(pointy-12)+") rotate(90)")
});
nodes = d3.selectAll("text")
simulation = d3.forceSimulation(nodes)
.force("x", d3.forceX().strength(10))
.force("y", d3.forceY().strength(10))
.force("collide",d3.forceCollide(20).strength(5))
.velocityDecay(0.15);
ticks = 0;
simulation.nodes(data)
.on("tick", d => {
ticks = ticks + 1;
d3.select(this).attr("x", function(d) { return d.x }).attr("y", function(d) { return d.x });
console.log("updated" + this)
});
Force layout is a relatively expensive way of moving labels to avoid collision. It is iteratively and computationally intensive.
More efficient algorithms add the labels one at a time, determining the best position for each. For example a 'greedy' strategy adds each label in sequence, selecting the position where the label has the lowest overlap with already added labels.
I've created a D3 components, d3fc-label-layout, that implements a number of label layout strategies:
https://github.com/d3fc/d3fc-label-layout
Here's an example of how to use it:
// Use the text label component for each datapoint. This component renders both
// a text label and a circle at the data-point origin. For this reason, we don't
// need to use a scatter / point series.
const labelPadding = 2;
const textLabel = fc.layoutTextLabel()
.padding(2)
.value(d => d.language);
// a strategy that combines simulated annealing with removal
// of overlapping labels
const strategy = fc.layoutRemoveOverlaps(fc.layoutGreedy());
// create the layout that positions the labels
const labels = fc.layoutLabel(strategy)
.size((d, i, g) => {
// measure the label and add the required padding
const textSize = g[i].getElementsByTagName('text')[0].getBBox();
return [
textSize.width,
textSize.height
];
})
.position((d) => {
return [
d.users,
d.orgs
]
})
.component(textLabel);
https://bl.ocks.org/ColinEberhardt/27508a7c0832d6e8132a9d1d8aaf231c
I have summed up the dataset using nest,rollup and d3.sum functions and I'm able to display the pie chart properly.But I'm unable display percentage for each slice based on the summed total.Can anyone please give suggestions on this issue...
Mycode:
======
d3.csv("pi.csv", function(error, csv_data) {
var data = d3.nest()
.key(function(d) { return d.ip;})
.rollup(function(d) {
return d3.sum(d, function(g) {return g.value; });
}).entries(csv_data);
data.forEach(function(d) {
d.ip= d.key;
d.value = d.values;
});
My dataset:
=========
ip,timestamp,value
92.239.29.77,1412132430000,3190
92.239.29.77,1412142430000,319011
92.239.29.78,1412128830000,545568
92.239.29.78,1412130600000,616409
92.239.29.78,1412132430000,319087
92.239.29.76,1412130600000,616409
92.239.29.76,1412132430000,319087
Thanks in advance
So it seems that you want to display the percentage of each pie slice on the chart. All you really need to do is to calculate the the total of the values and iterate across the data variable adding in the percentage.
The data.forEach function doesn't add anything to the data variable, so I would drop that. I should also point out that the d3.nest function rolls up and sums each individual ip entry, so you're not getting sum of all values. However, this is pretty easy to do with d3, you can just use d3.sum:
var tots = d3.sum(data, function(d) {
return d.values;
});
Once you've done that you iterate across the data variable like:
data.forEach(function(d) {
d.percentage = d.values / tots;
});
You can then access the percentage on each pie slice using something like d.data.percentage
And putting it all together.
Note you could also compute the percentage from the start and end angles for each slice: (d.endAngle - d.startAngle)/(2*Math.PI)*100.
I'm trying to make custom labels for my x-axis on D3. My current solution throws in the array elements randomly, but I'd like it to be in a specific order. How can this be done properly in with tickformat? My current code is pasted below:
var formatAxis = function(d) {
return arr[d % arr.length];
}
xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.tickValues(createTickValuesArray(arr.length))
.tickFormat(formatAxis);
Help would be greatly appreciated.
If you want the tick labels to be returned in the same order as in the array, you can use the index explicitly:
var formatAxis = function(d, i) {
return arr[i];
}