Dynamically generating multiple d3 svg graphs - javascript

I've got an array of objects called graphData (size varies). Each element contains all the information required to create a d3 graph, and I am able to successfully draw the graphs if I access graphData elements by hardcoding (i.e. graphdata[0], graphdata[1] etc).
The problem comes when I attempt to use a for loop to generate one graph for each of the elements. Looked around stackoverflow and the web, but the solutions are all about generating a fixed number of multiple graphs, not generating multiple graphs dynamically.
Below is my working code for generating one graph. What is the recommended way to generate x number of graphs automatically?
var graphData = data.graph;
var RADIUS = 15;
var edgeData = graphData[0].edges;
var nodeData = graphData[0].nodes;
var stageNum = graphData[0].stage;
var xScale = d3.scale.linear()
.domain([d3.min(edgeData, function (d) {
return d.start[0];
}),
d3.max(edgeData, function (d) {
return d.start[0];
})])
.range([50, w - 100]);
var yScale = d3.scale.linear()
.domain([d3.min(edgeData, function (d) {
return d.start[1];
}),
d3.max(edgeData, function (d) {
return d.start[1];
})])
.range([50, h - 100]);
var rScale = d3.scale.linear()
.domain([0, d3.max(edgeData, function (d) {
return d.start[1];
})])
.range([14, 17]);
// already have divs with classes stage1, stage2... created.
var svg = d3.select(".stage" + stageNum).append("svg")
.attr({"width": w, "height": h})
.style("border", "1px solid black");
var elemEdge = svg.selectAll("line")
.data(edgeData)
.enter();
var edges = elemEdge.append("line")
.attr("x1", function (d) {
return xScale(d.start[0]);
})
.attr("y1", function (d) {
return yScale(d.start[1]);
})
.attr("x2", function (d) {
return xScale(d.end[0]);
})
.attr("y2", function (d) {
return yScale(d.end[1]);
})
.attr("stroke-width", 2)
.attr("stroke", "black");
var elemNode = svg.selectAll("circle")
.data(nodeData)
.enter();
var nodes = elemNode.append("circle")
.attr("cx", function (d) {
return xScale(parseInt(d.x));
})
.attr("cy", function (d) {
return yScale(parseInt(d.y));
})
.attr({"r": rScale(RADIUS)})
.style("fill", "yellow")
.style("stroke", "black");

