In the last part, we changed the naming conventions, the labels and the drop-down list to reflect our new direction. Now let's truly turn this sucker into a line graph. The scale, legend and filler are fine. It's the actual chart area we need to change.
Get down to the part where we're inserting rect tags into
chart. Change them to circle tags.
chart.selectAll("circle")
.data(dataSet.stats)
.enter()
.append("circle")
.attr("x", function(d, i)
{
return ((i * (config.dataWidth + config.dataSpacing)) + 2) + "em";
})
.attr("y", function(d)
{
return height + "em";
})
.attr("width", function(d)
{
return (config.dataWidth) + "em";
})
.attr("height", function(d)
{
return "0em";
})
.transition()
.duration(500)
.attr("y", function(d)
{
return (height - (d * config.scale)) + "em";
})
.attr("height", function(d)
{
return (d * config.scale) + "em";
});
Since these are circle tags, we no longer want to specify
x and
y attributes, but rather
cx and
cy. Also, we will not be specifying
width, but rather
r, which is the radius. We won't specify
height at all, so get rid of it.
chart.selectAll("circle")
.data(dataSet.stats)
.enter()
.append("circle")
.attr("cx", function(d, i)
{
return ((i * (config.dataWidth + config.dataSpacing)) + 2) + "em";
})
.attr("cy", function(d)
{
return height + "em";
})
.attr("r", function(d)
{
return (config.dataWidth) + "em";
})
//.attr("height", function(d)
//{
// return "0em";
//})
.transition()
.duration(500)
.attr("cy", function(d)
{
return (height - (d * config.scale)) + "em";
})
//.attr("height", function(d)
//{
// return (d * config.scale) + "em";
//});
cy is currently fine as it is. But
cx needs to be the center of the circle. So we add half of the
dataWidth property into the equation.
chart.selectAll("circle")
.data(dataSet.stats)
.enter()
.append("circle")
.attr("cx", function(d, i)
{
return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("cy", function(d)
{
return height + "em";
})
.attr("r", function(d)
{
return (config.dataWidth) + "em";
})
.transition()
.duration(500)
.attr("cy", function(d)
{
return (height - (d * config.scale)) + "em";
});
Just for fun, let's set
r to animate as well, starting from 0 to
dataWidth.
chart.selectAll("circle")
.data(dataSet.stats)
.enter()
.append("circle")
.attr("cx", function(d, i)
{
return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("cy", function(d)
{
return height + "em";
})
.attr("r", function(d)
{
return "0em";
})
.transition()
.duration(500)
.attr("cy", function(d)
{
return (height - (d * config.scale)) + "em";
})
.attr("r", function(d)
{
return (config.dataWidth) + "em";
});
Also, the CSS needs to be modified. We're no longer styling rect but circle tags.
.lineChartSvg circle
{
fill: rgba(255, 255, 0, 1);
}
Uh-oh. Guess what we did wrong?
Let's change
dataWidth and
dataSpacing in the
config object.
var config =
{
"scale": 0.5,
"dataWidth": 0.5,
"dataSpacing": 10,
"scaleWidth": 6,
"legendHeight": 4,
"max": 0,
"mean": 0,
Looking good now!
Time to add lines. Instead of modifying something, we'll be adding new code. We begin with the standard D3 stuff, appending lines into chart. The data is the
stats array of the
dataSet object, and styling is the CSS class
lineChartDataLine which we'll create.
chart.selectAll("circle")
.data(dataSet.stats)
.enter()
.append("circle")
.attr("cx", function(d, i)
{
return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("cy", function(d)
{
return height + "em";
})
.attr("r", function(d)
{
return "0em";
})
.transition()
.duration(500)
.attr("cy", function(d)
{
return (height - (d * config.scale)) + "em";
})
.attr("r", function(d)
{
return (config.dataWidth) + "em";
});
chart.selectAll("line")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
chart.selectAll("text")
.data(dataSet.stats)
.enter()
.append("text")
.attr("x", function(d, i)
{
return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y", function(d)
{
return (height - (d * config.scale) - 1) + "em";
})
.text(function(d)
{
return d;
});
Our motive here is to join all the circle tags with line tags. So we'll specify
x1,
y1,
x2 and
y2 attributes.
chart.selectAll("line")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine")
.attr("x1", function(d)
{
})
.attr("y1", function(d)
{
})
.attr("x2", function(d)
{
})
.attr("y2", function(d)
{
});
But only from the
second element in the
stats array, will you know what the values of
x2 and
y2 are. So for the first element in the
stats array, the line stays within the first circle and does not extend out to the next circle. That means for
x1, we return the same value we did for the circle. We need to use
i here, to keep track of the index.
chart.selectAll("line")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d)
{
})
.attr("x2", function(d)
{
})
.attr("y2", function(d)
{
});
But this applies only if it is not the first element in the
stats array. Declare
val and set to
i. Check if
i is greater than 0. If it is, that means we decrement
val (to set it to the previous element's index) and use it to calculate the returned result. If it's the first element, there's no previous element, so just use the first element's values.
chart.selectAll("line")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
var val = i;
if (i > 0)
{
val = val - 1;
}
return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d)
{
})
.attr("x2", function(d)
{
})
.attr("y2", function(d)
{
});
For
y1, we'll also need
i. But this will be done slightly differently. If we're dealing with the first element, meaning
i is 0, then we'll use use the value of
d as what we did for the circle tags.
chart.selectAll("line")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
var val = i;
if (i > 0)
{
val = val - 1;
}
return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d, i)
{
return (height - (d * config.scale)) + "em";
})
.attr("x2", function(d)
{
})
.attr("y2", function(d)
{
});
But if it's
not the first element, then we have to take the value of the previous element.
chart.selectAll("line")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
var val = i;
if (i > 0)
{
val = val - 1;
}
return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d, i)
{
var val = d;
if (i > 0)
{
val = dataSet.stats[i - 1];
}
return (height - (val * config.scale)) + "em";
})
.attr("x2", function(d)
{
})
.attr("y2", function(d)
{
});
For
x2, we're just using the current element. Note that for
x1 and
x2, we follow what we did for circles because we want the line to begin and end in the middle of each circle.
chart.selectAll("line")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
var val = i;
if (i > 0)
{
val = val - 1;
}
return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d, i)
{
var val = d;
if (i > 0)
{
val = dataSet.stats[i - 1];
}
return (height - (val * config.scale)) + "em";
})
.attr("x2", function(d, i)
{
return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y2", function(d)
{
});
y2 will not require
i. It uses the same return value as the circle's
cy attribute, without
val.
chart.selectAll("line")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
var val = i;
if (i > 0)
{
val = val - 1;
}
return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d, i)
{
var val = d;
if (i > 0)
{
val = dataSet.stats[i - 1];
}
return (height - (val * config.scale)) + "em";
})
.attr("x2", function(d, i)
{
return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y2", function(d)
{
return (height - (d * config.scale)) + "em";
});
Don't forget to write the CSS class
lineChartDataLine. Make it 2 pixels thick and
yellow.
.lineChartLine
{
stroke: rgba(255, 255, 0, 1);
stroke-width: 1px;
}
.lineChartDataLine
{
stroke: rgba(255, 255, 0, 1);
stroke-width: 2px;
}
.lineChartFadedLine
{
stroke: rgba(255, 255, 0, 0.2);
stroke-width: 1px;
}
Hey, I still see no lines!
Ah, that's because we already appended a series of faded lines before attempting to append these
yellow lines. This won't work a second time with the same tags... or will it?
Let's try this. Instead of putting in "line" as the argument for the
selectAll() method, use "line.lineChartDataLine" which basically means select all line tags styled using
lineChartDataLine.
chart.selectAll("line.lineChartDataLine")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
var val = i;
if (i > 0)
{
val = val - 1;
}
return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d, i)
{
var val = d;
if (i > 0)
{
val = dataSet.stats[i - 1];
}
return (height - (val * config.scale)) + "em";
})
.attr("x2", function(d, i)
{
return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y2", function(d)
{
return (height - (d * config.scale)) + "em";
});
Here are your lines.
Let's animate those lines, shall we?
We'll animate
y1 and
y2. So let's chain a
transition() method and a
duration() method with an argument of 500.
chart.selectAll("line.lineChartDataLine")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
var val = i;
if (i > 0)
{
val = val - 1;
}
return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d, i)
{
var val = d;
if (i > 0)
{
val = dataSet.stats[i - 1];
}
return (height - (d * config.scale)) + "em";
})
.attr("x2", function(d, i)
{
return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y2", function(d)
{
return (height - (d * config.scale)) + "em";
})
.transition()
.duration(500);
Then make a copy of the
y1 and
y2 specifications.
chart.selectAll("line.lineChartDataLine")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
var val = i;
if (i > 0)
{
val = val - 1;
}
return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d, i)
{
var val = d;
if (i > 0)
{
val = dataSet.stats[i - 1];
}
return (height - (d * config.scale)) + "em";
})
.attr("x2", function(d, i)
{
return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y2", function(d)
{
return (height - (d * config.scale)) + "em";
})
.transition()
.duration(500)
.attr("y1", function(d, i)
{
var val = d;
if (i > 0)
{
val = dataSet.stats[i - 1];
}
return (height - (d * config.scale)) + "em";
})
.attr("y2", function(d)
{
return (height - (d * config.scale)) + "em";
});
And set the first
y1 and
y2 specification to return
height instead, which will set it right at the bottom of the chart.
chart.selectAll("line.lineChartDataLine")
.data(dataSet.stats)
.enter()
.append("line")
.attr("class", "lineChartDataLine");
.attr("x1", function(d, i)
{
var val = i;
if (i > 0)
{
val = val - 1;
}
return ((val * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y1", function(d)
{
return height + "em";
})
.attr("x2", function(d, i)
{
return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
})
.attr("y2", function(d)
{
return height + "em";
})
.transition()
.duration(500)
.attr("y1", function(d, i)
{
var val = d;
if (i > 0)
{
val = dataSet.stats[i - 1];
}
return (height - (d * config.scale)) + "em";
})
.attr("y2", function(d)
{
return (height - (d * config.scale)) + "em";
});
You'll see this animates quite nicely when you select different values from the drop-down lists!
But you know, don't take my word for it. Here's a demo!
Final notes
This web tutorial was way shorter than it could have been. You'll see that this code was written in a way that made modification easy.
I can't take all the credit of course. D3 certainly facilitates easy maintenance.
Laying it on the line for you!
T___T