D3 update data on the wrong bars - javascript

I want to update data on a click but the bars that are changing are not the right ones. There is something I cant quite fix with the select. On click the grey bars, which should be bar2 are updating. It should be bar.
Example: https://jsfiddle.net/Monduiz/kaqv37gu/
D3 chart:
var values = feature.properties;
var data = [
{name:"Employment rate",value:values["ERate15P"]},
{name:"Participation rate",value:values["PR15P"]},
{name:"Unemployment rate",value:values["URate15P"]}
];
var margin = {top: 70, right: 50, bottom: 20, left: 50},
width = 400 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom,
barHeight = height / data.length;
// Scale for X axis
var x = d3.scale.linear()
.domain([0, 100]) //set input to a scale of 0 - 1. The index has a score scale of 0 to 1. makes the bars more accurate for comparison.
.range([0, width]);
var y = d3.scale.ordinal()
.domain(["Employment rate", "Participation rate", "Unemployment rate"])
.rangeRoundBands([0, height], 0.2);
var svg = d3.select(div).select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.classed("chartInd", true);
var bar2 = svg.selectAll("g.bar")
.data(data)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
var bar = svg.selectAll("g.bar")
.data(data)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
bar2.append("rect")
.attr("height", y.rangeBand()-15)
.attr("fill", "#EDEDED")
.attr("width", 300);
bar.append("rect")
.attr("height", y.rangeBand()-15)
.attr("fill", "#B44978")
.attr("width", function(d){return x(d.value);});
bar.append("text")
.attr("class", "text")
.attr("x", 298)
.attr("y", y.rangeBand() - 50)
.text(function(d) { return d.value + " %"; })
.attr("fill", "black")
.attr("text-anchor", "end");
bar.append("text")
.attr("class", "text")
.attr("x", function(d) { return x(d.name) -5 ; })
.attr("y", y.rangeBand()-50)
//.attr("dy", ".35em")
.text(function(d) { return d.name; });
d3.select("p")
.on("click", function() {
//New values for dataset
var values = feature.properties;
var dataset = [
{name:"Employment rate",value:values["ERate15_24"]},
{name:"Participation rate",value:values["PR15_24"]},
{name:"Unemployment rate",value:values["URate15_24"]}
];
//Update all rects
var bar = svg.selectAll("rect")
.data(dataset)
.attr("x", function(d){return x(d.value);})
.attr("width", function(d){return x(d.value);})
});
}

var bar2 = svg.selectAll("g.bar")
.data(data)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
var bar = svg.selectAll("g.bar")
.data(data)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
'bar2' above generates 3 new g elements (one for each datum)
Since you don't set attr("class","bar") for these then 'bar' will also generate 3 new g elements - (if you had set the class attribute bar would return empty as no new elements would be generated and you'd see missing stuff)
Further on you add rects to all these g elements for six rectangles in total and in the click function you select all these rectangles and re-attach 3 fresh bits of data
Since bar2 was added first the rectangles in its g elements are hoovering up the new data
You need to select and set different classes on the g elements, .selectAll("g.bar") and .attr("class", "bar") for bar, and .selectAll("g.bar2") and .attr("class", "bar2") for bar2 (use the same name to keep it simple)
then in the new data you need select only the rects belonging to g elements of the bar class: svg.selectAll(".bar rect")
Another way would be to have only one set of g elements and add two types of rectangle (differentiated by class attribute)

Related

Update bar chart in d3 based on user input