Mike Bostock recommends implementing charts as reusable closures with methods. This would be an ideal implementation in your case as you want to have
multiple graphs with different data
potential reloading with new data (hopefully this is what you mean by dynamic?)
In broad strokes, what you want to do is wrap your code above into a function in very much the same way Mike describes in the post above, and then have data be an attribute of your closure. So here is some badly hacked code:
// your implementation here
var chart = function(){...}
var graphData = d3.json('my/graphdata.json', function(error, data){
// now you have your data
});
// let's say you have a div called graphs
var myGraphs = d3.select('.graphs')
.data(graphData)
.enter()
.append('g')
//now you have g elements for each of your datums in the graphData array
//we use the saved selection above and call the chart function on each of the elements in the selection
myGraphs.call(chart);
//note that internally in your `chart` closure, you have to take in a selection
//object and process it(data is already bound to each of your selections from above):
function chart(selection) {
selection.each(function(data) {
//...
Here is some more good reading on the topic.

Well you can try the following approach.
var graphData = data.graph;
//forEach will return each element for the callback, you can then make use of the e1 to draw the graph.
graphData.forEach(function(e1){
//graph code goes here.
});

providing this as your source array
//it's just a single circle in 3, 4
var stuff = [3, 4];
var source = [ [stuff, stuff], [stuff] ];
a bit of Array stuff
Array.prototype.max = function() {
return Math.max.apply(null, this);
};
Array.prototype.min = function() {
return Math.min.apply(null, this);
};
setup:
var dim = [];
source.forEach(function(elem){
elem.forEach(function(circle){
dim.push(circle.min());
dim.push(circle.max());
});
});
var min = dim.min();
var max = dim.max();
var x = d3.scale.linear()
.domain([min, max])
.scale([yourscale]);
var y = d3.scale.linear()
.domain([min, max])
.scale([yourscale]);
d3.select('body').selectAll('div')
.data(source) //first step: a div with an svg foreach array in your array
.enter()
.append('div')
.append('svg')
.selectAll('circle') //second step: a circle in the svg for each item in your array
.data(function(d){
return d; //returns one of the [stuff] arrays
}).enter()
.append('circle')
.attr('r', 5)
.attr('cx', function(d){
return x(d[0]);
})
.attr('cy', function(d){
return y(d[1]);
})
.style('fill','blue');

Related

Ajax callback: d3 outputs the same thing when different inputs are given

I have a function called show_product_price to draw line based on input data (containing date and price).
When I did a test by manually generating 2 dataset, then call the function, everything works fine and 2 lines are drawn.
like this:
show_product_price(data1, "origin");
show_product_price(data2, "competitor");
But when I try to call the function using 2 ajax calls. Problem happens. The function does receive 2 different datasets, but the 2 lines drawn overlap each other. Basically the 2 lines use the same dataset, even though during the 2 ajax call, the function receive 2 daatset.
I am not sure why this is happening.
The ajax looks like:
//inside one function, ajax call to get one dataset, I checked show_product_price() receive it successfuly
$.ajax({
url: 'xxx',
success: function(data) {
show_product_price(data, "origin");
}
});
//inside another function, get another dataset, also received successfuly
$.ajax({
url: 'xxx',
success: function(data) {
show_product_price(data, "competitor");
}
});
The function is:
function show_product_price(input, who) {
var data = JSON.parse(JSON.stringify(input));
// I did an alert() here, i amde sure the data passed in is different
//#visualisation is a svg
var vis = d3.select("#visualisation"),
WIDTH = 1000,
HEIGHT = 500,
MARGINS = {
top: 50,
right: 50,
bottom: 50,
left: 50
},
//set up scale here
xScale = d3.time.scale()
.range([MARGINS.left, WIDTH - MARGINS.right])
.domain([new Date(data[0].date),
d3.time.day.offset(new Date(data[data.length - 1].date), 1)]),
yScale = d3.scale.linear().range([HEIGHT - MARGINS.top, MARGINS.bottom])
.domain([0, d3.max(data, function(d) {
return d.price;
}) * 1.5]);
if (who == "origin") {
// Append axis and text, code works, omit here
}
var parseDate = d3.time.format("%Y-%m-%d").parse;
var lineGen = d3.svg.line()
.x(function(d) {
return xScale(parseDate(d.date));
})
.y(function(d) {
return yScale(d.price);
})
.interpolate("basis");
var color = "green";
if (who == "competitor") {
color = "red";
}
// draw each data point as a circle
vis.selectAll("circle." + who)
.data(data)
.enter()
.append("circle")
.attr("class", who)
.attr("cx", function(d) {
return xScale(parseDate(d.date));
})
.attr("cy", function(d) {
return yScale(d.price);
})
.attr("fill", color)
.transition()
.delay(function(d, i) { return i * (3000 / (data.length - 1)); })
.attr("r", 5);
//draw lines connecting the circles
vis.append('svg:path')
.attr('d', lineGen(data))
.attr('stroke', color)
.attr("id", who)
.attr('stroke-width', 2)
.attr('fill', 'none');
}

D3 stops plotting points when data source is modified

I am plotting points on a UK map using D3 off a live data stream. When the data points exceed 10,000 the browser becomes sluggish and the animation is no longer smooth. So I modify the dataPoints array to keep only the last 5000 points.
However when I modify the dataPoints the first time using splice() D3 stops rendering any new points. The old points gradually disappear (due to a transition) but there are no new points. I am not sure what I am doing wrong here.
I have simulated the problem by loading data of a CSV as well storing it in memory and plotting them at a rate of 1 point every 100ms. Once the number of dots goes above 10 I splice to retain the last 5 points. I see the same behaviour. Can someone review the code and let me know what I am doing wrong?
Setup and the plotting function:
var width = 960,
height = 1160;
var dataPoints = []
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height);
var projection = d3.geo.albers()
.center([0, 55.4])
.rotate([4.4, 0])
.parallels([40, 70])
.scale(5000)
.translate([width / 2, height / 2]);
function renderPoints() {
var points = svg.selectAll("circle")
.data(dataPoints)
points.enter()
.append("circle")
.attr("cx", function (d) {
prj = projection([d.longitude, d.latitude])
return prj[0];
})
.attr("cy", function (d) {
prj = projection([d.longitude, d.latitude])
return prj[1];
})
.attr("r", "4px")
.attr("fill", "blue")
.attr("fill-opacity", ".4")
.transition()
.delay(5000)
.attr("r", "0px")
}
/* JavaScript goes here. */
d3.json("uk.json", function(error, uk) {
if (error) return console.error(error);
console.log(uk);
var subunits = topojson.feature(uk, uk.objects.subunits);
var path = d3.geo.path()
.projection(projection);
svg.selectAll(".subunit")
.data(subunits.features)
.enter().append("path")
.attr("class", function(d) { return "subunit " + d.id })
.attr("d", path);
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a,b) {return a!== b && a.id !== 'IRL';}))
.attr("d", path)
.attr("class", "subunit-boundary")
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a,b) {return a=== b && a.id === 'IRL';}))
.attr("d", path)
.attr("class", "subunit-boundary IRL")
svg.selectAll(".place-label")
.attr("x", function(d) { return d.geometry.coordinates[0] > -1 ? 6 : -6; })
.style("text-anchor", function(d) { return d.geometry.coordinates[0] > -1 ? "start": "end"; });
svg.selectAll(".subunit-label")
.data(topojson.feature(uk, uk.objects.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; })
// function applyProjection(d) {
// console.log(d);
// prj = projection(d)
// console.log(prj);
// return prj;
// }
lon = -4.6
lat = 55.45
dataPoints.push([lon,lat])
renderPoints()
});
Function to cleanup old points
var cleanupDataPoints = function() {
num_of_elements = dataPoints.length
console.log("Pre:" + num_of_elements)
if(num_of_elements > 10) {
dataPoints = dataPoints.splice(-5, 5)
}
console.log("Post:" + dataPoints.length)
}
Loading data from CSV and plotting at a throttled rate
var bufferedData = null
var ptr = 0
var renderNext = function() {
d = bufferedData[ptr]
console.log(d)
dataPoints.push(d)
ptr++;
renderPoints()
cleanupDataPoints()
if(ptr < bufferedData.length)
setTimeout(renderNext, 100)
}
d3.csv('test.csv', function (error, data) {
bufferedData = data
console.log(data)
setTimeout(renderNext, 100)
})
In the lines
points = svg.selectAll("circle")
.data(dataPoints)
points.enter() (...)
d3 maps each element in dataPoints (indexed from 0 to 5000) to the circle elements (of which there should be 5000 eventually). So from its point of view, there is no enter'ing data: there are enough circles to hold all your points.
To make sure that the same data point is mapped to the same html element after it changed index in its array, you need to use an id field of some sort attached to each of your data point, and tell d3 to use this id to map the data to elements, instead of their index.
points = svg.selectAll("circle")
.data(dataPoints, function(d){return d.id})
If the coordinates are a good identifier for your point, you can directly use:
points = svg.selectAll("circle")
.data(dataPoints, function(d){return d.longitude+" "+d.latitude})
See https://github.com/mbostock/d3/wiki/Selections#data for more details.

