D3js - Creating and easily updating a multi-line chart - javascript

I've created a little test line chart using D3, but since I am quite new to the library I am not sure what the best way would be to add multiple lines to a chart, at the moment I only have one line displayed in this fiddle.
I would like to display 2 lines on the chart, but I am unsure of how to achieve that without copy pasting code, which I am sure would be very inefficient as I would like to update/animate the graph at regular intervals based on user selection.
Instead of this,
var data = [12345,22345,32345,42345,52345,62345,72345,82345,92345,102345,112345,122345,132345,142345];
I would like to display something like this,
var data = [
[12345,42345,3234,22345,72345,62345,32345,92345,52345,22345], // line one
[1234,4234,3234,2234,7234,6234,3234,9234,5234,2234] // line two
];
Would this be a possibility? If so, what would be the best way to approach this, so that I can easily update/animate the graph when needed?
Note: I am merely trying to learn and to familiarize myself with D3 best practices and the library as a whole. Thanks.

This is possible and reasonable.
There is a tutorial that approaches this at the
D3 Nested Selection Tutorial
which describes nesting of data.
Below is code that I hacked from your fiddle to demonstrate this.
var data = [
[12345,42345,3234,22345,72345,62345,32345,92345,52345,22345],
[1234,4234,3234,2234,7234,6234,3234,9234,5234,2234]
];
var width = 625,
height = 350;
var x = d3.scale.linear()
.domain([0,data[0].length]) // Hack. Computing x-domain from 1st array
.range([0, width]);
var y = d3.scale.linear()
.domain([0,d3.max(data[0])]) // Hack. Computing y-domain from 1st array
.range([height, 0]);
var line = d3.svg.line()
.x(function(d,i) { return x(i); })
.y(function(d) { return y(d); });
var area = d3.svg.area()
.x(line.x())
.y1(line.y())
.y0(y(0));
var svg = d3.select("body").append("svg")
//.datum(data)
.attr("width", width)
.attr("height", height)
//.append("g");
var lines = svg.selectAll( "g" )
.data( data ); // The data here is two arrays
// for each array, create a 'g' line container
var aLineContainer = lines
.enter().append("g");
/*svg.append("path")
.attr("class", "area")
.attr("d", area);*/
aLineContainer.append("path")
.attr("class", "area")
.attr("d", area);
/*svg.append("path")
.attr("class", "line")
.attr("d", line);*/
aLineContainer.append("path")
.attr("class", "line")
.attr("d", line);
/*svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("cx", line.x())
.attr("cy", line.y())
.attr("r", 3.5);*/
// Access the nested data, which are values within the array here
aLineContainer.selectAll(".dot")
.data( function( d, i ) { return d; } ) // This is the nested data call
.enter()
.append("circle")
.attr("class", "dot")
.attr("cx", line.x())
.attr("cy", line.y())
.attr("r", 3.5);
​
One deficiency is that I computed the domain for the x and y axes off the first array, which is a hack, but not pertinent to your question exactly.

Related

D3 chart can't update -- enter and exit property of selection both empty