I'm trying to update the bar chart in d3 based on the input selected by the user. The updated data is being displayed but it is being displayed on the old SVG elements. I tried using exit().remove() but it did not work.
Can anyone edit the code attached below so that the old SVG elements are removed.
<html>
<head>
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<style>
.rect {
fill: steelblue;
}
.text {
fill: white;
font: 10px sans-serif;
text-anchor: middle;
}
</style>
</head>
<body>
<select id = "variable">
<option >select</option>
<option value="AZ">Arizona</option>
<option value="IL">Illinois</option>
<option value="NV">NV</option>
</select>
<script>
var margin = {top: 20, right: 20, bottom: 70, left: 40},
width = 500 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var y = d3.scaleLinear()
.domain([0,5])
.range([height, 0]);
var yAxis = d3.axisLeft(y)
.ticks(10);
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var bar;
function update(state)
{
d3.csv("test3.csv", function(error, data)
{
data = data.filter(function(d, i)
{
if (d['b_state'] == state)
{
return d;
}
});
data = data.filter(function(d, i)
{
if (i<10)
{
return d;
}
});
var barWidth = width / data.length;
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Stars");
bar = svg.selectAll("bar")
.data(data)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(" + i * barWidth + ",0)"; });
bar.append("rect")
.attr("y", function(d) { return y(d.b_stars); })
.attr("height", function(d) { return height - y(d.b_stars); })
.attr("width", barWidth - 1)
.attr("fill", "steelblue");
bar.append("text")
.attr("x", function(d) { return height - y(d.b_stars); })
.attr("y", -40)
.attr("dy", ".75em")
.text(function(d) { return d.b_name; })
.attr("transform", "rotate(90)" );
});
svg.exit().remove();
bar.exit().remove();
}
d3.select("#variable")// selects the variable
.on("change", function() {// function that is called on changing
var variableName = document.getElementById("variable").value;// reads the variable value selected into another variable
update(variableName);});
</script>
</body>
Your problem here is about your bar selection. You can have a look to this part of the d3 documentation: Joining data.
By writing
bar = svg.selectAll("bar")
.data(data)
.enter()
You are selecting all bar elements, joining them data, and with this enter(), you are getting all the data items not linked to a bar element (enter() documentation).
But, your bar selector matches nothing. The parameter of the select()/selectAll() has to be a selector (element, class with ., id with #...). That is why the enter() your enter() selection is creating always new elements above the old ones instead of updating them.
So the first step is to rewrite this selection and creating the DOM elements that will match later this selection:
bar = svg.selectAll(".bar")
.data(data)
.enter()
.append("g")
.attr('class', 'bar')
.attr("transform", function(d, i) { return "translate(" + i * barWidth + ",0)"; });
Here, we are selecting all elements with the bar class. If there is no DOM element linked to an item from data, so we are creating it (in the enter() selection), as a new g with the bar class.
With the selection written like this, on the next call of your update, the selectAll('.bar') will match all the g previously created and not apply the enter() selection for existing elements.
To update or remove your existing bars, you can write your code like this:
var barData = svg.selectAll(".bar")
.data(data)
// Bars creation
var barEnter = barData.enter()
.append("g")
.attr('class', 'bar')
.attr("transform", function(d, i) { return "translate(" + i * barWidth + ",0)"; });
barEnter.append("rect")
.attr("y", function(d) { return y(d.b_stars); })
.attr("height", function(d) { return height - y(d.b_stars); })
.attr("width", barWidth - 1)
.attr("fill", "steelblue");
barEnter.append("text")
.attr("x", function(d) { return height - y(d.b_stars); })
.attr("y", -40)
.attr("dy", ".75em")
.text(function(d) { return d.b_name; })
.attr("transform", "rotate(90)" );
// Update the bar if the item in data is modified and already linked to a .bar element
barData.select('rect')
.attr("height", function(d) { return height - y(d.b_stars); })
// Remove the DOM elements linked to data items which are not anymore in the data array
barData.exit().remove()

d3 bar chart from multiple arrays

I'm making a barchart, but I cannot resolve its secondary function; updating with secondary data.
I make the bars with primary data from 2 separate arrays into an array like this;
labes = [label1, label2];
primarydata = [1,2]
data = [];
data = $.map(labels, function(v, i) {
return [[" " + v, " " + primaryData[i]]];
});
which gives output:
[["label1", "1"], ["label2", "2"]]
I then insert the data into an d3 bar chart.
var svg = d3.select($(svgobject).get(0)),
margin = {top: 20, right: 20, bottom: 30, left: 40},
width = $(svgobject).width() - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var x = d3.scaleBand().rangeRound([0, width]).padding(0.1),
y = d3.scaleLinear().rangeRound([height, 0]),
y1 = d3.scaleLinear().rangeRound([height, 0]);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain(data.map(function (d) {
return d[0];
}));
y.domain([0, d3.max(data, function (d) {
// parseInt needed here, or the scaling is wrong. Still scales though for some reason
return parseInt(d[1]);
})]);
// Inserts the x-axis line text
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + (height) + ")")
.call(d3.axisBottom(x))
.selectAll("text")
.attr("y", 0)
.attr("x", 8)
.attr("dy", "1.75em")
.attr("transform", "rotate(340)")
.style("text-anchor", "end");
// Inserts the y-axis line
// d3.format(".2s"), formats the line fx from 1300 to 1.3 thousand
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y).ticks(10).tickFormat(d3.format("2.2s")))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left)
.attr("x", 0 - (height / 2))
.attr("dy", "0.71em")
.attr("text-anchor", "end");
// Insert all the bars
g.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function (d) {
return x(d[0]);
})
.attr("y", function (d) {
return y(d[1]);
})
.attr("width", x.bandwidth())
.attr("height", function (d) {
return height - y(d[1]);
});
and this produces the barchart
However I want to with a push of a button put in comparable data like this;
secondaryData = [];
data = primaryData.concat(secondaryData).map(function(a, i) {
return [labels[i % 2], a.toString()];
});
And then I am at a loss as how to proceed. My bar does not even show the full data from the array with primary and secondary data. How do I insert the secondaryData but differentiate it? So I can style it differently.
I have also tried making a y1 taking data from d[2], but have failed in doing so.

