Sunday 10 May 2020

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

Following the D3 web tutorial in March, it's time to naturally progress to line charts in D3. This web tutorial will involve a lot less than the previous one because we'll be reusing a lot of stuff.

That's right, I won't be walking you through the layout and stuff. If you need that, go back to the first D3 tutorial. We will be taking code from there wholesale and modifying it to give us a line chart. How cool is that?!

So let's copy the entire code here. First of all, let's change everything that says "bar" to "line". This will serve to keep naming conventions straight and avoid confusion.
<!DOCTYPE html>
<html>
    <head>
        <title>D3 Line Chart</title>

        <style>
            body
            {
                font-size: 12px;
                font-family: arial;
            }

            .lineDashboard
            {
                height: 2em;
                width: 100%;
                text-align: center;
            }

            .lineChart
            {
                outline: 1px solid #000000;
                background-color: rgba(200, 0, 0, 1);
            }

            .lineChartContainer
            {
                margin: 0 auto 0 auto;
            }

            .lineScaleSvg
            {
                width: 5em;
                height: 20em;
                float: left;
                background-color: rgba(0, 255, 0, 0);
            }

            .lineScaleSvg text
            {
                fill: rgba(255, 255, 0, 1);
                text-anchor: end;
                font-size: 0.5em;
            }

            .lineChartSvg
            {
                width: 20em;
                height: 20em;
                float: left;
                background-color: rgba(0, 0, 255, 0);
            }

            .lineChartSvg text
            {
                fill: rgba(255, 255, 0, 1);
                text-anchor: middle;
                font-weight: bold;
            } 

            .lineChartSvg rect
            {
                fill: rgba(255, 255, 0, 1);
            }

            .lineFillerSvg
            {
                width: 5em;
                height: 3em;
                float: left;
                background-color: rgba(0, 0, 0, 0);
            }

            .lineLegendSvg
            {
                width: 20em;
                height: 3em;
                float: left;
                background-color: rgba(255, 0, 0, 0);
            }

            .lineLegendSvg text
            {
                fill: rgba(255, 255, 0, 1);
                text-anchor: middle;
                font-weight: bold;
            } 

            .lineChartDataMean
            {
                stroke: rgba(0, 0, 0, 1);
                stroke-width: 1px;
                stroke-dasharray: 1, 5;
            } 

            .lineChartLine
            {
                stroke: rgba(255, 255, 0, 1);
                stroke-width: 1px;
            }  

            .lineChartFadedLine
            {
                stroke: rgba(255, 255, 0, 0.2);
                stroke-width: 1px;
            }      
        </style>

        <script src="https://d3js.org/d3.v3.min.js"></script>
    </head>

    <body>
        <div class="lineChartContainer">
            <div class="lineDashboard">
                <select id="ddlYear">

                </select>

                <select id="ddlStat">

                </select>
            </div>

            <div class="lineChart">
                <svg class="lineScaleSvg">

                </svg>

                <svg class="lineChartSvg">

                </svg>

                <br style="clear:both"/>

                <svg class="lineFillerSvg">

                </svg>

                <svg class="lineLegendSvg">

                </svg>
            </div>
        </div>

        <script>
        var graphData =
        {
            "cols":
            [
                {
                    "title": "Adam Lallana",
                    "stats":
                    [
                        {"year": 2015, "goals": 7, "appearances": 49},
                        {"year": 2016, "goals": 8, "appearances": 35},
                        {"year": 2017, "goals": 0, "appearances": 15},
                        {"year": 2018, "goals": 0, "appearances": 16}
                    ]
                },
                {
                    "title": "Sadio ManĂ©",
                    "stats":
                    [
                        {"year": 2016, "goals": 13, "appearances": 29},
                        {"year": 2017, "goals": 20, "appearances": 44},
                        {"year": 2018, "goals": 26, "appearances": 50}
                    ]
                },
                {
                    "title": "Roberto Firminho",
                    "stats":
                    [
                        {"year": 2015, "goals": 11, "appearances": 49},
                        {"year": 2016, "goals": 12, "appearances": 41},
                        {"year": 2017, "goals": 27, "appearances": 54},
                        {"year": 2018, "goals": 16, "appearances": 48}
                    ]
                },
                {
                    "title": "Divock Origi",
                    "stats":
                    [
                        {"year": 2015, "goals": 10, "appearances": 33},
                        {"year": 2016, "goals": 11, "appearances": 43},
                        {"year": 2017, "goals": 0, "appearances": 1},
                        {"year": 2018, "goals": 7, "appearances": 21}
                    ]
                },
                {
                    "title": "Daniel Sturridge",
                    "stats":
                    [
                        {"year": 2015, "goals": 13, "appearances": 25},
                        {"year": 2016, "goals": 7, "appearances": 27},
                        {"year": 2017, "goals": 3, "appearances": 14},
                        {"year": 2018, "goals": 4, "appearances": 27}
                    ]
                },
                {
                    "title": "James Milner",
                    "stats":
                    [
                        {"year": 2015, "goals": 7, "appearances": 45},
                        {"year": 2016, "goals": 7, "appearances": 40},
                        {"year": 2017, "goals": 1, "appearances": 47},
                        {"year": 2018, "goals": 7, "appearances": 45}
                    ]
                },
            ],
            "rows": [2015, 2016, 2017, 2018],
            "stats": ["goals", "appearances"]
        };

        var config =
        {
            "scale": 0.5,
            "dataWidth": 10,
            "dataSpacing": 1,
            "scaleWidth": 6,
            "legendHeight": 4,
            "max": 0,
            "mean": 0,
            "getChartHeight": function()
            {
                return (this.max * this.scale * 1.5);
            },
            "getChartWidth": function(datalength)
            {
                return (datalength * (this.dataWidth + this.dataSpacing)) + (datalength * 0.5);
            },
            "setData": function ()
            {
                var year = d3.select("#ddlYear").node().value;
                var stat = d3.select("#ddlStat").node().value;

                var dataSet =
                {
                  "labels": [],
                  "stats": []
                };

                for (var i = 0; i < graphData.cols.length; i++)
                {
                    var filtered = graphData.cols[i].stats.filter(function(x) { return x.year == year;})

                    if (filtered.length > 0)
                    {
                      dataSet.labels.push(graphData.cols[i].title);
                      dataSet.stats.push(filtered[0][stat]);
                    }
                }

                config.mean = d3.mean(dataSet.stats, function(d) { return d; });

                var container = d3.select(".lineChartContainer");
                var wrapper = d3.select(".lineChart");
                var scale = d3.select(".lineScaleSvg");
                var chart = d3.select(".lineChartSvg");
                var legend = d3.select(".lineLegendSvg");
                var filler = d3.select(".lineFillerSvg");

                var height = config.getChartHeight();
                var width = config.getChartWidth(graphData.cols.length);

                container
                .style("width", function(d)
                {
                    return (width + config.scaleWidth) + "em";
                });

                wrapper
                .style("height", function(d)
                {
                    return (height + config.legendHeight) + "em";
                });

                scale
                .style("width", function(d)
                {
                    return config.scaleWidth + "em";
                })
                .style("height", function(d)
                {
                    return height + "em";
                })
                .html("");
               
                chart
                .style("height", function(d)
                {
                    return height + "em";
                })
                .style("width", function(d)
                {
                    return width + "em";
                })
                .html("");

                legend
                .style("height", function(d)
                {
                    return config.legendHeight + "em";
                })
                .style("width", function(d)
                {
                    return width + "em";
                })
                .html("");

                filler
                .style("width", function(d)
                {
                    return config.scaleWidth + "em";
                })
                .style("height", function(d)
                {
                    return config.legendHeight + "em";
                });

                var scaleData = [];
                var unitGrouping = 2;
                if (config.max > 10) unitGrouping = 5;
                if (config.max > 20) unitGrouping = 10;

                for (var i = 0; i < (config.max * 1.5); i += unitGrouping)
                {
                    scaleData.push(i);
                }

                scale.selectAll("line")
                .data(scaleData)
                .enter()
                .append("line")
                .attr("class", function(d)
                {
                    return (d == 0 ? "" : "lineChartLine");
                })
                .attr("x1", function(d)
                {
                    return (config.scaleWidth - 1) + "em";
                })
                .attr("y1", function(d)
                {
                    return (height - (d * config.scale)) + "em";
                })
                .attr("x2", function(d)
                {
                    return config.scaleWidth + "em";
                })
                .attr("y2", function(d)
                {
                    return (height - (d * config.scale)) + "em";
                });

                scale.selectAll("text")
                .data(scaleData)
                .enter()
                .append("text")
                .attr("x", function(d)
                {
                    return (config.scaleWidth + 1) + "em";
                })
                .attr("y", function(d)
                {
                    return ((height * 2) - ((d * config.scale * 2) - 0.25)) + "em";
                })
                .text(function(d)
                {
                    return (d == 0 ? "" : d);
                });

                scale
                .append("line")
                .attr("class", "lineChartLine")
                .attr("x1", function(d)
                {
                    return config.scaleWidth + "em";
                })
                .attr("y1", function(d)
                {
                    return "0em";
                })
                .attr("x2", function(d)
                {
                    return config.scaleWidth + "em";
                })
                .attr("y2", function(d)
                {
                    return height + "em";
                });

                chart.selectAll("line")
                .data(scaleData)
                .enter()
                .append("line")
                .attr("class", "lineChartFadedLine")
                .attr("x1", function(d)
                {
                    return "0em";
                })
                .attr("y1", function(d)
                {
                    return (height - (d * config.scale)) + "em";
                })
                .attr("x2", function(d)
                {
                    return width + "em";
                })
                .attr("y2", function(d)
                {
                    return (height - (d * config.scale)) + "em";
                });

                chart.selectAll("rect")
                .data(dataSet.stats)
                .enter()
                .append("rect")
                .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";
                });

                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;
                });

                chart
                .append("line")
                .attr("class", "lineChartDataMean")
                .attr("x1", function(d)
                {
                    return "0em";
                })
                .attr("y1", function(d)
                {
                    return (height - (config.mean * config.scale)) + "em";
                })
                .attr("x2", function(d)
                {
                    return width + "em";
                })
                .attr("y2", function(d)
                {
                    return (height - (config.mean * config.scale)) + "em";
                });

                legend.selectAll("text")
                .data(dataSet.labels)
                .enter()
                .append("text")
                .attr("x", function(d, i)
                {
                    return ((i * (config.dataWidth + config.dataSpacing)) + 2 + (config.dataWidth / 2)) + "em";
                })
                .attr("y", function(d)
                {
                    return config.dataSpacing + "em";
                })
                .text(function(d)
                {
                    return d;
                });

                legend
                .append("line")
                .attr("class", "lineChartLine")
                .attr("x1", function(d)
                {
                    return "0em";
                })
                .attr("y1", function(d)
                {
                    return "0em";
                })
                .attr("x2", function(d)
                {
                    return width + "em";
                })
                .attr("y2", function(d)
                {
                    return "0em";
                });
            }
        };

        config.max = d3.max(graphData.cols, function(d)
        {
            var maxStat = d3.max(d.stats, function(x)
            {
                return (x.goals > x.appearances ? x.goals : x.appearances);
            }
            );

            return maxStat;
        }
        );

        var ddlYear = d3.select("#ddlYear");

        ddlYear.selectAll("option")
        .data(graphData.rows)
        .enter()
        .append("option")
        .property("selected", function(d, i)
        {
            return i == 0;
        })
        .attr("value", function(d)
        {
            return d;
        })
        .text(function(d)
        {
            return d;
        });

        var ddlStat = d3.select("#ddlStat");

        ddlStat.selectAll("option")
        .data(graphData.stats)
        .enter()
        .append("option")
        .property("selected", function(d, i)
        {
            return i == 0;
        })
        .attr("value", function(d)
        {
            return d;
        })
        .text(function(d)
        {
            return d;
        });

        config.setData();

        d3.select("#ddlYear").on("change", function() { config.setData(); });
        d3.select("#ddlStat").on("change", function() { config.setData(); });
        </script>
    </body>
