Wednesday, 13 May 2020

Web Tutorial: D3 Line Chart (Part 2/2)

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


No comments:

Post a Comment