Keep legend constant and only update chart in D3

my coding is to plot x axis with location, y axis with (value1,value2 or value3) and legend with types(high, medium,low). what I'm trying to do is to add menu with value1,2,3 and add legend with different types so if I change from either menu or click on legend, plot got updated with only selected data.
however, my code below is only able to create legend set as default type or clicked but not able to include all types. is there any way to include all types in legends constantly no matter what type is clicked and only update chart accordingly?
thank you,
<script>
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960- margin.left - margin.right,
height = 900 - margin.top - margin.bottom,
radius = 3.5,
padding = 1,
xVar = "location",
cVar= " type";
default = "high";
// add the tooltip area to the webpage
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// force data to update when menu is changed
var menu = d3.select("#menu select")
.on("change", change);
// load data
d3.csv("sample.csv", function(error, data) {
formatted = data;
draw();
});
// set terms of transition that will take place
// when new indicator from menu or legend is chosen
function change() {
//remove old plot and data
var svg = d3.select("svg");
svg.transition().duration(100).remove();
//redraw new plot with new data
d3.transition()
.duration(750)
.each(draw)
}
function draw() {
// add the graph canvas to the body of the webpage
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
// setup x
var xValue = function(d) { return d[xVar];}, // data -> value
xScale = d3.scale.ordinal()
.rangeRoundBands([0,width],1), //value -> display
xMap = function(d) { return (xScale(xValue(d)) + Math.random()*10);}, // data -> display
xAxis = d3.svg.axis().scale(xScale).orient("bottom");
// setup y
var yVar = menu.property("value"),
yValue = function(d) { return d[yVar];}, // data -> value
yScale = d3.scale.linear().range([height, 0]), // value -> display
yMap = function(d) { return yScale(yValue(d));}, // data -> display
yAxis = d3.svg.axis().scale(yScale).orient("left");
// setup fill color
var cValue = function(d) { return d[cVar];},
color = d3.scale.category10();
// filter the unwanted data and plot with only chosen dataset.
data = formatted.filter(function(d, i)
{
if (d[cVar] == default)
{
return d;
}
});
data = formatted;
// change string (from CSV) into number format
data.forEach(function(d) {
d[xVar] = d[xVar];
d[yVar] = +d[yVar];
});
xScale.domain(data.sort(function(a, b) { return d3.ascending(a[xVar], b[xVar])})
.map(xValue) );
// don't want dots overlapping axis, so add in buffer to data domain
yScale.domain([d3.min(data, yValue)-1, d3.max(data, yValue)+1]);
// x-axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text(xVar);
// y-axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text(yVar);
// draw dots
var dot = svg.selectAll(".dot")
.data(data)
.enter()
.append("circle")
.attr("class", "dot")
.attr("r", radius)
.attr("cx", xMap)
.attr("cy", yMap)
.style("fill", function(d) { return color(cValue(d));})
.on("mouseover", function(d) {
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d[SN] + "<br/> (" + xValue(d)
+ ", " + yValue(d) + ")")
.style("left", (d3.event.pageX + 5) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
// draw legend
var legend = svg.selectAll(".legend")
.data(color.domain().slice())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
// draw legend colored rectangles
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color)
.on("click", function (d){
default = d;
return change();
});
// draw legend text
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d;})
.on("click", function (d) {
default = d;
return change();
});
};
</script>
</body>
sample.csv
location type value1 value2 value3
A high 1 -2 -5
B medium 2 3 4
C low 4 1 2
C medium 6 3 4
A high 4 5 6
D low -1 3 2
I found a way to include all types in the legend.
first, extract unique types from column "type" and save them in the "legend_keys" as array. second, instead of pre-define "default", set the first type in the "legend_keys" as a default. but next default will be set by the event on click out of legend.
d3.csv("sample.csv", function(error, data) {
formatted = data;
var nest = d3.nest()
.key(function(d) { return d[cVar]; })
.entries(formatted);
console.log(nest);
legend_keys = nest.map(function(o){return o.key});
default = legend_keys[0];
//console.log(legend_keys[0]);
draw();
});
Finally, when define the legend, read "legend_keys" as data as below.
By doing this, I can always keep all types in the legend.
var legend = svg.selectAll(".legend")
.data(legend_keys)
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; })
.on("click", function (d){
default = d;
console.log(default);
return change();
});