</html>


So far so good? The code should still perform the same. You should still get a bar chart when you run it. What we'll do next, is change a drop-down list. You see, now we don't want to choose data by year. We want to choose data by player, and see the line chart track progress over the years. So rename the drop-down list below...
<div class="lineDashboard">
    <select id="ddlPlayer">

    </select>

    <select id="ddlStat">

    </select>
</div>


Then we're going to change this block of code near the bottom of the screen. All instances of "ddlYear" will be changed to "ddlPlayer".
var ddlPlayer = d3.select("#ddlPlayer");

ddlPlayer.selectAll("option")
.data(graphData.rows)
.enter()
.append("option")
.property("selected", function(d, i)
{
    return i == 0;
})
.attr("value", function(d)
{
    return d;
})
.text(function(d)
{
    return d;
});

var ddlStat = d3.select("#ddlStat");

ddlStat.selectAll("option")
.data(graphData.stats)
.enter()
.append("option")
.property("selected", function(d, i)
{
    return i == 0;
})
.attr("value", function(d)
{
    return d;
})
.text(function(d)
{
    return d;
});

config.setData();

d3.select("#ddlPlayer").on("change", function() { config.setData(); });
d3.select("#ddlStat").on("change", function() { config.setData(); });


This block needs to be further changed. In the data, use the cols array of the graphData object instead of rows. And instead of returning d for value and text, return the title property of d, which is the player's name.
ddlPlayer.selectAll("option")
.data(graphData.cols)
.enter()
.append("option")
.property("selected", function(d, i)
{
    return i == 0;
})
.attr("value", function(d)
{
    return d.title;
})
.text(function(d)
{
    return d.title;
});


