How do I create the update function in D3JS - javascript

I am trying to build a bar graph that I can switch between the amount of data displayed based on a particular length of time. So far the code that I have is this,
var margin = {
top : 20,
right : 20,
bottom : 30,
left : 50
}, width = 960 - margin.left - margin.right, height = 500
- margin.top - margin.bottom;
var barGraph = function(json_data, type) {
if (type === 'month')
var barData = monthToArray(json_data);
else
var barData = dateToArray(json_data);
var y = d3.scale.linear().domain([ 0, Math.round(Math.max.apply(null,
Object.keys(barData).map(function(e) {
return barData[e]['Count']}))/100)*100 + 100]).range(
[ height, 0 ]);
var x = d3.scale.ordinal().rangeRoundBands([ 0, width ], .1)
.domain(d3.entries(barData).map(function(d) {
return barData[d.key].Date;
}));
var xAxis = d3.svg.axis().scale(x).orient("bottom");
var yAxis = d3.svg.axis().scale(y).orient("left");
var svg = d3.select("#chart").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 + ")");
svg.append("g").attr("class", "x axis").attr("transform",
"translate(0," + height + ")").call(xAxis);
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(
"Total Hits");
svg.selectAll(".barComplete").data(d3.entries(barData)).enter()
.append("rect").attr("class", "barComplete").attr("x",
function(d) {
return x(barData[d.key].Date)
}).attr("width", x.rangeBand() / 2).attr("y",
function(d) {
return y(barData[d.key].Count);
}).attr("height", function(d) {
return height - y(barData[d.key].Count);
}).style("fill", "orange");
var bar = svg.selectAll(".barHits").data(d3.entries(barData))
.enter().append("rect").attr("class", "barHits").attr(
"x", function(d) {
return x(barData[d.key].Date) + x.rangeBand() / 2
}).attr("width", x.rangeBand() / 2).attr("y",
function(d) {
return y(barData[d.key].Count);
}).attr("height", function(d) {
return height - y(barData[d.key].Count);
}).style("fill", "red");
};
This does a great job displaying my original data set, I have a button set up to switch between the data sets which are all drawn at the beginning of the page. all of the arrays exist but no matter how hard I try I keep appending a new graph to the div so after the button click I have two graphs, I have tried replacing all of the code in the div, using the enter() exit() remove() functions but when using these I get an error stating that there is no exit() function I got this by following a post that someone else had posted here and another one here but to no avail. Any ideas guys?

What you are most likely doing is drawing a new graph probably with a new div each time the button is clicked, one thing you can try doing is to hold on to the charts container element somewhere, and when the button is clicked, you simply clear it's children and re-draw the graphs.

In practice, I almost never chain .data() and .enter().
// This is the syntax I prefer:
var join = svg.selectAll('rect').data(data)
var enter = join.enter()
/* For reference: */
// Selects all rects currently on the page
// #returns: selection
svg.selectAll('rect')
// Same as above but overwrites data of existing elements
// #returns: update selection
svg.selectAll('rect').data(data)
// Same as above, but now you can add rectangles not on the page
// #returns: enter selection
svg.selectAll('rect').data(data).enter()
Also important:
# selection.enter()
The enter selection merges into the update selection when you append or insert.

Okay, I figured it out So I will first direct all of your attention to here Where I found my answer. The key was in the way I was drawing the svg element, instead of replacing it with the new graph I was just continually drawing a new one and appending it to the #chart div. I found the answer that I directed you guys to and added this line in the beginning of my existing barGraph function.
d3.select("#barChart").select("svg").remove();
and it works like a charm, switches the graphs back and forth just as I imagined it would, now I have a few more tasks for the bargraph itself and I can move on to another project.
Thank you all for all of your help and maybe I can return the favor one day!

Related

How to update bar graph data whenever array is updated in d3 v6.3.1?