I'm trying to make a scatter plot using a .json file. It will let the user to select which group of data in the json file to be displayed. So I'm trying to use the update pattern.
The following code will make the first drawing, but every time selectGroup() is called(the code is in the html file), nothing got updated. The console.log(selection) did come back with a new array each time, but the enter and exit property of that selection is always empty.
Can anyone help me take a look? Thanks a lot!
var margin = {
top: 30,
right: 40,
bottom: 30,
left: 40
}
var width = 640 - margin.right - margin.left,
height = 360 - margin.top - margin.bottom;
var dataGroup;
var groupNumDefault = "I";
var maxX, maxY;
var svg, xAxis, xScale, yAxis, yScale;
//select and read data by group
function init() {
d3.json("data.json", function (d) {
maxX = d3.max(d, function (d) {
return d.x;
});
maxY = d3.max(d, function (d) {
return d.y;
});
console.log(maxY);
svg = d3.select("svg")
.attr("id", "scatter_plot")
.attr("width", 960)
.attr("height", 500)
.append("g")
.attr("id", "drawing_area")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//x-axis
xScale = d3.scale.linear().range([0, width]).domain([0, maxX]);
xAxis = d3.svg.axis()
.scale(xScale).orient("bottom").ticks(6);
//y-axis
yScale = d3.scale.linear().range([0, height]).domain([maxY, 0]);
yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(6);
svg.append("g")
.attr("class", "x_axis")
.attr("transform", "translate(0," + (height) + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y_axis")
.call(yAxis);
});
selectGroup(groupNumDefault);
}
//update data
function selectGroup(groupNum) {
d3.json("/data.json", function (d) {
dataGroup = d.filter(function (el) {
return el.group == groupNum;
});
console.log(dataGroup);
drawChart(dataGroup);
});
}
//drawing function
function drawChart(data) {
var selection = d3.select("svg").selectAll("circle")
.data(data);
console.log(selection);
selection.enter()
.append("circle")
.attr("class", "dots")
.attr("cx", function (d) {
console.log("updating!");
return xScale(d.x);
})
.attr("cy", function (d) {
return yScale(d.y);
})
.attr("r", function (d) {
return 10;
})
.attr("fill", "red");
selection.exit().remove();
}
init();
The problem here is on two fronts:
Firstly, your lack of a key function in your data() call means data is matched by index (position in data array) by default, which will mean no enter and exit selections if the old and current datasets sent to data() are of the same size. Instead, most (perhaps all) of the data will be put in the update selection when d3 matches by index (first datum in old dataset = first datum in new dataset, second datum in old dataset = second datum in new dataset etc etc)
var selection = d3.select("svg").selectAll("circle")
.data(data);
See: https://bl.ocks.org/mbostock/3808221
Basically, you need your data call adjusted to something like this (if your data has an .id property or anything else that can uniquely identify each datum)
var selection = d3.select("svg").selectAll("circle")
.data(data, function(d) { return d.id; });
This will generate enter() and exit() (and update) selections based on the data's actual contents rather than just their index.
Secondly, not everything the second time round is guaranteed be in the enter or exit selections. Some data may be just an update of existing data and not in either of those selections (in your case it may be intended to be completely new each time). However, given the situation just described above it's pretty much guaranteed most of your data will be in the update selection, some of it by mistake. To show updates you will need to alter the code like this (I'm assuming d3 v3 here, apparently it's slightly different for v4)
selection.enter()
.append("circle")
.attr("class", "dots")
.attr("r", function (d) {
return 10;
})
.attr("fill", "red");
// this new bit is the update selection (which includes the just added enter selection
// now, the syntax is different in v4)
selection // v3 version
// .merge(selection) // v4 version (remove semi-colon off preceding enter statement)
.attr("cx", function (d) {
console.log("updating!");
return xScale(d.x);
})
.attr("cy", function (d) {
return yScale(d.y);
})
selection.exit().remove();
Those two changes should see your visualisation working, unless of course the problem is something as simple as an empty set of data the second time around which would also explain things :-)

d3.js area() under a line using gradient function- x value only covering part of the graph

I am trying to create a line graph using d3.js, which takes a csv input and converts it to a data array with the keys x1, x2, class. Using that data I create a perceptron decision boundary using weights and the gradient function which updates and transitions after each iteration.
This works nicely.
What I am struggling with is the area under the curve, as I want positive values to be blue and negative to be red. The areas move with the line correctly, but something is wrong with the x values as the width of the area doesn't cover the whole graph. It covers about half, and starts about a quarter of the way in (I can't post an image.)
Here is the code I'm using for all of these elements, but I thin I am misunderstanding the way area uses the x attribute;
lineFunction = d3.svg.line()
.x(function(d) { return widthScale(d.x1); })
.y(function(d) { return heightScale(((-d.x1 * w1) - w0)/w2); })
.interpolate("linear");
var area = d3.svg.area()
.x(function(d) { return widthScale(d.x1); })
.y0(xPos)
.y1(function(d) { return heightScale(((-d.x1 * w1) - w0)/w2); })
.interpolate("linear");
var area2 = d3.svg.area()
.x(function(d) { return widthScale(d.x1); })
.y0(heightAlter)
.y1(function(d) { return heightScale(((-d.x1 * w1) - w0)/w2); })
.interpolate("linear");
lineGraph = canvas.append("path")
.attr("d", lineFunction(lineData))
.attr("class", "autoLine")
.attr("stroke", "black")
.attr("stroke-width", 1)
.attr("fill", "none")
.attr("clip-path", "url(#clip)");
shadedArea = canvas.append("path")
.data([data])
.attr("d", area)
.attr("class", "area")
.attr("clip-path", "url(#clip)");
shadedAreaPos = canvas.append("path")
.data([data])
.attr("d", area2)
.attr("class", "area2")
.attr("clip-path", "url(#clip)");
Any help would be much appreciated.
I managed to find that the root of the problem was the way in which the data array was ordered. I do not fully understand what is happening behind the scenes, but by simply reordering the array in ascending order on the x1 values, it now works correctly. I hope this helps someone.

Make An Line Chart Using Array - D3.js

