.. _step_14: Step 14: Map legend =================== .. comments In this step, we are going to create a legend for the colors on the map. .. contents:: Contents :depth: 2 :local: Legend ------ First, we are going to create a container in the HTML document for the legend. .. code-block:: html :caption: index.html - legend container :linenos: :lineno-start: 23 :emphasize-lines: 3
We add the legend SVG and define a function which will update the legend if the key of the map changes or if the window is resized. .. code-block:: js :caption: map.js - legend functions :linenos: :lineno-start: 67 :emphasize-lines: 3-113 // ... // We prepare a number format which will always return 2 decimal places. var formatNumber = d3.format('.2f'); // For the legend, we prepare a very simple linear scale. Domain and // range will be set later as they depend on the data currently shown. var legendX = d3.scale.linear(); // We use the scale to define an axis. The tickvalues will be set later // as they also depend on the data. var legendXAxis = d3.svg.axis() .scale(legendX) .orient("bottom") .tickSize(13) .tickFormat(function(d) { return formatNumber(d); }); // We create an SVG element in the legend container and give it some // dimensions. var legendSvg = d3.select('#legend').append('svg') .attr('width', '100%') .attr('height', '44'); // To this SVG element, we add a element which will hold all of our // legend entries. var g = legendSvg.append('g') .attr("class", "legend-key YlGnBu") .attr("transform", "translate(" + 20 + "," + 20 + ")"); // We add a element for each quantize category. The width and // color of the rectangles will be set later. g.selectAll("rect") .data(quantize.range().map(function(d) { return quantize.invertExtent(d); })) .enter().append("rect"); // We add a element acting as the caption of the legend. The text // will be set later. g.append("text") .attr("class", "caption") .attr("y", -6) /** * Function to update the legend. * Somewhat based on http://bl.ocks.org/mbostock/4573883 */ function updateLegend() { // We determine the width of the legend. It is based on the width of // the map minus some spacing left and right. var legendWidth = d3.select('#map').node().getBoundingClientRect().width - 50; // We determine the domain of the quantize scale which will be used as // tick values. We cannot directly use the scale via quantize.scale() // as this returns only the minimum and maximum values but we need all // the steps of the scale. The range() function returns all categories // and we need to map the category values (q0-9, ..., q8-9) to the // number values. To do this, we can use invertExtent(). var legendDomain = quantize.range().map(function(d) { var r = quantize.invertExtent(d); return r[1]; }); // Since we always only took the upper limit of the category, we also // need to add the lower limit of the very first category to the top // of the domain. legendDomain.unshift(quantize.domain()[0]); // On smaller screens, there is not enough room to show all 10 // category values. In this case, we add a filter leaving only every // third value of the domain. if (legendWidth < 400) { legendDomain = legendDomain.filter(function(d, i) { return i % 3 == 0; }); } // We set the domain and range for the x scale of the legend. The // domain is the same as for the quantize scale and the range takes up // all the space available to draw the legend. legendX .domain(quantize.domain()) .range([0, legendWidth]); // We update the rectangles by (re)defining their position and width // (both based on the legend scale) and setting the correct class. g.selectAll("rect") .data(quantize.range().map(function(d) { return quantize.invertExtent(d); })) .attr("height", 8) .attr("x", function(d) { return legendX(d[0]); }) .attr("width", function(d) { return legendX(d[1]) - legendX(d[0]); }) .attr('class', function(d, i) { return quantize.range()[i]; }); // We update the legend caption. To do this, we take the text of the // currently selected dropdown option. var keyDropdown = d3.select('#select-key').node(); var selectedOption = keyDropdown.options[keyDropdown.selectedIndex]; g.selectAll('text.caption') .text(selectedOption.text); // We set the calculated domain as tickValues for the legend axis. legendXAxis .tickValues(legendDomain) // We call the axis to draw the axis. g.call(legendXAxis); } // ... We call the function after the window has been resized and when the map colors have been updated. .. code-block:: js :caption: map.js - update legend on window resize :linenos: :lineno-start: 10 :emphasize-lines: 3-5 // ... // We add a listener to the browser window, calling updateLegend when // the window is resized. window.onresize = updateLegend; // ... .. code-block:: js :caption: map.js - update legend after map colors change :linenos: :lineno-start: 249 :emphasize-lines: 3-4 // ... // We call the function to update the legend. updateLegend(); // ... Lastly, we need some style for the legend container and the legend rectangles. .. code-block:: css :caption: style.css - legend style :linenos: :lineno-start: 64 :emphasize-lines: 3-19 /* ... */ #legend { border: 1px solid silver; border-top: 0; } .legend-key path { display: none; } .legend-key text { font-size: 1rem; } .legend-key line { stroke: #000; shape-rendering: crispEdges; } /* ... */ Code ---- * For reference, the file ``index.html`` after step 14: https://github.com/lvonlanthen/data-map-d3/blob/step-14/index.html * For reference, the file ``style.css`` after step 14: https://github.com/lvonlanthen/data-map-d3/blob/step-14/style.css * For reference, the file ``map.js`` after step 14: https://github.com/lvonlanthen/data-map-d3/blob/step-14/map.js * The diff view of step 13 and step 14: https://github.com/lvonlanthen/data-map-d3/compare/step-13...step-14?diff=split