All of the d3 tutorials I\'ve found use data arranged in arrays of objects from which they graph one point for each object in the array. Given data in the following structure:<
The general rule when using a data-join is that you want a one-to-one mapping from data to elements. So, if you have two series in your scatterplot, you’ll want two container elements (such as G elements) to represent the series. Since you currently have only one data
array, you’ll also want to use array.map to convert the data representation into two parallel arrays with the same representation. This way, you don’t have to duplicate code for each series.
Say your data was represented in a CSV file with one column for the x-values, and multiple other columns for the y-values of each series:
x,y1,y2
5,90,22
25,30,25
45,50,80
65,55,9
85,25,95
If you want the code to be completely generic, you first need to compute the series’ names, such as ["y1", "y2"]
. (If you added a third column to the CSV file, it might be ["y1", "y2", "y3"]
.) You can compute the names using d3.keys, which extracts the named properties from an object. For example, d3.keys({foo: 1, bar: 2})
returns ["foo", "bar"]
.
// Compute the series names ("y1", "y2", etc.) from the loaded CSV.
var seriesNames = d3.keys(data[0])
.filter(function(d) { return d !== "x"; })
.sort();
Now that you have the series names, you can create an array of arrays of points. The outer array represents the series (of which there are two) and the inner arrays store the data points. You can simultaneously convert the points to a consistent representation (objects with x
and y
properties), allowing you to reuse code across series.
// Map the data to an array of arrays of {x, y} tuples.
var series = seriesNames.map(function(series) {
return data.map(function(d) {
return {x: +d.x, y: +d[series]};
});
});
Note this code uses the +
operator to coerce the CSV values to numbers. (CSV files are untyped, so they are initially strings.)
Now that you’ve mapped your data to a regular format, you can create G elements for each series, and then circle elements within for each point. The resulting SVG structure will look like this:
<g class="series">
<circle class="point" r="4.5" cx="1" cy="2"/>
<circle class="point" r="4.5" cx="3" cy="2"/>
…
</g>
<g class="series">
<circle class="point" r="4.5" cx="5" cy="4"/>
<circle class="point" r="4.5" cx="7" cy="6"/>
…
</g>
And the corresponding D3 code:
// Add the points!
svg.selectAll(".series")
.data(series)
.enter().append("g")
.attr("class", "series")
.style("fill", function(d, i) { return z(i); })
.selectAll(".point")
.data(function(d) { return d; })
.enter().append("circle")
.attr("class", "point")
.attr("r", 4.5)
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); });
I’ve also added a bit of code to assign each series a unique color by adding a fill style to the containing G element. There are lots of different ways to do this, of course. (You might want to be more specific about the color for each series, for example.) I’ve also left out the code that computes the domains of your x and y scales (as well as rendering the axes), but if you want to see the entire working example:
Place the two circles for each data point into a single svg:g
element. This produces a one-to-one mapping for the data to elements but still allows you to show two different points.
var nodeEnter = vis1.selectAll("circle")
.data(dataSet)
.enter()
.insert("svg:g");
nodeEnter.insert("svg:circle")
.attr("cx", function (d) { return 100 - d.xVar})
.attr("cy", function (d) { return 100 - d.yVar1})
.attr("r", 2)
.style("fill", "green");
nodeEnter.insert("svg:circle")
.attr("cx", function (d) { return 100 - d.xVar})
.attr("cy", function (d) { return 100 - d.yVar2})
.attr("r", 2)
.style("fill", "blue");
Working JSFiddle.