How to apply the the enter() and data() methods for ds3 in-memory data

I have loaded data from a WebSocket connection into an array variable "data". I can see the 50 elements in the array and they do have the correct map elements.
The following snippet works properly: at the end the "data" elements have all rows transformed:
data.forEach(function (d) {
d[date] = parseDate(d[date]);
d[close] = +d[close];
});
Now, how to apply this data array to the internal d3 "values" so that the subsequent d3 dom manipulations use that data? In the next snippet I have made the attempt based on the examples / blogs I had seen:
var svg = d3.select("body").selectAll("svg")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
/* The following is NOT the correct place/way to do it.. need help here! */
.data(data)
.enter()
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
UPDATE Well I just tried moving those two data(data) and .enter() lines around - now placing them right after the selectAll().
var svg = d3.select("body").selectAll("svg")
.data(data)
.enter()
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
The results? Well we do have data now ! Maybe too much of a good thing?
EDIT Here is the entire function
function updateD3(data) {
var WIDTH = 1800, HEIGHT = 800;
var margin = {top: 120, right: 20, bottom: 120, left: 100},
width = WIDTH - margin.left - margin.right,
height = HEIGHT - margin.top - margin.bottom;
var parseDate = d3.time.format("%m-%d-%Y %H").parse;
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom").ticks(31);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var line = d3.svg.line()
.x(function (d) {
return x(d[date]);
})
.y(function (d) {
return y(d[close]);
});
var myNode = document.body;
while (myNode.firstChild) {
myNode.removeChild(myNode.firstChild);
}
var len = data.length;
console.log("data size=" + len + " date: " + data[0][date] + " close: " + data[0][close]
+ " last value: date: " + data[len-1][date] + " close: " + data[len-1][close]);
var date = "CALL_HOUR";
var close = "DROPPED_CALL";
data.forEach(function (d) {
d[date] = parseDate(d[date]);
d[close] = +d[close];
});
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain(d3.extent(data, function (d) {
return d[date];
}));
y.domain(d3.extent(data, function (d) {
return d[close];
}));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-1.1em")
.attr("dy", ".15em")
.attr("transform", function (d) {
return "rotate(-65)"
});
svg.append("text") // text label for the x axis
.attr("x", width / 2)
.attr("y", height + margin.bottom)
.style("text-anchor", "middle")
.style("font-size", "14px")
.text("Call Date");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Dropped Calls");
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
svg.append("text")
.attr("x", (width / 2))
.attr("y", 0 - (margin.top / 2))
.attr("text-anchor", "middle")
.style("font-size", "20px")
.style("text-decoration", "underline")
.text("No. of Dropped Calls vs Date Line Chart");
}
By biding your data to svg elements, you are creating as many svg elements as you have data items. I suspect that is not what you want. I created this FIDDLE exemplifying what you are doing but also showing how to bind the data to a single g.
Part of fiddle with data under a single g:
var data = [10,20,30];
var svg = d3.select("body")
.append("svg")
.attr("class","oneSvg")
.attr("width", 100)
.attr("height",100)
.append("g")
.attr("transform", "translate(0,0)");
circles = svg.selectAll("circle")
.data(data);
circles
.enter()
.append("circle")
.attr("cx",function (d) {return d;})
.attr("cy",20)
.attr("r",5)
.style("fill","blue");

