D3.js Multiple Line Plot – Part 4

Working version of the plot, download here.

This post discusses the code responsible for creating an interactive legend for a multiple line plot — as seen on the Plot page of The Bubble Index. The legend will be located below the plot. In order to create an area to contain room for the legend’s text, the plot display must not fill the entire height of the svg object. As seen in part 1, the plot has a bottom margin of size 100 and a top margin of size 20. This allows space for a legend to span the area below the plot.

var maxPerRow = Math.ceil(indices.length / 2);
var legendSpace = width / maxPerRow;

The code assumes that two rows will be enough to display all the time-series labels, including an “Add All” and “Remove All” button.

The “Add All” feature will add every time-series contained in the original data to the plot. It is like a plot “reset.” Whatever is currently displayed will be removed. Thus, the y-axis will need to be re-scaled to fit the original range of data.

svg.append("text")
    .attr("x", 0)
    .attr("y", height + (margin.bottom / 2) + 15)
    .attr("class", "legend addAll")
    .style("fill", "red")
    .text("Add All")
    .on("click", function() {
        d3.selectAll(".index").data().forEach(function(d) {
            removeWindow(d.name, false, false);
        });
        y.domain([
            d3.min(indices, function(c) {
                return d3.min(c.values, function(v) {
                    return v.yvalue;
                });
            }),
            d3.max(indices, function(c) {
                return d3.max(c.values, function(v) {
                    return v.yvalue;
                });
            })
        ]);
        svg.select(".y.axis")
            .call(yAxis);
        indices.forEach(function(d, i) {
            var myID = "legend" + d.name;
            var selection = d3.select("#" + myID);
            if (selection.attr("class") == "legend-clicked") {
                selection.style("fill", color(d.name)).attr("class", "legend " + d.name);
                addWindow(d.name, true, false, true);
            }
        });
        addMouse();
    });

The “Remove All” feature will remove all time-series currently displayed in the plot. Note: while writing this, I noticed it may not be necessary to re-scale the y-axis.

svg.append("text")
    .attr("x", 0)
    .attr("y", height + (margin.bottom / 2) + 45)
    .attr("class", "legend removeAll")
    .style("fill", "red")
    .text("Remove All")
    .on("click", function() {
        indices.forEach(function(d, i) {
            var myID = "legend" + d.name;
            var selection = d3.select("#" + myID);
            selection.style("fill", "black").attr("class", "legend-clicked");
            removeWindow(d.name, true, false);
        });
        var displayedIndices = d3.selectAll(".index").data();
        y.domain([
            d3.min(displayedIndices, function(c) {
                return d3.min(c.values, function(v) {
                    return v.yvalue;
                });
            }),
            d3.max(displayedIndices, function(c) {
                return d3.max(c.values, function(v) {
                    return v.yvalue;
                });
            })
        ]);
        svg.select(".y.axis")
            .call(yAxis);
    });

At this point in the code, the function which creates a mouse-over interaction is called. The mouse-over function will be discussed in a later post.

addMouse();

Next, a legend entry for each time-series is appended to the bottom of the svg plot. Note: In the HTML header, there is a CSS style sheet which contains a class for a legend element which has been clicked-on.

.legend {
    font-size: 16px;
    font-weight: bold;
    text-anchor: middle;
    cursor:pointer;
} 

.legend-clicked {
    font-size: 16px;
    font-weight: bold;
    text-anchor: middle;
    text-decoration: line-through;
    cursor:pointer;
}

When the user clicks on a time-series label, that particular time-series will either be removed or added, depending on the current status of that time-series. For instance, if the “52 Day” label is black and has a strike-through style, it means the time-series has been removed from the plot. Clicking on this label will add the time-series back to the plot. However, in order for this to happen, the y-axis needs to be re-scaled and all the currently displayed lines will need to be re-drawn. The functions which re-scale and update the plot will be included in a future post.

indices.forEach(function(d, i) {
    svg.append("text")
        .attr("x", legendSpace + (i % maxPerRow) * legendSpace)
        .attr("y", (i & lt; maxPerRow) ? (height + (margin.bottom / 2) + 15) : (height + (margin.bottom / 2) + 45))
        .attr("class", "legend " + d.name)
        .attr("id", "legend" + d.name)
        .style("fill", color(d.name))
        .text(d.name + " Days")
        .on("click", function() {
            var selection = d3.select(this);
            if (selection.attr("class") == "legend-clicked") {
                selection.style("fill", color(d.name)).attr("class", "legend " + d.name);
                addWindow(d.name, false, false, true);
                addMouse();
            } else {
                selection.style("fill", "black").attr("class", "legend-clicked");
                removeWindow(d.name, true, true);
            }
        });
});