text: d3.chart = d3.chart || {};
  
  /**
   * Heatmap chart for d3.js
   *
   * Usage:
   * var chart = d3.chart.heatmap();
   * d3.select('#chart')
   *   .datum(data)
   *   .call(chart);
   */
  
  d3.chart.heatmap = function(options) {
    function createHeatmap(selection, columnHeaders) {
      selection.each(function(data) {
  
        var rowSortOrder = false;
        var colSortOrder = false;
  
        var margin = {top: 10, right: 10, bottom: 10, left: 10};  // NOTE: Heatmap position will be adjusted to fit the labels
  
        var legendMargin = {top: 10, right: 10, bottom: 10, left: 10};
        var legendHeight = 30;
        var legendWidth = 200;
  
        //==================================================
        // Note: If the first column header is empty then assume the first column
        //       is row labels. Not the best method ever but will work for now.
        var useRowLabels = (columnHeaders[0] == "");
  
        var dataset = [];
        var rowHeaders = [];
  
        // Parse data
        data.forEach(function(d, i) {
          dataset[i] = [];
          columnHeaders.forEach(function(column) {
            var val;
            if (useRowLabels && column == "") {
              val = d[column];
              rowHeaders.push(val);
            }
            else {
              // Convert all values from strings to numbers...
              val = (d[column] == "") ? NaN : +d[column];
              dataset[i].push(val);
            }
          });
        });
  
        //==================================================
  
        var numberOfColumns = columnHeaders.length;
        var numberOfRows = dataset.length;
  
        if (useRowLabels) {
          // Note: array.shift() removes and returns the first element of an array
          columnHeaders.shift();
        }
        else {
          rowHeaders = d3.range(numberOfRows);
        }
  
        // Find min and max
        var min = d3.min(dataset, function(d) {
          return d3.min(d);
        });
        var max = d3.max(dataset, function(d) {
          return d3.max(d);
        });
  
        //==================================================
  
        // Create buttons
        // TODO: Add a margin and spacing
        var buttons = selection.append("div");
        var sortingOptions = {columnsAndRows: "Initial order on columns and rows", rows: "Initial order on rows", columns: "Initial order on columns"};
  
        buttons.append("button")
          .text(function(d) { return sortingOptions.columnsAndRows; })
          .attr("class", "columnsAndRowsButton")
          .on("click", function() { return changeOrder("columnsAndRows"); });
  
        buttons.append("button")
          .text(function(d) { return sortingOptions.rows; })
          .attr("class", "columnsAndRowsButton")
          .on("click", function() { return changeOrder("rows"); });
  
        buttons.append("button")
          .text(function(d) { return sortingOptions.columns; })
          .attr("class", "columnsAndRowsButton")
          .on("click", function() { return changeOrder("columns"); });
  
        //==================================================
  
        var svg = selection.append("svg");
  
        margin.left += getMaxWidth(rowHeaders);
  
        // Use the number of cells to determine the size and spacing
        var n = Math.max(numberOfColumns, numberOfRows);
        var cellSize = Math.floor(1600 / n);      // TODO: Should cells be square?
        if (cellSize < 10) {
          cellSize = 10;
        }
        else if (cellSize > 25) {
          cellSize = 25;
        }
  
        var width = cellSize * numberOfColumns;
        var height = cellSize * numberOfRows;
  
        var columnLabelSize = getMaxWidth(columnHeaders);
        if (columnLabelSize < 30) { columnLabelSize = 30; }
        var heatmapY = margin.top + legendHeight + legendMargin.top + legendMargin.bottom + columnLabelSize;
  
        svg.attr("width", width + margin.left + margin.right)
          .attr("height", height + heatmapY)
          .attr("transform", "translate(" + 0 + "," + margin.top + ")");
  
        // TODO: If the heatmap height is not set here, then it gets smaller (?) as
        //       the number of columns (patients) increases ... The width seems to be okay.
        d3.select("#heatmap").style("height", (height + heatmapY + 100) + "px");
  
        //==================================================
  
        createLegend(svg);
  
        svg = svg.append("g")
          .attr("transform", "translate(" + margin.left  + "," + heatmapY + ")");
  
        // Create a color scale
        var colors = ["#0000FF", "#FFFF00"] // blue ---> yellow
        var colorScale = d3.scale.linear()
          .domain([min, max])
          .range(colors);
  
        //==================================================
  
        // Create heatmap row-by-row
        var row = svg.selectAll(".row")
          .data(dataset)
          .enter().append("g")
          .attr("class", "row");
  
        var rowNumber = 0;
        var cells = row.selectAll(".cells")
          .data (function (d) {
            rowNumber++;
            return d;
          })
          .enter()
          .append("rect")
          .attr("x", function(d, columnNumber) {
            return columnNumber * cellSize;
          })
          .attr("y", function(d, columnNumber, rowNumber) {
            return rowNumber * cellSize;
          })
          .attr("class", function(d, columnNumber, rowNumber) {
            return "cell bordered cr" + rowNumber + " cc" + columnNumber;
          })
          .attr("row", function(d, columnNumber, rowNumber) {
            return rowNumber;
          })
          .attr("col", function(d, columnNumber, rowNumber) {
            return columnNumber;
          })
          .attr("width", cellSize)
          .attr("height", cellSize)
          .attr("fill", function(d) {
            return isNaN(d) ? "white" : colorScale(d);
          })
          .on('mouseover', function(d, columnNumber, rowNumber) {
            d3.select('#colLabel_' + columnNumber).classed("hover", true);
            d3.select('#rowLabel_' + rowNumber).classed("hover", true);
          })
          .on('mouseout', function(d, columnNumber, rowNumber) {
            d3.select('#colLabel_' + columnNumber).classed("hover", false);
            d3.select('#rowLabel_' + rowNumber).classed("hover", false);
          })
          .append("title")  // Tooltip
          .text(function(d) {
            return d;
          });
  
        //==================================================
  
        // Labels
        var rowLabels = svg.append("g")
          .attr("class", "rowLabels")
          .selectAll(".rowLabel")
          .data(rowHeaders)
          .enter().append("text")
          .text(function(d) {
            return d;
          })
          .attr("x", 0)
          .attr("y", function(d, i) {
            return (i * cellSize);
          })
          .style("text-anchor", "end")
          .attr("transform", function(d, i) {
              return "translate(-3," + cellSize / 1.5 + ")";
          })
          .attr("class", "rowLabel mono")
          .attr("id", function(d, i) {
            return "rowLabel_" + i;
          })
          .on('mouseover', function(d, i) {
            d3.select('#rowLabel_' + i).classed("hover", true);
          })
          .on('mouseout', function(d, i) {
            d3.select('#rowLabel_' + i).classed("hover", false);
          })
          .on("click", function(d, i) {
            rowSortOrder = !rowSortOrder;
            sortByValues("r", i, rowSortOrder);
            d3.select("#order").property("selectedIndex", 0);
          });
  
        var colLabels = svg.append("g")
          .attr("class", "colLabels")
          .selectAll(".colLabel")
          .data(columnHeaders)
          .enter().append("text")
          .text(function(d) { return d; })
          .attr("x", 0)
          .attr("y", function(d, i) {
            return (i * cellSize);
          })
          .style("text-anchor", "left")
          .attr("transform", function(d, i) {
            return "translate(" + cellSize / 2 + ", -3) rotate(-90) rotate(45, 0, " + (i * cellSize) + ")";
          })
          .attr("class", "colLabel mono")
          .attr("id", function(d, i) {
            return "colLabel_" + i;
          })
          .on('mouseover', function(d, i) {
            d3.select('#colLabel_' + i).classed("hover", true);
          })
          .on('mouseout', function(d, i) {
            d3.select('#colLabel_' + i).classed("hover", false);
          })
          .on("click", function(d, i) {
            colSortOrder = !colSortOrder;
            sortByValues("c", i, colSortOrder);
            d3.select("#order").property("selectedIndex", 0);
          });
  
        function getMaxWidth(labels) {
          var maxWidth = 0;
          svg.selectAll("text.foo").data(labels)
            .enter().append("text")
            .attr("class", "rowLabel mono")
            .text(function(d) { return d; })
            .each(function(d) {
                maxWidth = Math.max(this.getBBox().width, maxWidth);
            })
            .remove();
          return maxWidth;
        }
        //==================================================
  
        function createLegend(svg) {
          var svg = svg.append("g");
  
          var gradient = svg.append("svg:defs")
            .append("svg:linearGradient")
            .attr("id", "gradient")
            .attr("x1", "0%")
            .attr("y1", "0%")
            .attr("x2", "100%")
            .attr("y2", "0%")
            .attr("spreadMethod", "pad");
  
          gradient.append("svg:stop")
            .attr("offset", "0%")
            .attr("stop-color", "#0000FF")  // blue
            .attr("stop-opacity", 1);
  
          gradient.append("svg:stop")
            .attr("offset", "100%")
            .attr("stop-color", "#FFFF00")  // yellow
            .attr("stop-opacity", 1);
  
          svg.append("rect")
            .attr("class", "legend")
            .attr("width", legendWidth)
            .attr("height", legendHeight)
            .style("fill", "url(#gradient)")
            .attr("stroke-width", 1)
            .attr("stroke", "black");
  
          var legendScale = d3.scale.linear()
            .domain([min, max])
            .range([0, legendWidth]);
  
          var tickmarks = d3.svg.axis()
            .scale(legendScale)
            .orient("bottom")
            .ticks(5);
  
          var xAxis = svg.append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(" + "0" + "," + legendHeight + ")")
            .call(tickmarks);
  
          var bbox = xAxis.node().getBBox();  // Get x-axis dimensions
  
          svg.append("text")
            .attr("class", "legend label")
            .attr("text-anchor", "middle")
            .style("font-size", "16px")
            .attr("x", legendWidth / 2)
            .attr("y", legendHeight + bbox.height)
            .attr("dy", "1.0em")  // Allow small margin between text and x-axis
            .text(""); // TODO: Read this from the input file?
        }
  
        //==================================================
  
        // Change ordering of cells
        function sortByValues(rORc, i, sortOrder) {
          var t = svg.transition().duration(1000);
          var values = [];
          var sorted;
          d3.selectAll(".c" + rORc + i)
            .filter(function(d) {
              values.push(d); // TODO: Handle NaN or other invalid values?
            });
  
          if (rORc == "r") { // sort on columns
            sorted = d3.range(numberOfColumns).sort(function(a, b) {
              if (sortOrder) {
                return values[b] - values[a];
              } else {
                return values[a] - values[b];
              }
            });
            t.selectAll(".cell")
              .attr("x", function(d) {
                var col = parseInt(d3.select(this).attr("col"));
                return sorted.indexOf(col) * cellSize;
              });
            t.selectAll(".colLabel")
              .attr("y", function(d, i) {
                return sorted.indexOf(i) * cellSize;
              })
              .attr("transform", function(d, i) {
                return "translate(" + cellSize / 2 + ", -3) rotate(-90) rotate(45, 0, " + (sorted.indexOf(i) * cellSize) + ")";
              });
          } else { // sort on rows
            sorted = d3.range(numberOfRows).sort(function(a, b) {
              if (sortOrder) {
                return values[b] - values[a];
              } else {
                return values[a] - values[b];
              }
            });
            t.selectAll(".cell")
              .attr("y", function(d) {
                var row = parseInt(d3.select(this).attr("row"));
                return sorted.indexOf(row) * cellSize;
              });
            t.selectAll(".rowLabel")
              .attr("y", function(d, i) {
                return sorted.indexOf(i) * cellSize;
              })
              .attr("transform", function(d, i) {
                return "translate(-3," + cellSize / 1.5 + ")";
              });
          }
        }
  
        //==================================================
  
        function changeOrder(newOrder) {
          var t = svg.transition().duration(1000);
          if (newOrder == "columns") { // initial sort on columns
            sortColumns();
          } else if (newOrder == "rows") { // initial sort on rows
            sortRows();
          } else if (newOrder == "columnsAndRows") { // initial sort on rows and columns
            t.selectAll(".cell")
              .attr("x", function(d) {
                var col = parseInt(d3.select(this).attr("col"));
                return col * cellSize;
              })
              .attr("y", function(d) {
                var row = parseInt(d3.select(this).attr("row"));
                return row * cellSize;
              });
            t.selectAll(".colLabel")
              .attr("y", function(d, i) {
                return i * cellSize;
              })
              .attr("transform", function(d, i) {
                return "translate(" + cellSize / 2 + ", -3) rotate(-90) rotate(45, 0, " + (i * cellSize) + ")";
              });
            t.selectAll(".rowLabel")
              .attr("y", function(d, i) {
                return i * cellSize;
              })
              .attr("transform", function(d, i) {
                return "translate(-3," + cellSize / 1.5 + ")";
              });
          }
        }
  
        function sortColumns() {
          var t = svg.transition().duration(1000);
          t.selectAll(".cell")
            .attr("x", function(d) {
              var col = parseInt(d3.select(this).attr("col"));
              return col * cellSize;
            });
          t.selectAll(".colLabel")
            .attr("y", function(d, i) {
              return i * cellSize;
            })
            .attr("transform", function(d, i) {
              return "translate(" + cellSize / 2 + ", -3) rotate(-90) rotate(45, 0, " + (i * cellSize) + ")";
            });
        }
  
        function sortRows() {
          var t = svg.transition().duration(1000);
          t.selectAll(".cell")
            .attr("y", function(d) {
              var row = parseInt(d3.select(this).attr("row"));
              return row * cellSize;
            });
          t.selectAll(".rowLabel")
            .attr("y", function(d, i) {
              return i * cellSize;
            })
            .attr("transform", function(d, i) {
              return "translate(-3," + cellSize / 1.5 + ")";
            });
        }
      });
    }
  
    return createHeatmap;
  };
  
  