I'm trying to update a bargraph created using d3.js to display values from a regularly updated array. Currently, I have a function d3Data that is called upon page load(using jQuery) and as a function invoked whenever buttons are clicked on the page. This d3 data updates the array and then calls another function d3New that is supposed to rerender the bar graph.
The bar graph is able to render along with the bar rectangles if hard coded data in the array is used. However, since I initialize the starting array as empty I am unable to see the rectangles as it seems my bar graph doesn't display rectangles based on updated values in this array.
Here is my logic for displaying the rectangles within the bar graph:
var rects = svg.selectAll("rect")
.data(data)
rects.enter().append("rect")
rects.exit().remove()
rects.attr("x", function(d, i) { return (i * 2.0 + 1.3) * barWidth; })
.attr("y", function(d,i) {
return Math.min(yScale(0), yScale(d))
})
.attr("height", function(d) {
// the height of the rectangle is the difference between the scale value and yScale(0);
return Math.abs(yScale(0) - yScale(d));
})
.attr("width", barWidth)
.style("fill", "grey")
.style("fill", function(d,i) { return color[i];})
I understand the enter() function intially joins the data to the rectangle elements and the exit function is used in order to remove any previous rectangle element values upon rectangle rerender. But, no rectangles are rendered to the screen and not sure why? Here is what it looks like:
Any help would be great
edit:
Here is some more of the two functions:
function d3Data() {
var dataArray = [];
for (var key in gradeFrequency) {
dataArray.push(gradeFrequency[key]);
}
d3New(dataArray);
}
function d3New(data) {
var height = 500;
var width = 500;
var margin = {left: 100, right: 10, top: 100, bottom: 20};
var color = ["#C6C7FF", "#8E8EFC", "#5455FF", "#8E8EFC", "#C6C7FF"];
var svg = d3.select("body").append("svg")
.attr('height', height)
.attr('width', width)
.append("g")
.attr("transform", "translate("+ [margin.left + "," + margin.top] + ")");
var barWidth = 30;
var chartHeight = height-margin.top-margin.left;
var xScale= d3.scaleBand()
.domain(["A", "B", "C", "D", "F"])
.range([100, 450])
.padding([0.8])
// Draw the axis
svg.append("g")
.attr("transform", "translate(-100,300)")
.call(d3.axisBottom(xScale));
var yScale = d3.scaleLinear()
.domain([0, 1.0])
.range([chartHeight, 0]);
var rects = svg.selectAll("rect")
.data(data)
rects.enter().append("rect").merge(rects)
rects.exit().remove()
I figured out how to fix my problem. Had to add:
d3.selectAll("svg").remove();
to the start of the function in order to remove previous outdated graphs and also add the attributes for "rect" before the .exit().remove(). So instead of:
var rects = svg.selectAll("rect")
.data(data)
rects.enter().append("rect").merge(rects)
rects.exit().remove()
rects.attr(..).attr(..).attr(..)
I did:
rects.enter().append("rect").merge("rect").attr(..).attr(..).attr(..) and so on.
rects.exit().remove()
Since the attributes for the rectangles need to be updated as well they had to go before the .exit() and .remove() calls

D3 js set data from data-attribute

During creating graphs I need to set data. Those data(Array of Objects) I have already in HTML like this:
<svg class="graph-n" data-stuff="{simplified data}"></svg>
Then with Javascript and D3 JS I initialize and setup graphs with the following code:
<script>
var margin = { top: 20, right: 20, bottom: 30, left: 50},
width = 1500 - margin.left - margin.right,
height = 350 - margin.top - margin.bottom;
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
var valueline = d3.line()
.x(function(d) { return x(new Date(d.t)); })
.y(function(d) { return y(d.y); });
var svg = d3.selectAll(".graph-n")
.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 new Date(d.t); }));
y.domain([0, d3.max(data, function(d) { return d.y; })]);
svg.append("path")
.attr("class", "line")
.attr("d", valueline);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
svg.append("g")
.call(d3.axisLeft(y));
</script>
The question is how shall I say, that data are inside each element during Selection in data attribute 'data-stuff' ?
Each SVG has data to plot in his own data attribute.
Or is my approach wrong and I shall use different approach?
Thank you for your responses.
There is no way to just tell d3 explicitly "take data from this attribute". You can however set the data programatically, loading it from the attribute of your choosing. There are several ways on how to achieve it, as demonstrated on these selection examples (they use <ul> and <li> for simplicity, <svg> is usage is analogous):
// the pure D3 way
d3.selectAll("ul.d3-pure") // select the element
.datum(function() { return this.getAttribute("data-list").split(",")}) // set selection's data based on its data attribute
.selectAll("li") // create new selection
.data((d) => d) // set the data from the parent element
.enter().append("li") // create missing elements
.text((content) => content); // set elements' contents
// the DOM way
var domUls = document.querySelectorAll("ul.dom"); // select elements
for(var i = 0; i < domUls.length; i++) { // iterate over those elements
const ul = domUls[i];
const d3_ul = d3.select(ul); // create D3 object from the node
const data = ul.getAttribute("data-list").split(",");
d3_ul.selectAll("li").data(data) // create new selection and assign its data
.enter().append("li") // create missing elements
.text((content) => content) // set elements' content
}
// the hybrid D3-DOM way
d3.selectAll("ul.d3-hybrid") // select elements
.each(function() { // iterate over each node of the selection
const ul = d3.select(this); // "this" is the "ul" HTML node
const data = ul.attr("data-list").split(",");
ul.selectAll("li").data(data) // create new selection, assign its data
.enter().append("li") // create missing elements
.text((content) => content) // set elements' content
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<ul class="d3-pure" data-list="1,2,3">
</ul>
<ul class="dom" data-list="a,b,c">
</ul>
<ul class="d3-hybrid" data-list="I,II,III">
</ul>
Modern browsers accepts node().dataset
Using D3_selection.node() and pure Javascript's DOM-node dataset property, as commented by #altocumulus before.
It is an old Javascript standard for HTML elements (since Chorme 8 and Firefox 6) but new for SVG (since Chorme 55 and Firefox 51).
The values of dataset's key-values are pure strings, but a good practice is to adopt JSON string format for non-string datatypes, to parse it by JSON.parse().
Using it
Code snippet to get and set key-value datasets at HTML and SVG.
console.log("-- GET values --")
var x = d3.select("#html_example").node().dataset;
console.log("s:", x.s );
for (var i of JSON.parse(x.list)) console.log("list_i:",i)
var y = d3.select("#svg_example g").node().dataset;
console.log("s:", y.s );
for (var i of JSON.parse(y.list)) console.log("list_i:",i)
console.log("-- SET values --");
y.s="BYE!"; y.list="null";
console.log( d3.select("#svg_example").node().innerHTML )
<script src="https://d3js.org/d3.v5.min.js"></script>
<p id="html_example" data-list="[1,2,3]" data-s="Hello123">Hello dataset!</p>
<svg id="svg_example">
<g data-list="[4,5,6]" data-s="Hello456 SVG"></g>
</svg>

How to use d3fc-label-label.js on a map?

I'm trying to position labels on map overlapping-free by using using d3fc-label-label.js in combination with d3.js. While labeling the map by basic d3 functions works well, the approach with the help of d3fc-label-label.js (heavily inspired by this example) produces a map with all the labels placed in top left corner.
Here's the javascript part that does the job
var width = 1300,
height = 960;
var projection = d3.geoMercator()
.scale(500)
// Center the Map to middle of shown area
.center([10.0, 50.5])
.translate([width / 2, height / 2]);
// ??
var path = d3.geoPath()
.projection(projection)
.pointRadius(2);
// Set svg width & height
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// var g = svg.append("g");
d3.json("europe_wgs84.geojson", function(error, map_data) {
if (error) return console.error(error);
// var places = topojson.feature(map_data, map_data.objects.places);
// "path" instead of ".subunit"
svg.selectAll("path")
.data(map_data.features)
.enter().append("path")
.attr("d", path)
.attr("class", function(d) { return "label " + d.id})
var labelPadding = 2;
// the component used to render each label
var textLabel = fc.layoutTextLabel()
.padding(labelPadding)
//.value(function(d) { return map_data.properties.iso; });
.value(function(d) { return d.properties.iso; });
// use simulate annealing to find minimum overlapping text label positions
var strategy = fc.layoutGreedy();
// create the layout that positions the labels
var labels = fc.layoutLabel(strategy)
.size(function(_, i, g) {
// measure the label and add the required padding
var textSize = d3.select(g[i])
.select('text')
.node()
.getBBox();
return [textSize.width + labelPadding * 2, textSize.height + labelPadding * 2];
})
.position(function(d) { return projection(d.geometry.coordinates); })
.component(textLabel);
// render!
svg.datum(map_data.features)
.call(labels);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.0/d3.min.js"></script>
See the gist that includes the data and a HTML file.
I would guess the issue is related to append the labels correctly to path of the map. Sadly, I haven't figured it out and would greatly appreciate any help!
I believe the problem lies in the fact that you are not passing single coordinates as the label's position.
layoutLabel.position(accessor)
Specifies the position for each item in the associated array. The
accessor function is invoked exactly once per datum, and should return
the position as an array of two values, [x, y].
In the example you show, that you are basing the design on, the variable places contains point geometries, it is to these points that labels are appended. Looking in the topojson we find places looking like:
"places":{"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[5868,5064],"properties":{"name":"Ayr"}},{"type":"Point","coordinates":[7508,6637],"properties":{"name":"Aberdeen"}},{"type":"Point","coordinates":[6609,5933],"properties":{"name":"Perth"}},...
Note that geometries.coordinates of each point contains one coordinate. However, in your code, d.geometry.coordinates contains an array of coordinates as it contains the boundary points of the entire path of each feature. This will cause errors in label placement. Instead, you might want to use path.centroid(d), this will return a single coordinate that is at the center of each country/region/path. Placement might not be perfect, as an extreme example, a series of countries arranged as concentric rings will have the same centroid. Here is a basic block showing placement using path.centroid (this shows only the placement - not the formatting of the labels as I'm not familiar with this library extension).
If you were wondering why the linked example's regional labels appear nicely, in the example each region has a label appended at its centroid, bypassing d3fc-label-layout altogether:
svg.selectAll(".subunit-label")
.data(subunits.features)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.id; })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.properties.name; });

Multiple pie charts update with only one input d3.js

I modified Mike Bostock's Pie Chart Update III in order to get two different pie charts in one using two elements with one csv file for each chart.
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var leftG = svg.append("g")
.attr("transform", "translate(" + width / 4 + "," + height / 2 + ")");
var rightG = svg.append("g")
.attr("transform", "translate(" + 3 * (width / 4) + "," + height / 2 + ")");
I want to have each radio button updating both pie charts. Now my problem is that the update inputs only alter the second pie chart.
I can't figure out how to do this since the change() function is inside the d3.csv() function and one cannot access the other.
d3.selectAll("input")
.on("change", changeLeft);
function changeLeft() {
var value = this.value;
pieLeft.value(function(d) { return d[value]; }); // change the value function
pathLeft = pathLeft.data(pieLeft); // compute the new angles
pathLeft.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
}
Thanks in advance.
EDIT : Here is my plnkr
declare a global variable
var changeLeft;
Replace your function with the below one and it will work:
changeLeft = function() {
var value = this.value;
pieLeft.value(function(d) { return d[value]; }); // change the value function
pathLeft = pathLeft.data(pieLeft); // compute the new angles
pathLeft.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
}

function that repeats indefinitely

The code below is from the last/bottom example of Mike Bostock's D3.js path tutorial http://bost.ocks.org/mike/path/. It creates a live graph of a user's page scrolling activity. If you watch the code run, you'll notice that the graph is running continuously, with the line graph sliding from right to left whether or not there's been any scrolling activity. Question: What is it about the tick function below that makes it run continuously, and how could it be altered to stop and start upon a click event?
(function() {
var n = 243,
duration = 750,
now = new Date(Date.now() - duration),
count = 0,
data = d3.range(n).map(function() { return 0; });
var margin = {top: 6, right: 0, bottom: 20, left: 40},
width = 960 - margin.right,
height = 120 - margin.top - margin.bottom;
var x = d3.time.scale()
.domain([now - (n - 2) * duration, now - duration])
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var line = d3.svg.line()
.interpolate("basis")
.x(function(d, i) { return x(now - (n - 1 - i) * duration); })
.y(function(d, i) { return y(d); });
var svg = d3.select("body").append("p").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.style("margin-left", -margin.left + "px")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
var axis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(x.axis = d3.svg.axis().scale(x).orient("bottom"));
var path = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.datum(data)
.attr("class", "line");
var transition = d3.select({}).transition()
.duration(750)
.ease("linear");
d3.select(window)
.on("scroll", function() { ++count; });
(function tick() {
transition = transition.each(function() {
// update the domains
now = new Date();
x.domain([now - (n - 2) * duration, now - duration]);
y.domain([0, d3.max(data)]);
// push the accumulated count onto the back, and reset the count
data.push(Math.min(30, count));
count = 0;
// redraw the line
svg.select(".line")
.attr("d", line)
.attr("transform", null);
// slide the x-axis left
axis.call(x.axis);
// slide the line left
path.transition()
.attr("transform", "translate(" + x(now - (n - 1) * duration) + ")");
// pop the old data point off the front
data.shift();
}).transition().each("start", tick);
})();
})()
first off, you are missing the part where transition is defined.
Its a var of some sort but is not defined in your snippet.
That is kinda important but its not needed to know why the function continues to run.
You need to first understand both forms of the jquery each function.
http://api.jquery.com/each/
http://api.jquery.com/jquery.each/
transition = transition.each(function() { ... }).transition().each("start", tick);
::note that ... is hiding a lot of code::
this is essentially one line of code.
it is saying for each child of the transition var, if any, run the "..." code for it. That is the first jquery each statement. Inside this function $(this) would equal the child obj of transition. The child object is never used however.
After all iterations are complete, then the transition() function is ran. (again not sure what that is)
Lastly, each is called again but with a different form. This time it is saying run the tick function with the string "start" as the obj. It means that inside that function $(this) = "start". Again the obj is never used.
Because the invoking object is never used, essentially .each is just calling the tick function.
Really, this is a very odd way of doing this. not sure why there is such heavy use of .each. My understanding is that .each is actually kind of slow compared to other means of iterating and invoking callbacks.
UPDATE--
to start/stop with a click I would introduce a var at the top of the script.
var running = true;
(set to false to start as not running)
then surround contents of the tick function with an if statement.
if(running)
{
transition = transition.each(function() { ... }).transition().each("start", tick);
}
then create one or two click handlers. (for one toggle or start and stop buttons)
There are many ways to accomplish this.
$(#[button ID]).click( function(){
if(running)
{
running = false;
}
else
{
running = true;
tick();
}
});
This is a basic plan of attack. There maybe an issue when clicking the button in rapid succession. That is for you to fix as needed.
I've had this same problem before...the transition is defined as:
var transition = d3.select({}).transition()
.duration(1000)
.ease("linear");
You need to run a function that changed the transition to zero:
transition = transition.transition(0).duration(0);
This essentially stops the transition from running completely.
Then to restart the transition:
transition = d3.select({}).transition()
.duration(shiftDuration)
.ease("linear");
tick();

Categories