Plotting svg circles based off csv data

I'm trying to plot circles from data in my csv file, but the circles are not appearing on the svg canvas. I believe the problem stems from how I load in the data (it gets loaded as an array of objects), but I'm not quite sure how to figure out what to do next.
Based off this tutorial: https://www.dashingd3js.com/svg-text-element
D3.js code:
var circleData = d3.csv("files/data.csv", function (error, data) {
data.forEach(function (d) {
d['KCComment'] = +d['KCComment'];
d['pscoreResult'] = +d['pscoreResult'];
d['r'] = +d['r'];
});
console.log(data);
});
var svg = d3.select("body").append("svg")
.attr("width", 480)
.attr("height", 480);
var circles = svg.selectAll("circle")
.data(circleData)
.enter()
.append("circle");
var circleAttributes = circles
.attr("cx", function (d) { return d.KCComment; })
.attr("cy", function (d) { return d.pscoreResult; })
.attr("r", function (d) { return d.r; })
.style("fill", "green");
var text = svg.selectAll("text")
.data(circleData)
.enter()
.append("text");
var textLabels = text
.attr("x", function(d) { return d.KCComment; })
.attr("y", function(d) { return d.pscoreResult; })
.text(function (d) { return "( " + d.KCComment + ", " + d.pscoreResult + " )"; })
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", "red");
What the CSV looks like:
fmname, fmtype, KCComment, pscoreResult, r
test1, type1, 7.1, 8, 39
test2, type2, 1.2, 3, 12
You should have the circle-drawing code within the d3.csv function's callback, so it's only processed when the data is available.
d3.csv("data.csv", function (error, circleData) {
circleData.forEach(function (d) {
d['KCComment'] = +d['KCComment'];
d['pscoreResult'] = +d['pscoreResult'];
d['r'] = +d['r'];
});
console.log(circleData);
// Do the SVG drawing stuff
...
// Finished
});
Also note that instead of setting var circleData = d3.csv(... you should just define it in the callback function.
Here's a plunker with the working code: http://embed.plnkr.co/fzBX0o/preview
You'll be able to see a number of further issues now: both circles are overlapping and only one quarter is visible. That's because your KCComment and pscoreResult values used to define the circles' cx and cy are too small. Try multiplying them up so that the circles move right and down and are a bit more visible! Same is true of the text locations, but I'll leave those problems for you to solve

updating multi series area graph in d3

I am creating an area graph using d3.
This code works, but I am not happy with the data structure in the update method (toggle), indeed I don't understand what I am doing wrong.
When creating the graph I pass in the data via:
data([nsw,qld])
// then draw the chart
When I update the graph I pass in the data via:
chart.data([nsw]);
// draw the nsw series in the chart
chart.data([qld]);
// draw the qld series in the chart
Surely I can update the data by passing it in in the correct format of [nsw,qld]
<script type="text/javascript" src="lib/d3.min.js" charset="utf-8"></script>
<svg id="chart"></svg>
<script>
var NSW = "NSW";
var QLD = "QLD";
var width = 400;
var height = 400;
var years = [1,2,3,4,5];
var data = years.map(function(){ return [Math.random(),Math.random()]; }); // generate bogus data
var nsw = data.map(function(d) { return d[0];}); // extract new south wales data
var qld = data.map(function(d) { return d[1];}); // extract queensland data
var chart = d3.select("#chart").attr("width", width).attr("height", height).append("g");
var x = d3.scale.linear().domain([0, years.length]).range([0, width]);
var y = d3.scale.linear().domain([0, d3.max(data, function(d){ return Math.max(d[0], d[1]); })]).range([height,0]);
var area = d3.svg.area().x(function(d,i) { return x(i); }).y0(height).y1(function(d, i) { return y(d); });
console.log([nsw,qld])
chart
.selectAll("path.area")
.data([nsw,qld]) // !!! here i can pass both arrays in.
.enter()
.append("path")
.attr("fill", "rgba(0,0,0,0.5)")
.attr("class", function(d,i) { return [NSW,QLD][i]; })
.attr("d", area);
chart.on("click", function() {
data = years.map(function(){return [ Math.random(),Math.random()];}); // switch in some new random data
var nsw = data.map(function(d) { return d[0];})
var qld = data.map(function(d) { return d[1];})
y.domain([0, d3.max(data, function(d){ return Math.max(d[0], d[1]); })]).range([height, 0]);
var svg = chart.transition();
/*
svg
.selectAll("path")
.data([nsw,qld]) // !!! this doesn't work.
.duration(750)
.attr("d", function(d) { return area(d); });
*/
chart.data([nsw]); // !!! here i'm only passing in one!
svg.select("path.NSW")
.duration(750)
.attr("d", function(d) {return area(d); });
chart.data([qld]); // !!! ...and then another
svg.select("path.QLD")
.duration(750)
.attr("d", function(d) { return area(d); });
});
</script>
Link to fiddle:
http://jsfiddle.net/MLA3x/
The problem is that you're using a transition in the update part and not a selection. That is, you're calling .data() on a transition, which you can't do. It works fine if you add the transition after updating the data:
chart
.selectAll("path")
.data([nsw,qld])
.transition()
.duration(750)
.attr("d", function(d) { return area(d); });
Complete demo here.

d3.js referencing a single array column

I have an array of generated values to plot a line function. However, when I call the array, the function only returns single values of the array, rather than the column, and hence draws a straight line (http://tributary.io/inlet/8822590). What is the correct syntax here? Thanks in advance.
// Create data
var v1 = 4.137,
t = 10,
x = [],
y = [];
for (i = 0.1; i < 190; i += 0.1) {
x.push(i);
y.push((Math.pow((v1 / i), 1 / t) - 1) * 100);
}
var data = [x, y];
// Scale data
var xscale = d3.scale.linear()
.domain(d3.extent(data, function (d) {
return d[0];
}))
var yscale = d3.scale.linear()
.domain(d3.extent(data, function (d) {
return d[1];
}))
var line = d3.svg.line()
.x(function (d) {
return xscale(d[0])
})
.y(function (d) {
return yscale(d[1])
});
var svg = d3.select("svg");
var path = svg.append("path")
.data([data])
.attr("d", line) //this calls the line function with this element's data
.style("fill", "none")
.style("stroke", "#000000")
.attr("transform", "translate(" + [96, 94] + ")")
D3 expects the data for a line to be an array of array where each element determines one line and each element within the inner array the point of the line. You've passed in a single array (for one line) with two elements, so you get two points.
To plot all the points, push the coordinates as separate elements:
for (i = 0.1; i < 190; i += 0.1) {
data.push([i, (Math.pow((v1/i),1/t)-1)*102]);
}
The rest of your code can remain basically unchanged. Complete example here.

Categories

Resources