I'm clueless on Ajax calls, have been reading on them for a few hours, but all of the tutorials mention load method and a listener like click.
I have a function (drawThreat) that adds some icons (fontawesome) on my svg, there is a json file with a bunch of x and ys, all I need to do is to have an ajax call that runs this function with all of the x and ys in the json file every 5 seconds and updates the svg element on the page. Here is the function:
function drawThreat (x, y){
var canvas = d3.select("#map")
.append("svg")
.attr("width", 500)
.attr("height", 500)
.attr("id", "Threat");
var Threat = canvas.append('svg')
.append('text')
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.attr("x", x)
.attr("y", y)
.attr("class", "threat")
.style('font-family','FontAwesome')
.style('font-size','20px')
.style('fill', 'red')
.text(function (d) {
return '\uf2be'
});
return Threat
};
Any help would be appreciated:) even if it is a link to a tutorial you find related to the question. I looked at 6 or 7 tutorials so far with no luck.
You basically can have a code similar to the below one if I understood your need correctly :
var dataFileUrl = "url-to-your.json"; // assign the url to a variable
var canvas = d3.select("#map") // get a ref from the SVG element
.append("svg") // you might want to define it only one time
.attr("width", 500)
.attr("height", 500)
.attr("id", "Threat");
var updateInterval = setInterval(function(){
d3.json(dataFileUrl , function (jsonData) { // AJAX call
drawThreat(jsonData); // calls main function
});
},5000); // every 5 * 1000ms
function drawThreat (jsonData){
canvas.selectAll('text.threat').remove(); // makes sure we don't have old icons
canvas.selectAll('text')
.data(jsonData) // for each set in json
.enter()
.append('text') // create a text and append it to svg canvas
.attr("class", "threat") // with class of threat
.attr("x", function (d) { return d[0]; }) // first element of each set
.attr("y", function (d) { return d[1]; }) // second element of each set
.text('\uf2be');
};
to reduce js codes I suggest adding styles and attributes using CSS:
.threat{
text-anchor:middle;
dominant-baseline:central;
font-family:FontAwesome;
font-size:20px;
fill:red;
}
You can also see it in action here : https://codepen.io/FaridNaderi/pen/awPBwm
Hope it helps you to get the point.
Related
I am trying to write a transitioning bar graph that uses two CVS files. I know that both of the files are loading properly because it shows in the console that the first one loads with the page and the second one loads when you click the update button.
The only thing that I have really thought of trying was changing the svg select to group instead of selecting all rectangles incase there was something screwed up there.
This block is creating the svg element, bringing in the first CSV file, and appending the rectangles onto the chart. My only thought for what the problem could be is that it is inside a function, but if I take it out of the function how do I bind the data to them?
//Creating SVG Element
var chart_w = 1000,
chart_h = 500,
chart_pad_x = 40,
chart_pad_y = 20;
var svg = d3.select('#chart')
.append('svg')
.attr('width', chart_w)
.attr('height', chart_h);
//Defining Scales
var x_scale = d3.scaleBand().range([chart_pad_x, chart_w -
chart_pad_x]).padding(0.2);
var y_scale = d3.scaleLinear().range([chart_pad_y, chart_h -
chart_pad_y]);
//Data-------------------------------------------------------------------
d3.csv('data.csv').then(function(data){
console.log(data);
generate(data); });
function generate(data){
//Scale domains
x_scale.domain(d3.extent(data, function(d){ return d }));
y_scale.domain([0, d3.max(data, function(d){ return d })]);
//Create Bars
svg.select('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i){
return x_scale(i);
})
.attr('y', function(d){
return y_scale(d);
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d){
return y_scale(d);
})
.attr('transform',
"translate(0,0)")
.attr('fill', '#03658C')
'''
The results I have experienced is a blank window with just the update button. As previously stated I know that the data is being generated because I can see it in the console.
Try using the following:
svg.selectAll('rect')
.data(data)
If you use svg.select this will only make the data binding with the first element found.
d3.select(selector): Selects the first element that matches the specified selector string. If no elements match the selector, returns an empty selection. If multiple elements match the selector, only the first matching element (in document order) will be selected. For example, to select the first anchor element:
This should be clear if you inspect the DOM nodes.
To fix the issue lets change some things in your code:
Lets create a dummy fetch function:
(function simulateCSVFetch() {
const data = [1,2,3,4,5];
generate(data);
})();
You are also using a scaleBand with an incomplete domain by using the extent function:
d3.extent(): Returns the minimum and maximum value in the given iterable using natural order. If the iterable contains no comparable values, returns [undefined, undefined]. An optional accessor function may be specified, which is equivalent to calling Array.from before computing the extent.
x_scale.domain(d3.extent(data, function(d) { // cant use extent since we are using a scaleBand, we need to pass the whole domain
return d;
}));
console.log(x_scale.domain()) // [min, max]
The scaleBand needs the whole domain to be mapped
Band scales are typically used for bar charts with an ordinal or categorical dimension. The unknown value of a band scale is effectively undefined: they do not allow implicit domain construction.
If we continue using that scale we will be only to get two values for our x scale. Lets fix that with the correct domain:
x_scale.domain(data);
Lastly use the selectAll to create the data bind:
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i) {
return x_scale(d);
})
.attr('y', function(d) {
return chart_h - y_scale(d); // fix y positioning
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d) {
return y_scale(d);
})
.attr('fill', '#03658C');
This should do the trick.
Complete code:
var chart_w = 1000,
chart_h = 500,
chart_pad_x = 40,
chart_pad_y = 20;
var svg = d3
.select('#chart')
.append('svg')
.style('background', '#f9f9f9')
.style('border', '1px solid #cacaca')
.attr('width', chart_w)
.attr('height', chart_h);
//Defining Scales
var x_scale = d3.scaleBand()
.range([chart_pad_x, chart_w - chart_pad_x])
.padding(0.2);
var y_scale = d3.scaleLinear()
.range([chart_pad_y, chart_h - chart_pad_y]);
//Data-------------------------------------------------------------------
(function simulateCSVFetch() {
const data = [1,2,3,4,5];
generate(data);
})();
function generate(data) {
console.log(d3.extent(data, function(d) { return d }));
//Scale domains
x_scale.domain(d3.extent(data, function(d) { // cant use extent since we are using a scaleBand, we need to pass the whole domain
return d;
}));
// Band scales are typically used for bar charts with an ordinal or categorical dimension. The unknown value of a band scale is effectively undefined: they do not allow implicit domain construction.
x_scale.domain(data);
y_scale.domain([0, d3.max(data, function(d) {
return d
})]);
//Create Bars
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i) {
return x_scale(d);
})
.attr('y', function(d) {
return chart_h - y_scale(d); // fix y positioning
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d) {
return y_scale(d);
})
.attr('fill', '#03658C');
}
Working jsfiddle here
I'm working on a bar chart that updates its data based on the mouseover of another element. When the chart updates, if there are less bars in the new chart, the chart permanently has fewer bars and changing the data back does not add them back in. I've added a gif to show this - when it gets down to 3 bars, they never come back.
Here's my code:
var scatter_versus_dataset; // the main set
var scatter_versus_dataset_filtered;
// set versus y scale
scatter_versus_y = d3.scaleBand().range([0, SCATTER_VERSUS_HEIGHT])
// set versus x scale
scatter_versus_x_fatal = d3.scaleLinear().range([0, SCATTER_VERSUS_WIDTH / 3]);
scatter_versus_x_nonfatal = d3.scaleLinear().range([-1 * SCATTER_VERSUS_WIDTH / 3, 0 ])
// set the versus colors
scatter_versus_z = d3.scaleOrdinal().range(STACK_COLOURS);
...
function updateScatterVersus(code){
// filter the set
scatter_versus_dataset_filtered = scatter_versus_dataset.filter(function (d) { return (d.majorOccCodeGroup == code) })
scatter_versus_y.domain(scatter_versus_dataset_filtered.map(function (d) { return d.occupation; })).padding(BAR_PADDING);
scatter_versus_x_fatal.domain([0, d3.max(scatter_versus_dataset_filtered, function (d) { return d.f_total_rate; })]).nice();
scatter_versus_x_nonfatal.domain([d3.min(scatter_versus_dataset_filtered, function (d) { return +-1 * d.nf_total_rate; }), 0]).nice();
var bars = d3.selectAll("#scatter_versus_fatal_rect")
.data(scatter_versus_dataset_filtered)
bars.exit()
.remove()
bars.transition()
.duration(600)
.attr("y", function (d) {
return scatter_versus_y(d.occupation);
})
.attr("x", function (d) {
return scatter_versus_x_fatal(0) + SCATTER_VERSUS_GAP_HALF;
})
.attr("width", function (d) {
return scatter_versus_x_fatal(d.f_total_rate);
})
.attr("height", scatter_versus_y.bandwidth())
bars.enter()
.append("rect")
.attr('id', 'scatter_versus_fatal_rect')
.classed("bar", true)
.attr("y", function (d) {
return scatter_versus_y(d.occupation);
})
.attr("x", function (d) {
return scatter_versus_x_fatal(0) + SCATTER_VERSUS_GAP_HALF;
})
.attr("width", function (d) {
return scatter_versus_x_fatal(d.f_total_rate);
})
.attr("height", scatter_versus_y.bandwidth())
}
The process for redrawing the other side of the chart is exactly the same. The problem is still there if i only draw one of the sides.
The data is just from a csv, and I don't think it's the problem - the filtered set has the right number of entries and it's fine in other charts. It's probably something to do with the removal and redrawing but I can't find many examples of this. Or perhaps a key? I can upload some data if needed but it's a pretty big CSV.
id in HTML is unique, only 1 tag should have it.
Select the div for the bars, then selectAll tags with class is bar and bind data.
Remove the id you add to the rects.
var bars = d3.select("#scatter_versus_fatal_rect")
.selectAll(".bar")
.data(scatter_versus_dataset_filtered);
bars.enter()
.append("rect")
// .attr('id', 'scatter_versus_fatal_rect')
.classed("bar", true)
......
I trying to understand how the D3 chord diagram works. My first step is to display the arcs for the diagram with following script. But for some reason, the arcs are not showing up.
See web page HERE
Can some one tell me what I am missing?
<body>
<script>
// Chart dimensions.
var width = 960,
height = 750,
innerRadius = Math.min(width, height) * .41,
outerRadius = innerRadius * 1.1;
//Create SVG element with chart dementions
var svg = d3. select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append ("g")
.attr("transform", "translate (" + width / 2 + "," + height / 2 + ")");
//------------Reformat Data ------------------------------------------
var matrix = []; // <- here is the data
d3.tsv('picData.tsv', function(err, data)
{
//console.log(data);
pictures = d3.keys(data[0]).slice(1);
//console.log(pictures);
data.forEach(function(row)
{
var mrow = [];
pictures.forEach(function(c)
{
mrow.push(Number(row[c]));
});
matrix.push(mrow);
//console.log(mrow);
});
//console.log('1st row: ' + matrix[0]);
//console.log(matrix);
});
//---------------- Define diagram layout ----------------------------
var chord = d3.layout.chord() //<-- produce a chord diagram from a matrix of input data
.matrix(matrix) //<-- data in matrix form
.padding(0.05)
.sortSubgroups(d3.descending);
var fill = d3.scale.category20(); //<-- https://github.com/mbostock/d3/wiki/API-Reference#d3scale-scales
//console.log(fill);
var g = svg.selectAll("g.group")
.data(chord.groups)
.enter().append("svg:g")
.attr("class", "group");
//console.log(g);
// create arcs
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
//console.log(arc);
g.append("path")
.attr("d", arc)
.style("fill", function(d) { console.log(d.index); return fill(d.index);})
.style("stroke", function(d) { return fill(d.index); })
.attr("id", function(d, i) { return"group-" + d.index });;
g.append("svg:text")
.attr("x", 6)
.attr("class", "picture")
.attr("dy", 15)
.append("svg:textPath")
.attr("xlink:href", function(d) { return "#group-" + d.index; })
.text(function(d) { return pictures[d.index]; });
//console.log(g);
</script>
</body>
Your problem stems from the fact that d3.tsv is asynchronous:
Issues an HTTP GET request for the comma-separated values (CSV) file at the specified url... The request is processed asynchronously.
As a result, all of your code under "Define diagram layout" is being executed before any data is loaded. Otherwise, your code works fine (See image below). So just move all your code into your d3.tsv(...) call and you'll be all set.
Your script is running without errors, but no elements are being created from your data join. That's usually a sign that you are passing in a zero-length data array.
In fact, you're not passing in an array at all; you're passing a function object. When d3 looks up the array length of that object, it returns undefined, which gets coerced to the number zero, and so no groups and no chords are created.
Relevant part of your code:
var g = svg.selectAll("g.group")
.data(chord.groups)
.enter().append("svg:g")
.attr("class", "group");
To actually get the array of chord group data objects, you need to call chord.groups(). Without the () at the end, chord.groups is just the name of the function as an object.
Edited to add:
Ooops, I hadn't even noticed that your drawing code wasn't included inside your d3.tsv callback function. Fix that, as described in mdml's answer, then fix the above.
I have some code on this jsFiddle here that generates a histogram for a data array called "values". That's all well and good.
When I want to update this histogram with a new data array, called "newData", things go wrong. I am trying to adhere to the enter(), update(), exit() D3 strategy (which I am obviously extremely new with). An animation does indeed occur, but as you can see by the fiddle, it just squishes everything into the upper right hand corner. Can someone point out what I am doing wrong in this segment of the code (the update)?
//Animations
d3.select('#new')
.on('click', function(d,i) {
var newHist = d3.layout.histogram().bins(x.ticks(bins))(newData);
var rect = svg.selectAll(".bar")
.data(values, function(d) { return d; });
// enter
rect.enter().insert("g", "g")
.attr("class", "bar")
.attr("transform", function(d) { return "translate(" + x(d) + "," + y(d) + ")"; });
rect.enter().append("rect")
.attr("x", 1)
.attr("width", w)
.attr("height", function(d) { return y(d); });
rect.enter().append("text")
.attr("dy", ".75em")
.attr("y", 6)
.attr("x", x(histogram[0].dx) / 2)
.attr("text-anchor", "middle")
.text(function(d) { return formatCount(d); });
// update
svg.selectAll('.bar')
.data(newHist)
.transition()
.duration(3000)
.attr("transform", function(d) { return "translate(" + x(d.x) + "," + y(d.y) + ")"; });
svg.selectAll("rect")
.data(newHist)
.transition()
.duration(3000)
.attr("height", function(d) { return height - y(d.y); });
svg.selectAll("text")
.data(newHist)
.transition()
.duration(3000)
.text(function(d) { return formatCount(d.y); });
// exit
rect.exit()
.remove();
});
The entirety of the code is on the JSFiddle linked above. Thanks!
Looking at the code above and the fiddle, a few things jump out at me:
(Line 85) You are still binding the original data
(Lines 105, 115) You are binding the data multiple times
(Line 99) You are still referencing the original histogram variable without updating it with the new data
You are declaring multiple bind/add/update/remove patterns for a single set of (changing) data
You're on the right track, but you need to differentiate between things that need to be updated when the data changes, and things that should not be updated/declared when the data changes. You should only have to declare the d3 pattern (bind, add, update, remove) once... it will work for updated datasets.
So, declare as much as you can outside the makeHist(values) function, and only have code that needs the changed data inside the function (this includes modifying a previously declared scale's domain and range). Then, the on click function can simply call the makeHist function again with the new data.
Here's a rough outline:
// generate data
// declare everything that can be static
// (add svg to dom, declare axes, etc)
// function that handles everything that new data should modify
function makeHist(values) {
// modify domains of axes, create histogram
// bind data
var rect = svg.selectAll('rect')
.data(histogram);
// add new elements
rect.enter().append('rect');
// update existing elements
rect.transition()
.duration(3000)
.attr('transform', '...');
// remove old elements
rect.exit().remove();
}
// generate initial histogram
makeHist(initialValues);
// handle on click event
d3.select('#new')
.on('click', function() {
makeHist(newData);
});
Here's a mostly working updated fiddle, it needs a little bit of cleanup, though:
http://jsfiddle.net/spanndemic/rf4cw/
Spoiler Alert: the two datasets aren't all that different
I've been trying to make a specific type of force direct graph, similar to this (http://bl.ocks.org/mbostock/950642):
However, instead of having all the same images I wish to have different images representing different information on the graph.
My first step to this is being able to change all the circle images to random linked shapes. Whatever I try to implement in my code, the circles I have just disappear, instead of being replaced by different shapes. Any help on this problem would be great. Here is the code. Sorry, I'm also new to this site.
// nodes
var nodeSelecton = svg.selectAll(".node").data(nodes).enter().append("g").attr({
class : "node"
}).call(force.drag);
nodeSelecton.append("circle").attr({
r : nodeRadius
}).style("fill", function(d) {
return color(d.group);
});
nodeSelecton.append("svg:text").attr("text-anchor", "middle").attr('dy', ".35em").text(function(d) {
return d.name;
});
// Add a new random shape.
// nodes.push({
// type: d3.svg.symbolTypes[~~(Math.random() * d3.svg.symbolTypes.length)],
// size: Math.random() * 300 + 100
This is a jsfiddle that is equivalent to the first example that you linked. I just changed getting data to be from the JavaScript code instead of json file, since jsfiddle doesn't support external json files that well.
First Solution
Now, let's replace constant image with a set of different images
Instead of this code:
.attr("xlink:href", "https://github.com/favicon.ico")
we'll insert this code:
.attr("xlink:href", function(d) {
var rnd = Math.floor(Math.random() * 64 + 1);
var imagePath =
"http://www.bigbiz.com/bigbiz/icons/ultimate/Comic/Comic"
+ rnd.toString() + ".gif";
console.log(imagePath);
return imagePath;
})
and we'll get this:
Second Solution
As you suggested in your code from the question, one could use built-in SVG symbols.
Instead of this whole segment for inserting images:
node.append("image")
.attr("xlink:href", "https://github.com/favicon.ico")
.attr("x", -8)
.attr("y", -8)
.attr("width", 16)
.attr("height", 16);
we could use this code:
node.append("path")
.attr("d", d3.svg.symbol()
.size(function(d) {
return 100;
})
.type(function(d) {
return d3.svg.symbolTypes[~~(Math.random() * d3.svg.symbolTypes.length)];
}))
.style("fill", "steelblue")
.style("stroke", "white")
.style("stroke-width", "1.5px")
.call(force.drag);
and we'll get this:
Hope this helps.