Making an interactive histogram in D3.js

I recently worked on some updates to the MPG tracking page I set up in January. One of my goals was to make the graphs on the page respond to mouseover events by displaying more data. Here’s some sample code for a simplified version of the histogram that now appears on the MPG page, the data is below the code. The code for this example and the entire MPG project is available on github. See also the interactive miles over time graph.

index.html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
  <body>
    <title>MPG Data</title>
    <style>
      .d3-tip {
        line-height: 1;
        font-weight: bold;
        padding: 12px;
        background: rgba(0, 0, 0, 0.8);
        color: #fff;
        border-radius: 2px;
      }
      .bar rect {
        fill: steelblue;
        shape-rendering: crispEdges;
      }
      .bar rect:hover{
        fill: rgba(0,0,0,.8);
      }
      .axis path, .axis line {
        fill: none;
        stroke: #000;
        shape-rendering: crispEdges;
      }
    </style>
    <script src="../../mpg/lib/d3.v3.min.js"></script>
    <!See https://github.com/Caged/d3-tip  (d3.tip.js is the index.js file)>
    <script src="../../mpg/lib/d3.tip.v0.6.3.js"></script>
    </script>
    <script>
// plot a histogram from mpg data in a .csv file
function parser(d) {
    d.pMPG = +d.MPG;
    return d;
}

function mpghist(csvdata) {
    var binsize = 2;
    var minbin = 36;
    var maxbin = 60;
    var numbins = (maxbin - minbin) / binsize;

    // whitespace on either side of the bars in units of MPG
    var binmargin = .2; 
    var margin = {top: 10, right: 30, bottom: 50, left: 60};
    var width = 450 - margin.left - margin.right;
    var height = 250 - margin.top - margin.bottom;

    // Set the limits of the x axis
    var xmin = minbin - 1
    var xmax = maxbin + 1

    histdata = new Array(numbins);
    for (var i = 0; i < numbins; i++) {
		histdata[i] = { numfill: 0, meta: "" };
	}

	// Fill histdata with y-axis values and meta data
    csvdata.forEach(function(d) {
		var bin = Math.floor((d.pMPG - minbin) / binsize);
		if ((bin.toString() != "NaN") && (bin < histdata.length)) {
			histdata[bin].numfill += 1;
			histdata[bin].meta += "<tr><td>" + d.City +
				" " + d.State + 
				"</td><td>" + 
				d.pMPG.toFixed(1) + " mpg</td></tr>";
		}
    });

    // This scale is for determining the widths of the histogram bars
    // Must start at 0 or else x(binsize a.k.a dx) will be negative
    var x = d3.scale.linear()
	  .domain([0, (xmax - xmin)])
	  .range([0, width]);

    // Scale for the placement of the bars
    var x2 = d3.scale.linear()
	  .domain([xmin, xmax])
	  .range([0, width]);
	
    var y = d3.scale.linear()
	  .domain([0, d3.max(histdata, function(d) { 
						return d.numfill; 
						})])
	  .range([height, 0]);

    var xAxis = d3.svg.axis()
	  .scale(x2)
	  .orient("bottom");
    var yAxis = d3.svg.axis()
	  .scale(y)
	  .ticks(8)
	  .orient("left");

    var tip = d3.tip()
	  .attr('class', 'd3-tip')
	  .direction('e')
	  .offset([0, 20])
	  .html(function(d) {
	    return '<table id="tiptable">' + d.meta + "</table>";
	});

    // put the graph in the "mpg" div
    var svg = d3.select("#mpg").append("svg")
	  .attr("width", width + margin.left + margin.right)
	  .attr("height", height + margin.top + margin.bottom)
	  .append("g")
	  .attr("transform", "translate(" + margin.left + "," + 
						margin.top + ")");

    svg.call(tip);

    // set up the bars
    var bar = svg.selectAll(".bar")
	  .data(histdata)
	  .enter().append("g")
	  .attr("class", "bar")
	  .attr("transform", function(d, i) { return "translate(" + 
	       x2(i * binsize + minbin) + "," + y(d.numfill) + ")"; })
	  .on('mouseover', tip.show)
	  .on('mouseout', tip.hide);

    // add rectangles of correct size at correct location
    bar.append("rect")
	  .attr("x", x(binmargin))
	  .attr("width", x(binsize - 2 * binmargin))
	  .attr("height", function(d) { return height - y(d.numfill); });

    // add the x axis and x-label
    svg.append("g")
	  .attr("class", "x axis")
	  .attr("transform", "translate(0," + height + ")")
	  .call(xAxis);
    svg.append("text")
	  .attr("class", "xlabel")
	  .attr("text-anchor", "middle")
	  .attr("x", width / 2)
	  .attr("y", height + margin.bottom)
	  .text("MPG");

    // add the y axis and y-label
    svg.append("g")
	  .attr("class", "y axis")
	  .attr("transform", "translate(0,0)")
	  .call(yAxis);
    svg.append("text")
	  .attr("class", "ylabel")
	  .attr("y", 0 - margin.left) // x and y switched due to rotation
	  .attr("x", 0 - (height / 2))
	  .attr("dy", "1em")
	  .attr("transform", "rotate(-90)")
	  .style("text-anchor", "middle")
	  .text("# of fill-ups");
}

// Read in .csv data and make graph
d3.csv("prius_gas.csv", parser,
       function(error, csvdata) {
	   mpghist(csvdata);
}); 
    </script>
    <div id="mpg" class="graph"></div>
  </body>
</html>

prius_gas.csv

City,State,MPG
Berkeley,CA,48.75
Tahoe City,CA,39.45
Meyers,CA,47.05
San Rafael,CA,53.09
Berkeley,CA,45.55
Tehachapi,CA,51.8
Hurricane,UT,52.54
Scipio,UT,49
Ely,NV,48.45
Colfax,CA,52.18
Vacaville,CA,50.7
Roseburg,OR,46.54
Crescent City,CA,47.59
Berkeley,CA,44.03
Berkeley,CA,52.81

D3-tip project: http://labratrevenge.com/d3-tip/