You'll notice right away that your layout looks wrong. That's to be expected because we introduced a JavaScript error somewhere with all these changes. But fret not... our first drop-down list is duly changed. It now displays the list of player names!


In the setData() method, you need to select ddlPlayer instead of ddlYear. While you're at it, change the name of the variable from "year" to "player".
"setData": function ()
{
    var player = d3.select("#ddlPlayer").node().value;
    var stat = d3.select("#ddlStat").node().value;

    var dataSet =
    {
      "labels": [],
      "stats": []
    };

    for (var i = 0; i < graphData.cols.length; i++)
    {
        var filtered = graphData.cols[i].stats.filter(function(x) { return x.year == year;})

        if (filtered.length > 0)
        {
          dataSet.labels.push(graphData.cols[i].title);
          dataSet.stats.push(filtered[0][stat]);
        }
    }


This For loop determines the labels, if you recall. We'll be making substantial changes to it. First, remove that line, you won't need it.
for (var i = 0; i < graphData.cols.length; i++)
{
    //var filtered = graphData.cols[i].stats.filter(function(x) { return x.year == year;})

    if (filtered.length > 0)
    {
      dataSet.labels.push(graphData.cols[i].title);
      dataSet.stats.push(filtered[0][stat]);
    }
}


Instead, set filtered outside of the For loop. It's straightforward because you're simply getting the one element in the cols array whose title property corresponds to player.
var filtered = graphData.cols.filter(function(x) { return x.title == player;});

for (var i = 0; i < graphData.cols.length; i++)
{
    //var filtered = graphData.cols[i].stats.filter(function(x) { return x.year == year;})

    if (filtered.length > 0)
    {
      dataSet.labels.push(graphData.cols[i].title);
      dataSet.stats.push(filtered[0][stat]);
    }
}


Instead of iterating through the cols array, iterate through the stats array of the first (and only) element in filtered. You won't need the If block either, so get rid of it.
var filtered = graphData.cols.filter(function(x) { return x.title == player;});

for (var i = 0; i < filtered[0].stats.length; i++)
{
    //var filtered = graphData.cols[i].stats.filter(function(x) { return x.year == year;})

    //if (filtered.length > 0)
    //{
      dataSet.labels.push(graphData.cols[i].title);
      dataSet.stats.push(filtered[0][stat]);
    //}
}


Remember, we want to display year in the labels instead of player names, so change this accordingly.
//if (filtered.length > 0)
//{
  dataSet.labels.push(filtered[0].stats[i].year);
  dataSet.stats.push(filtered[0][stat]);
//}


And here, we'll still obtain the statistic, but instead take reference from the appropriate element in stats.
//if (filtered.length > 0)
//{
  dataSet.labels.push(filtered[0].stats[i].year);
  dataSet.stats.push(filtered[0].stats[i][stat]);
//}


Remove the commented out lines; it's neater.
var filtered = graphData.cols.filter(function(x) { return x.title == player;});

for (var i = 0; i < filtered[0].stats.length;  i++)
{
    dataSet.labels.push(filtered[0].stats[i].year);
    dataSet.stats.push(filtered[0].stats[i][stat]);
}


Your page should display fine now, but it's still displaying a bar chart. Still, the labels have changed!


Next

Now that it appears to be working, let's make it work correctly.

No comments:

Post a Comment