I want to make an line chart using d3.js with php.
My array is like as below:
{"x_axis":["xyz","abc","pqr"],"y_axis":["1","2","2"]}
I want to put whole x_axis array on x-axis and y_axis array on y-axis of line graph.
Please advice me for the same.
Thank you in advance.
You will have to use d3.scale.ordinal and map function to plot the values on axis. See this plnkr for example.
var xScale = d3.scale.ordinal()
.rangeBands([0,300],0.01);
var yScale = d3.scale.ordinal()
.rangeBands([0,300],0.01);
xScaleMappedValues = xScale.domain(data.map( function(d) {console.log(d.x); return d.x;}))
yScaleMappedValues = yScale.domain(data.map( function(d) {console.log(d.y); return d.y;}))
EDIT :
To plot data points against those values
svg.append("g")
.attr("class","datapoint")
.selectAll(".gCircle")
.data(data)
.enter()
.append("g")
.append("circle")
.attr("cx", function(d) {console.log(d.x); return xScale(d.x) + xScale.rangeBand()/2 })
.attr("cy", function(d) { return yScale(d.y) + yScale.rangeBand()/2; })
.attr("r",5)

Zooming datasets in d3.js

I have overlayed two datasets, a boundary map and a point map in d3.js. I want to be able to zoom both datasets at the same time. With the current code, only the point map responds to the zoom. How can I zoom both datasets at the same time
The code is shown below
var canvas = d3.select("body").append("svg")
.attr("width",260)
.attr("height",400)
d3.json("/Maps/iowastate.json",function (data){
var group = canvas.selectAll("g")
.data(data.features)
.enter()
.append("g")
var projection =d3.geo.mercator()
.scale(250)
//.translate([0,0]);
var path = d3.geo.path().projection(projection);
var areas = group.append("path")
.attr("d", path)
.attr("class","area")
.attr("fill","black");
d3.csv("/Maps/detectors.csv",function (d){
var group = canvas.selectAll("g")
.data(d)
.enter()
.append("circle")
.attr("cx", function(d) {
return projection([d.StartLong,d.StartLat])[0];
})
.attr("cy", function(d,i) {
return projection([d.StartLong,d.StartLat])[1];
})
.attr("r", 0.1)
.style("fill", "red");
//console.log(projection(d[0].StartLat))
var zoom = d3.behavior.zoom()
.on("zoom",function(){
group.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
group.selectAll("path")
.attr("d", path.projection(projection));
});
canvas.call(zoom)
})
var zoom = d3.behavior.zoom()
.on("zoom",function(){
group.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
group.selectAll("path")
.attr("d", path.projection(projection));
});
canvas.call(zoom)
})
You are applying the right modifications, but twice to the same set of elements instead of the two different layers. To make it work, keep a reference to the other group (e.g. by using different variable names) and apply the transformations to both groups.

d3.js create objects on top of each other

I create rectangles in my SVG element using this code:
var rectangles = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect");
rectangles.attr("x", function (d) {
return xScale(getDate(d));
//return xScale(d.start);
})
.attr("y", function (d, i) {
return (i * 33);
})
.attr("height", 30)
.transition()
.duration(1000)
.attr("width", function (d) {
return d.length;
})
.attr("rx", 5)
.attr("ry", 5)
.attr("class", "rectangle")
.attr("onclick", function (d) {
return "runaction(" + d.start + ")";
});
How can I create new rectangles on top of the previous ones?
This is an answer to this question I got from Scott Murray, author of great introductory tutorials for d3.js http://alignedleft.com/tutorials/d3/ which helped me a lot with understanding its functionality. I hope he won't mind me putting his answer here for everyone's benefit.
Thank you very much Scott!
And yes, that's absolutely possible. Taking your example, let's say you want to draw one set of circles with the dataset called "giraffeData" bound to them. You would use:
svg.selectAll("circle")
.data(giraffeData)
.enter()
.append("circle");
But then you have a second data set (really just an array of values) called "zebraData". So you could use the same code, but change which data set you reference here:
svg.selectAll("circle")
.data(zebraData)
.enter()
.append("circle");
Of course, this will inadvertently select all the circles you already created and bind the new data to them — which isn't really what you want. So you'll have to help D3 differentiate between the giraffe circles and the zebra circles. You could do that by assigning them classes:
svg.selectAll("circle.giraffe")
.data(giraffeData)
.enter()
.append("circle")
.attr("class", "giraffe");
svg.selectAll("circle.zebra")
.data(zebraData)
.enter()
.append("circle")
.attr("class", "zebra");
Or, you could group the circles of each type into a separate SVG 'g' element:
var giraffes = svg.append("g")
.attr("class", "giraffe");
giraffes.selectAll("circle")
.data(giraffeData)
.enter()
.append("circle");
var zebras = svg.append("g")
.attr("class", "zebra");
zebras.selectAll("circle")
.data(zebraData)
.enter()
.append("circle");
I'd probably choose the latter, as then your DOM is more cleanly organized, and you don't have to add a class to every circle. You could just know that any circle inside the g with class zebra is a "zebra circle".

Categories