d3 Reusable histogram

I've been trying to implement Reusability on a histogram plotted using d3.
I want that after plotting of the dataset, I want to plot statistical mean, variance etc. on the same plot.These would be user driven, basically I want to use the same plot.
Here's my attempt on coding the skeleton histogram code
function histogram(){
//Defaults
var margin = {top: 20, right: 20, bottom: 20, left: 20},
width = 760,
height = 200;
function chart(selection){
selection.each(function(d,i){
var x = d3.scale.linear()
.domain( d3.extent(d) )
.range( [0, width] );
var data = d3.layout.histogram()
//Currently generates 20 equally spaced bars
.bins(x.ticks(20))
(d);
var y = d3.scale.linear()
.domain([0, d3.max(d) ])
.range([ height - margin.top - margin.bottom, 0 ]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select(this).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var bar = svg.selectAll(".bar")
.data(data)
.enter().append("g")
.attr("class", "bar");
/*
Corrected bars
bar.append("text")
.attr("dy", ".75em")
.attr("y", 6)
.attr("x", x(data[0].dx) / 2)
.attr("text-anchor", "middle")
.text(function(d) { return formatCount(d.y); });
*/
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class","y axis")
.call(yAxis);
bar.append("rect")
.attr("x", function(d,i){ return x(d.x); })
.attr("width", x(data[0].dx) - 1)
.attr('y',height)
.transition()
.delay( function(d,i){ return i*50; } )
.attr('y',function(d){ return y(d.y) })
.attr("height", function(d) { return height - y(d.y); });
});
}
//Accessors//
chart.width = function(value) {
if (!arguments.length) return width;
width = value;
return chart;
};
chart.height = function(value) {
if (!arguments.length) return height;
height = value;
return chart;
};
return chart;
}
It's assigning a negative width for bars. My input dataset would simply be an array of numbers and I need to plot the frequency distribution
If you're asking how to implement the avg, standard deviation, once you have your histogram you can draw lines and text on it to represent the avg. I would calculate which bar the average is in, and the % of the way through the bar and then something like this:
var averageBar = vis.selectAll("g.bar:nth-child(" + (averageBarIndex + 1) + ")");
averageBar.append("svg:line")
.attr("x1", 0)
.attr("y1", y.rangeBand()*averageBarPercentage)
.attr("x2", w)
.attr("y2", y.rangeBand() * averageBarPercentage)
.style("stroke", "black");
averageBar.append("svg:text")
.attr("x", w-150)
.attr("y", y.rangeBand() * averageBarPercentage-15)
.attr("dx", -6)
.attr("dy", "10px")
.attr("text-anchor", "end")
.text("Average");
That will give you a line marking the average, you can do similar for the standard deviation.

Categories