Summary

D3.js is a powerful library that enables bespoke visualization on a very fine-grained level.


Advanced Solution



      // D3 Animated Scatter Plot

      // Section 1: Pre-Data Setup
      // ===========================
      // Before we code any data visualizations,
      // we need to at least set up the width, height and margins of the graph.
      // Note: I also added room for label text as well as text padding,
      // though not all graphs will need those specifications.
      
      // Grab the width of the containing box
      var width = parseInt(d3.select("#scatter").style("width"));
      
      // Designate the height of the graph
      var height = width - width / 3.9;
      
      // Margin spacing for graph
      var margin = 20;
      
      // space for placing words
      var labelArea = 110;
      
      // padding for the text at the bottom and left axes
      var tPadBot = 40;
      var tPadLeft = 40;
      
      // Create the actual canvas for the graph
      var svg = d3
        .select("#scatter")
        .append("svg")
        .attr("width", width)
        .attr("height", height)
        .attr("class", "chart");
      
      // Set the radius for each dot that will appear in the graph.
      // Note: Making this a function allows us to easily call
      // it in the mobility section of our code.
      var circRadius;
      function crGet() {
        if (width <= 530) {
          circRadius = 5;
        }
        else {
          circRadius = 10;
        }
      }
      crGet();
      
      // The Labels for our Axes
      
      // A) Bottom Axis
      // ==============
      
      // We create a group element to nest our bottom axes labels.
      svg.append("g").attr("class", "xText");
      // xText will allows us to select the group without excess code.
      var xText = d3.select(".xText");
      
      // We give xText a transform property that places it at the bottom of the chart.
      // By nesting this attribute in a function, we can easily change the location of the label group
      // whenever the width of the window changes.
      function xTextRefresh() {
        xText.attr(
          "transform",
          "translate(" +
            ((width - labelArea) / 2 + labelArea) +
            ", " +
            (height - margin - tPadBot) +
            ")"
        );
      }
      xTextRefresh();
      
      // Now we use xText to append three text SVG files, with y coordinates specified to space out the values.
      // 1. Poverty
      xText
        .append("text")
        .attr("y", -26)
        .attr("data-name", "poverty")
        .attr("data-axis", "x")
        .attr("class", "aText active x")
        .text("In Poverty (%)");
      // 2. Age
      xText
        .append("text")
        .attr("y", 0)
        .attr("data-name", "age")
        .attr("data-axis", "x")
        .attr("class", "aText inactive x")
        .text("Age (Median)");
      // 3. Income
      xText
        .append("text")
        .attr("y", 26)
        .attr("data-name", "income")
        .attr("data-axis", "x")
        .attr("class", "aText inactive x")
        .text("Household Income (Median)");
      
      // B) Left Axis
      // ============
      
      // Specifying the variables like this allows us to make our transform attributes more readable.
      var leftTextX = margin + tPadLeft;
      var leftTextY = (height + labelArea) / 2 - labelArea;
      
      // We add a second label group, this time for the axis left of the chart.
      svg.append("g").attr("class", "yText");
      
      // yText will allows us to select the group without excess code.
      var yText = d3.select(".yText");
      
      // Like before, we nest the group's transform attr in a function
      // to make changing it on window change an easy operation.
      function yTextRefresh() {
        yText.attr(
          "transform",
          "translate(" + leftTextX + ", " + leftTextY + ")rotate(-90)"
        );
      }
      yTextRefresh();
      
      // Now we append the text.
      // 1. Obesity
      yText
        .append("text")
        .attr("y", -26)
        .attr("data-name", "obesity")
        .attr("data-axis", "y")
        .attr("class", "aText active y")
        .text("Obese (%)");
      
      // 2. Smokes
      yText
        .append("text")
        .attr("x", 0)
        .attr("data-name", "smokes")
        .attr("data-axis", "y")
        .attr("class", "aText inactive y")
        .text("Smokes (%)");
      
      // 3. Lacks Healthcare
      yText
        .append("text")
        .attr("y", 26)
        .attr("data-name", "healthcare")
        .attr("data-axis", "y")
        .attr("class", "aText inactive y")
        .text("Lacks Healthcare (%)");
      
      // 2. Import our .csv file.
      // ========================
      // This data file includes state-by-state demographic data from the US Census
      // and measurements from health risks obtained
      // by the Behavioral Risk Factor Surveillance System.
      
      // Import our CSV data with d3's .csv import method.
      d3.csv("assets/data/data.csv").then(function(data) {
        // Visualize the data
        visualize(data);
      });
      
      // 3. Create our visualization function
      // ====================================
      // We called a "visualize" function on the data obtained with d3's .csv method.
      // This function handles the visual manipulation of all elements dependent on the data.
      function visualize(theData) {
        // PART 1: Essential Local Variables and Functions
        // =================================
        // curX and curY will determine what data gets represented in each axis.
        // We designate our defaults here, which carry the same names
        // as the headings in their matching .csv data file.
        var curX = "poverty";
        var curY = "obesity";
      
        // We also save empty variables for our the min and max values of x and y.
        // this will allow us to alter the values in functions and remove repetitious code.
        var xMin;
        var xMax;
        var yMin;
        var yMax;
      
        // This function allows us to set up tooltip rules (see d3-tip.js).
        var toolTip = d3
          .tip()
          .attr("class", "d3-tip")
          .offset([40, -60])
          .html(function(d) {
            // x key
            var theX;
            // Grab the state name.
            var theState = "
" + d.state + "
"; // Snatch the y value's key and value. var theY = "
" + curY + ": " + d[curY] + "%
"; // If the x key is poverty if (curX === "poverty") { // Grab the x key and a version of the value formatted to show percentage theX = "
" + curX + ": " + d[curX] + "%
"; } else { // Otherwise // Grab the x key and a version of the value formatted to include commas after every third digit. theX = "
" + curX + ": " + parseFloat(d[curX]).toLocaleString("en") + "
"; } // Display what we capture. return theState + theX + theY; }); // Call the toolTip function. svg.call(toolTip); // PART 2: D.R.Y! // ============== // These functions remove some repitition from later code. // This will be more obvious in parts 3 and 4. // a. change the min and max for x function xMinMax() { // min will grab the smallest datum from the selected column. xMin = d3.min(theData, function(d) { return parseFloat(d[curX]) * 0.90; }); // .max will grab the largest datum from the selected column. xMax = d3.max(theData, function(d) { return parseFloat(d[curX]) * 1.10; }); } // b. change the min and max for y function yMinMax() { // min will grab the smallest datum from the selected column. yMin = d3.min(theData, function(d) { return parseFloat(d[curY]) * 0.90; }); // .max will grab the largest datum from the selected column. yMax = d3.max(theData, function(d) { return parseFloat(d[curY]) * 1.10; }); } // c. change the classes (and appearance) of label text when clicked. function labelChange(axis, clickedText) { // Switch the currently active to inactive. d3 .selectAll(".aText") .filter("." + axis) .filter(".active") .classed("active", false) .classed("inactive", true); // Switch the text just clicked to active. clickedText.classed("inactive", false).classed("active", true); } // Part 3: Instantiate the Scatter Plot // ==================================== // This will add the first placement of our data and axes to the scatter plot. // First grab the min and max values of x and y. xMinMax(); yMinMax(); // With the min and max values now defined, we can create our scales. // Notice in the range method how we include the margin and word area. // This tells d3 to place our circles in an area starting after the margin and word area. var xScale = d3 .scaleLinear() .domain([xMin, xMax]) .range([margin + labelArea, width - margin]); var yScale = d3 .scaleLinear() .domain([yMin, yMax]) // Height is inverses due to how d3 calc's y-axis placement .range([height - margin - labelArea, margin]); // We pass the scales into the axis methods to create the axes. // Note: D3 4.0 made this a lot less cumbersome then before. Kudos to mbostock. var xAxis = d3.axisBottom(xScale); var yAxis = d3.axisLeft(yScale); // Determine x and y tick counts. // Note: Saved as a function for easy mobile updates. function tickCount() { if (width <= 500) { xAxis.ticks(5); yAxis.ticks(5); } else { xAxis.ticks(10); yAxis.ticks(10); } } tickCount(); // We append the axes in group elements. By calling them, we include // all of the numbers, borders and ticks. // The transform attribute specifies where to place the axes. svg .append("g") .call(xAxis) .attr("class", "xAxis") .attr("transform", "translate(0," + (height - margin - labelArea) + ")"); svg .append("g") .call(yAxis) .attr("class", "yAxis") .attr("transform", "translate(" + (margin + labelArea) + ", 0)"); // Now let's make a grouping for our dots and their labels. var theCircles = svg.selectAll("g theCircles").data(theData).enter(); // We append the circles for each row of data (or each state, in this case). theCircles .append("circle") // These attr's specify location, size and class. .attr("cx", function(d) { return xScale(d[curX]); }) .attr("cy", function(d) { return yScale(d[curY]); }) .attr("r", circRadius) .attr("class", function(d) { return "stateCircle " + d.abbr; }) // Hover rules .on("mouseover", function(d) { // Show the tooltip toolTip.show(d, this); // Highlight the state circle's border d3.select(this).style("stroke", "#323232"); }) .on("mouseout", function(d) { // Remove the tooltip toolTip.hide(d); // Remove highlight d3.select(this).style("stroke", "#e3e3e3"); }); // With the circles on our graph, we need matching labels. // Let's grab the state abbreviations from our data // and place them in the center of our dots. theCircles .append("text") // We return the abbreviation to .text, which makes the text the abbreviation. .text(function(d) { return d.abbr; }) // Now place the text using our scale. .attr("dx", function(d) { return xScale(d[curX]); }) .attr("dy", function(d) { // When the size of the text is the radius, // adding a third of the radius to the height // pushes it into the middle of the circle. return yScale(d[curY]) + circRadius / 2.5; }) .attr("font-size", circRadius) .attr("class", "stateText") // Hover Rules .on("mouseover", function(d) { // Show the tooltip toolTip.show(d); // Highlight the state circle's border d3.select("." + d.abbr).style("stroke", "#323232"); }) .on("mouseout", function(d) { // Remove tooltip toolTip.hide(d); // Remove highlight d3.select("." + d.abbr).style("stroke", "#e3e3e3"); }); // Part 4: Make the Graph Dynamic // ========================== // This section will allow the user to click on any label // and display the data it references. // Select all axis text and add this d3 click event. d3.selectAll(".aText").on("click", function() { // Make sure we save a selection of the clicked text, // so we can reference it without typing out the invoker each time. var self = d3.select(this); // We only want to run this on inactive labels. // It's a waste of the processor to execute the function // if the data is already displayed on the graph. if (self.classed("inactive")) { // Grab the name and axis saved in label. var axis = self.attr("data-axis"); var name = self.attr("data-name"); // When x is the saved axis, execute this: if (axis === "x") { // Make curX the same as the data name. curX = name; // Change the min and max of the x-axis xMinMax(); // Update the domain of x. xScale.domain([xMin, xMax]); // Now use a transition when we update the xAxis. svg.select(".xAxis").transition().duration(300).call(xAxis); // With the axis changed, let's update the location of the state circles. d3.selectAll("circle").each(function() { // Each state circle gets a transition for it's new attribute. // This will lend the circle a motion tween // from it's original spot to the new location. d3 .select(this) .transition() .attr("cx", function(d) { return xScale(d[curX]); }) .duration(300); }); // We need change the location of the state texts, too. d3.selectAll(".stateText").each(function() { // We give each state text the same motion tween as the matching circle. d3 .select(this) .transition() .attr("dx", function(d) { return xScale(d[curX]); }) .duration(300); }); // Finally, change the classes of the last active label and the clicked label. labelChange(axis, self); } else { // When y is the saved axis, execute this: // Make curY the same as the data name. curY = name; // Change the min and max of the y-axis. yMinMax(); // Update the domain of y. yScale.domain([yMin, yMax]); // Update Y Axis svg.select(".yAxis").transition().duration(300).call(yAxis); // With the axis changed, let's update the location of the state circles. d3.selectAll("circle").each(function() { // Each state circle gets a transition for it's new attribute. // This will lend the circle a motion tween // from it's original spot to the new location. d3 .select(this) .transition() .attr("cy", function(d) { return yScale(d[curY]); }) .duration(300); }); // We need change the location of the state texts, too. d3.selectAll(".stateText").each(function() { // We give each state text the same motion tween as the matching circle. d3 .select(this) .transition() .attr("dy", function(d) { return yScale(d[curY]) + circRadius / 3; }) .duration(300); }); // Finally, change the classes of the last active label and the clicked label. labelChange(axis, self); } } }); // Part 5: Mobile Responsive // ========================= // With d3, we can call a resize function whenever the window dimensions change. // This make's it possible to add true mobile-responsiveness to our charts. d3.select(window).on("resize", resize); // One caveat: we need to specify what specific parts of the chart need size and position changes. function resize() { // Redefine the width, height and leftTextY (the three variables dependent on the width of the window). width = parseInt(d3.select("#scatter").style("width")); height = width - width / 3.9; leftTextY = (height + labelArea) / 2 - labelArea; // Apply the width and height to the svg canvas. svg.attr("width", width).attr("height", height); // Change the xScale and yScale ranges xScale.range([margin + labelArea, width - margin]); yScale.range([height - margin - labelArea, margin]); // With the scales changes, update the axes (and the height of the x-axis) svg .select(".xAxis") .call(xAxis) .attr("transform", "translate(0," + (height - margin - labelArea) + ")"); svg.select(".yAxis").call(yAxis); // Update the ticks on each axis. tickCount(); // Update the labels. xTextRefresh(); yTextRefresh(); // Update the radius of each dot. crGet(); // With the axis changed, let's update the location and radius of the state circles. d3 .selectAll("circle") .attr("cy", function(d) { return yScale(d[curY]); }) .attr("cx", function(d) { return xScale(d[curX]); }) .attr("r", function() { return circRadius; }); // We need change the location and size of the state texts, too. d3 .selectAll(".stateText") .attr("dy", function(d) { return yScale(d[curY]) + circRadius / 3; }) .attr("dx", function(d) { return xScale(d[curX]); }) .attr("r", circRadius / 3); } }

Basic Solution



      // First, we want to get the width of the id location

      var width = parseInt(d3.select("#scatter").style("width"));
      var height = width - width / 4;
      var margin = 20;
      var labelArea = 110;
      var tPadBot = 40;
      var tPadLeft = 40;
      
      var svg = d3
        .select("#scatter")
        .append("svg")
        .attr("width", width)
        .attr("height", height)
        .attr("class", "chart");
      
      var circRadius;
      function crGet() {
        if (width <= 530) {
          circRadius = 5;
        }
        else {
          circRadius = 10;
        }
      }
      crGet();
      
      svg.append("g").attr("class","xText");
      var xText = d3.select(".xText");
      
      xText.attr(
        "transform",
        "translate(" +
          ((width - labelArea) / 2 + labelArea) +
          ", " +
          (height - margin - tPadBot) +
          ")"
        );
      
      xText
        .append("text")
        .attr("y", -26)
        .attr("data-name", "poverty")
        .attr("data-axis", "x")
        .attr("class", "aText active x")
        .text("In Poverty (%)");
      
      var leftTextX = margin + tPadLeft;
      var leftTextY = (height + labelArea) / 2 - labelArea;
      
      svg.append("g").attr("class", "yText");
      var yText = d3.select(".yText");
      
      yText.attr(
        "transform",
        "translate(" + leftTextX + ", " + leftTextY + ")rotate(-90)"
      );
      
      yText
        .append("text")
        .attr("y", 26)
        .attr("data-name", "heathcare")
        .attr("data-axis", "y")
        .attr("class", "aText active y")
        .text("Lacks Healthcare (%)");
      
      d3.csv("assets/data/data.csv").then(function(data) {
        visualize(data);
      });
      
        function visualize(theData) {
          var curX = "poverty";
          var curY = "healthcare";
          var xMin;
          var xMax;
          var yMin;
          var yMax;
      
          function xMinMax() {
            xMin = d3.min(theData, function(d) {
              return parseFloat(d[curX]) * 0.90;
            });
            xMax = d3.max(theData, function(d) {
              return parseFloat(d[curX]) * 1.10;
            });
          }
          function yMinMax() {
            yMin = d3.min(theData, function(d) {
              return parseFloat(d[curY]) * 0.90;
            });
            yMax = d3.max(theData, function(d) {
              return parseFloat(d[curY]) * 1.10;
            });
          }
      
      
          xMinMax();
          yMinMax();
      
          var xScale = d3
            .scaleLinear()
            .domain([xMin, xMax])
            .range([margin + labelArea, width - margin]);
          var yScale = d3
            .scaleLinear()
            .domain([yMin, yMax])
            .range([height - margin - labelArea, margin]);
      
            var xAxis = d3.axisBottom(xScale);
            var yAxis = d3.axisLeft(yScale);
      
            svg
              .append("g")
              .call(xAxis)
              .attr("class", "xAxis")
              .attr("transform", "translate(0," + (height - margin - labelArea) + ")");
            svg
              .append("g")
              .call(yAxis)
              .attr("class", "xAxis")
              .attr("transform", "translate(" + (margin + labelArea) + ", 0)");
            
            var theCircles = svg.selectAll("g theCircles").data(theData).enter();
            
            var toolTip = d3
            .tip()
            .attr("class", "d3-tip")
            .offset([40, -60])
            .html(function(d) {
              // x key
              var theX;
              // Grab the state name.
              var theState = "
" + d.state + "
"; // Snatch the y value's key and value. var theY = "
" + curY + ": " + d[curY] + "%
"; // If the x key is poverty theX = "
" + curX + ": " + d[curX] + "%
"; // Display what we capture. return theState + theX + theY; }); // Call the toolTip function. svg.call(toolTip); theCircles .append("circle") .attr("cx", function(d) { return xScale(d["poverty"]); }) .attr("cy", function(d){ return yScale(d["healthcare"]); }) .attr("r", 10).attr("fill", "#add8e6") .attr("class", function(d){ return "stateCircle" + d.abbr; }) .on("mouseover", function(d) { toolTip.show(d,this); d3.select(this).style("stroke", "#323232"); }) .on("mouseout", function(d) { toolTip.hide(d); d3.select(this).style("stroke", "#e3e3e3"); }); theCircles .append("text") .text(function(d) { return d.abbr; }) .attr("dx", function(d) { return xScale(d[curX]); }) .attr("dy", function(d) { return yScale(d[curY]) + 10 / 2.5; }) .attr("font-size", 10) .attr("class", "stateText") .on("mouseover", function(d) { toolTip.show(d); d3.select("." + d.abbr).style("stroke", "#e3e3e3"); }); }