var classesNumber = 10, cellSize = 24; //######################################################### function heatmap_display(url, heatmapId, paletteName) { //########################################################################## // Patrick.Brockmann@lsce.ipsl.fr //########################################################################## //================================================== // References // http://bl.ocks.org/Soylent/bbff6cc507dca2f48792 // http://bost.ocks.org/mike/selection/ // http://bost.ocks.org/mike/join/ // http://stackoverflow.com/questions/9481497/understanding-how-d3-js-binds-data-to-nodes // http://bost.ocks.org/mike/miserables/ // http://bl.ocks.org/ianyfchang/8119685 //================================================== var tooltip = d3.select(heatmapId) .append("div") .style("position", "absolute") .style("visibility", "hidden"); //================================================== // http://bl.ocks.org/mbostock/3680958 function zoom() { svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); } // define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom); //================================================== var viewerWidth = $(document).width(); var viewerHeight = $(document).height(); var viewerPosTop = 200; var viewerPosLeft = 100; var legendElementWidth = cellSize * 2; // http://bl.ocks.org/mbostock/5577023 var colors = colorbrewer[paletteName][classesNumber]; // http://bl.ocks.org/mbostock/3680999 var svg; //================================================== d3.json(url, function(error, data) { //console.log(data); var arr = data.data; var row_number = arr.length; var col_number = arr[0].length; //console.log(col_number, row_number); var colorScale = d3.scale.quantize() .domain([0.0, 1.0]) .range(colors); svg = d3.select(heatmapId).append("svg") .attr("width", viewerWidth) .attr("height", viewerHeight) .call(zoomListener) .append("g") .attr("transform", "translate(" + viewerPosLeft + "," + viewerPosTop + ")"); svg.append('defs') .append('pattern') .attr('id', 'diagonalHatch') .attr('patternUnits', 'userSpaceOnUse') .attr('width', 4) .attr('height', 4) .append('path') .attr('d', 'M-1,1 l2,-2 M0,4 l4,-4 M3,5 l2,-2') .attr('stroke', '#000000') .attr('stroke-width', 1); var rowSortOrder = false; var colSortOrder = false; var rowLabels = svg.append("g") .attr("class", "rowLabels") .selectAll(".rowLabel") .data(data.index) .enter().append("text") .text(function(d) { return d.count > 1 ? d.join("/") : 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); //$("#order").jqxComboBox({selectedIndex: 0}); }); var colLabels = svg.append("g") .attr("class", "colLabels") .selectAll(".colLabel") .data(data.columns) .enter().append("text") .text(function(d) { d.shift(); return d.count > 1 ? d.reverse().join("/") : d.reverse(); }) .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); }); var row = svg.selectAll(".row") .data(data.data) .enter().append("g") .attr("id", function(d) { return d.idx; }) .attr("class", "row"); var j = 0; var heatMap = row.selectAll(".cell") .data(function(d) { j++; return d; }) .enter().append("svg:rect") .attr("x", function(d, i) { return i * cellSize; }) .attr("y", function(d, i, j) { return j * cellSize; }) .attr("rx", 4) .attr("ry", 4) .attr("class", function(d, i, j) { return "cell bordered cr" + j + " cc" + i; }) .attr("row", function(d, i, j) { return j; }) .attr("col", function(d, i, j) { return i; }) .attr("width", cellSize) .attr("height", cellSize) .style("fill", function(d) { if (d != null) return colorScale(d); else return "url(#diagonalHatch)"; }) .on('mouseover', function(d, i, j) { d3.select('#colLabel_' + i).classed("hover", true); d3.select('#rowLabel_' + j).classed("hover", true); if (d != null) { tooltip.html('
' + d.toFixed(3) + '
'); tooltip.style("visibility", "visible"); } else tooltip.style("visibility", "hidden"); }) .on('mouseout', function(d, i, j) { d3.select('#colLabel_' + i).classed("hover", false); d3.select('#rowLabel_' + j).classed("hover", false); tooltip.style("visibility", "hidden"); }) .on("mousemove", function(d, i) { tooltip.style("top", (d3.event.pageY - 55) + "px").style("left", (d3.event.pageX - 60) + "px"); }) .on('click', function() { //console.log(d3.select(this)); }); var legend = svg.append("g") .attr("class", "legend") .attr("transform", "translate(0,-300)") .selectAll(".legendElement") .data([0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]) .enter().append("g") .attr("class", "legendElement"); legend.append("svg:rect") .attr("x", function(d, i) { return legendElementWidth * i; }) .attr("y", viewerPosTop) .attr("class", "cellLegend bordered") .attr("width", legendElementWidth) .attr("height", cellSize / 2) .style("fill", function(d, i) { return colors[i]; }); legend.append("text") .attr("class", "mono legendElement") .text(function(d) { return "≥" + Math.round(d * 100) / 100; }) .attr("x", function(d, i) { return legendElementWidth * i; }) .attr("y", viewerPosTop + cellSize); //================================================== // 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) { if (d != null) values.push(d); else values.push(-999); // to handle NaN }); //console.log(values); if (rORc == "r") { // sort on cols sorted = d3.range(col_number).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(row_number).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 + ")"; }); } } //================================================== d3.select("#order").on("change", function() { var newOrder = d3.select("#order").property("value"); changeOrder(newOrder, heatmapId); }); //================================================== d3.select("#palette") .on("keyup", function() { var newPalette = d3.select("#palette").property("value"); if (newPalette != null) // when interfaced with jQwidget, the ComboBox handles keyup event but value is then not available ? changePalette(newPalette, heatmapId); }) .on("change", function() { var newPalette = d3.select("#palette").property("value"); changePalette(newPalette, heatmapId); }); }); //================================================== } //######################################################### function changeOrder(newOrder, heatmapId) { var svg = d3.select(heatmapId); var t = svg.transition().duration(1000); if (newOrder == "sortinit_col") { // initial sort on cols (alphabetically if produced like this) 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) + ")"; }); } else if (newOrder == "sortinit_row") { // initial sort on rows (alphabetically if produced like this) 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 + ")"; }); } else if (newOrder == "sortinit_col_row") { // initial sort on rows and cols (alphabetically if produced like this) 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 changePalette(paletteName, heatmapId) { var colors = colorbrewer[paletteName][classesNumber]; var colorScale = d3.scale.quantize() .domain([0.0, 1.0]) .range(colors); var svg = d3.select(heatmapId); var t = svg.transition().duration(500); t.selectAll(".cell") .style("fill", function(d) { if (d != null) return colorScale(d); else return "url(#diagonalHatch)"; }) t.selectAll(".cellLegend") .style("fill", function(d, i) { return colors[i]; }); }