Skip to content

Instantly share code, notes, and snippets.

@SumNeuron
Last active January 10, 2018 18:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SumNeuron/d4744240642294c1e529947192fa8b32 to your computer and use it in GitHub Desktop.
Save SumNeuron/d4744240642294c1e529947192fa8b32 to your computer and use it in GitHub Desktop.
StackOverflow Help Request: Heatmap with Lasso and Dendogram
/*
NOTE: This code has the assumption that either the chart being produced with have
a dendrogram and labels (e.g. next to the cells in a heat map) OR that the chart
will have visible axes and labels along the axes. Thus in the config
if
config.percentElementsTake.axes.x != 0
then
config.percentElementsTake.dendogram.x = 0
and vis versa
*/
/*
A prototype function to get the absolute position of an element
in the svg regardless of its relative window. This is used for the lasso tool.
*/
d3.selection.prototype.absolutePosition = function() {
var el = this.node();
var elPos = el.getBoundingClientRect();
var vpPos = getVpPos(el);
function getVpPos(el) {
if(el.parentElement.tagName === 'svg') {
return el.parentElement.getBoundingClientRect();
}
return getVpPos(el.parentElement);
}
return {
top: elPos.top - vpPos.top,
left: elPos.left - vpPos.left,
width: elPos.width,
bottom: elPos.bottom - vpPos.top,
height: elPos.height,
right: elPos.right - vpPos.left
};
};
/*
A hard-coded self-made lasso icon to stand place as a temporary improvement over
an empty square.
*/
lassoIconSvg = '<g class="lassoIconGroup">\
<g transform="matrix(1.131,-0.30305,0.256192,0.956123,-7.16519,1.94122)">\
<circle cx="12.739" cy="10.052" r="7.237" style="fill:none;stroke:rgb(35,31,32);stroke-width:0.38px;"/>\
</g>\
<g transform="matrix(1,0,0,1,-3.9849,-0.558624)">\
<g transform="matrix(0.229548,0,0,0.229548,4.97755,13.524)">\
<circle cx="12.739" cy="10.052" r="7.237" style="fill:none;stroke:rgb(35,31,32);stroke-width:1.82px;"/>\
</g>\
<g transform="matrix(1,0,0,1,0.10204,0)">\
<path d="M6.641,17.018C6.641,17.018 9.458,19.952 5.388,19.952" style="fill:none;stroke:rgb(35,31,32);stroke-width:0.42px;"/>\
</g>\
</g>\
<g transform="matrix(1.11933,-0.646247,0.646247,1.11933,-13.8193,6.95008)">\
<path d="M15.504,9.879L17.966,17.243L13.041,17.243L15.504,9.879Z" style="stroke:black;stroke-width:0.32px;"/>\
</g>\
<g transform="matrix(1.49555,-0.863456,0.236964,0.410433,-12.0893,23.4167)">\
<path d="M15.504,9.879L17.966,17.243L13.041,17.243L15.504,9.879Z" style="fill:white;stroke:white;stroke-width:0.33px;"/>\
</g>\
</g>'
// For matter of clarity we need to establish a naming convention for what labels
// are horizontally oriented but are vertically stacked (labels on the y axis)
// and which are vertically (or slanted) oriented but are horizontally spaced (x axis).
// Let the former be y axis labels and the latter x axis labels
barConfig = {
svg: {
id: "barchartSVG",
width: 500,
height: 500
},
percentElementsTake : {
axes: {
x: 0.1,
y: 0.1
},
buttons: 0.0,
dendogram: {
x: 0.0,
y: 0.0
},
labels: {
x: 0.1,
y: 0.1
},
legend: 0.0,
title: 0.05,
toolbar: 0.0,
tooltip: 0.12,
spaceBetween: {
x: 0.02,
y: 0.02
}
}
}
// This is the config for a single heatmap
config = {
svg: {
id: "heatmapSVG",
width: 500,
height: 500
},
percentElementsTake : {
axes: {
x: 0.0,
y: 0.0
},
buttons: 0.0,
dendogram: {
x: 0.1,
y: 0.1
},
labels: {
x: 0.1,
y: 0.1
},
legend: 0.1,
title: 0.05,
toolbar: 0.05,
tooltip: 0.12,
spaceBetween: {
x: 0.02,
y: 0.02
}
},
linkedSVGS: [
function( subdata ) {
makeLinkedBarChart(barConfig, subdata, 'meta', "sex")
}
]
}
// global variables of some use
DEBUG = true;
LASSO = false;
function configurateHeatmap(config) {
// select svg
svg = d3.select("svg#"+config.svg.id)
if (svg.empty()) {
if (DEBUG) {
console.log("ERROR\n\tconfigurateHeatmap(config): svg selection is empty")
}
}
svg.style("width", config.svg.width)
svg.style("height", config.svg.height)
}
function configurateSVG(config) {
// select svg
svg = d3.select("svg#"+config.svg.id)
if (svg.empty()) {
if (DEBUG) {
console.log("ERROR\n\tconfigurateHeatmap(config): svg selection is empty")
}
}
svg.style("width", config.svg.width)
svg.style("height", config.svg.height)
}
// get the heatmap labels acording to the keys in which they are stored
function getHeatmapLabels(data, xAxisKey, yAxisKey) {
// store unique labels for each access
xAxisLabels = []
yAxisLabels = []
data.map(
function( element )
{
curYKey = element[yAxisKey]
curXKey = element[xAxisKey]
if ( yAxisLabels.indexOf(curYKey) == -1 ) {
yAxisLabels.push( curYKey )
}
if ( xAxisLabels.indexOf(curXKey) == -1 ) {
xAxisLabels.push( curXKey )
}
}
)
return {"xAxis": xAxisLabels, "yAxis": yAxisLabels}
}
function getUnique(array) {
// store unique labels for each access
unique = []
array.map(
function( element ){
if ( unique.indexOf(element) == -1 ) {
unique.push( element )
}
})
return unique
}
// number of cells wide x number of cells high
function getheatmapCellDimensions(xAxisLabels, yAxisLabels) {
return {x: xAxisLabels.length, y: yAxisLabels.length}
}
function getMetaData(data, metaKey, metaSubkey) {
metaData = []
for (var i = 0; i < data.length; i++) {
metaData.push(data[i][metaKey][metaSubkey])
}
return metaData
}
function tally( array ) {
tallies = {}
array.map( function ( element ) {
if ( d3.keys(tallies).indexOf(element) == -1 ) {
tallies[element] = 1
} else {
tallies[element] += 1
}
})
return tallies
}
//-------------------------------------------------------------------//
// //
// MAKE AXES //
// //
//-------------------------------------------------------------------//
// Makes axes / updates axes if they already exist
function makeBarChartAxes(svg, xAxesLabels, yMax, pixelsRequiredByOthers, drawingSpace) {
// y-axis
var yAxisScale = d3.scaleLinear().domain([0, yMax]).range([drawingSpace.y, 0])
var yAxis = d3.axisLeft().scale(yAxisScale).tickSize(pixelsRequiredByOthers.axes.x / 4).ticks(5)
// x-axis
var xAxisScale = d3.scaleBand()
.domain(xAxesLabels.map(function(d) {
// if (d[hyperParameters.data.x].length > hyperParameters.fonts.axes.maxCharacters.x) {
// return d[hyperParameters.data.x].slice(0, hyperParameters.fonts.axes.maxCharacters.x - 3) + "..."
// } // for truncating long text
return d.slice(0, 12)
}))
.range([0, drawingSpace.x])
.align([0.5])
var xAxis = d3.axisBottom().scale(xAxisScale)
// y axis
svg.select(".yAxisContainer").transition().duration(500).call(yAxis)
svg.selectAll(".yAxisContainer").transition().duration(500).attr("transform", function(d,i){
x = pixelsRequiredByOthers.spaceBetween.y * 3 + pixelsRequiredByOthers.dendogram.y + pixelsRequiredByOthers.labels.y + pixelsRequiredByOthers.axes.y
y = pixelsRequiredByOthers.spaceBetween.x * 4 + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.title + pixelsRequiredByOthers.buttons
trans = "translate("+(x)+","+(y)+")"
return trans
})
// x axis
svg.select(".xAxisContainer").transition().duration(500).call(xAxis)
svg.selectAll(".xAxisContainer").transition().duration(500).attr("transform", function(d,i){
x = pixelsRequiredByOthers.spaceBetween.y * 4 + pixelsRequiredByOthers.dendogram.y + pixelsRequiredByOthers.labels.y + pixelsRequiredByOthers.axes.y
y = pixelsRequiredByOthers.spaceBetween.x * 5 + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.title + pixelsRequiredByOthers.buttons + drawingSpace.y
trans = "translate("+(x)+","+(y)+")"
return trans
})
// Adjust text to angle, fontsize and fontfamily
xFontSize = pixelsRequiredByOthers.labels.x / 4
yFontSize = pixelsRequiredByOthers.labels.y / 4
svg.selectAll(".xAxisContainer")
.selectAll('text')
.attr("text-anchor", "end")
.attr("font-size", xFontSize)
.attr("transform", "rotate(-45)").transition().duration(500)
.attr("x", -xFontSize)
.attr("y", xFontSize)
svg.selectAll(".yAxisContainer")
.selectAll('text')
.attr("font-size", yFontSize)
}
//-------------------------------------------------------------------//
// //
// MAKE LASSO //
// //
//-------------------------------------------------------------------//
/*
Lasso function call heirarchy:
setUpLasso - calls and organizes the following
| setUpLassoIcon (0)
|
| lassoCells (1)
| |
| |---> lassoTick (2)
| |-----| ---> checkWhichCellsInLasso(svg, lassoPointData) (3)
| |-----| ---> applyToCellsInLasso(svg) (4)
function 0 sets up the toggle indecator and global boolean toggle to the lasso icon
function 1 creates the container for the the lasso polygon which the user sees
function 2 is called by function 1 and it adds the current mouse position
to lasso and then calls functions 3 and 4
function 3 marks which cells of the heatmap are inside the lasso with the class
inLasso
function 4 finally inacts what should happen to cells in the selection
Currently, changes selected cell styles and then calls linked svg
functions with the subset data
Also, it resets non-select cells to default styles.
*/
function setUpLasso(data, config, svg, chartContainer, toolbarContainer, pixelsRequiredByOthers) {
setUpLassoIcon(data, svg, toolbarContainer, pixelsRequiredByOthers)
// stores the mouse movements
var lassoPointData = [];
svg.on("mousedown", function(d, i){
lassoCells(svg, chartContainer, config)
})
svg.on("mouseup", function(d, i) {
// remove the polygon
svg.select(".lassoContainer").remove();
// stop tracking mouse movements
svg.on("mousemove", null);
})
}
function setUpLassoIcon(data, svg, toolbarContainer, pixelsRequiredByOthers) {
/*
This will make the lassoIcon and add the event listener for
when the lassoIcon is clicked
*/
if (toolbarContainer.select(".lassoIconContainer").empty()) {
lassoIconContainer = toolbarContainer.append("g").attr("class","lassoIconContainer") // move lasso over
} else {
lassoIconContainer = toolbarContainer.select("g.lassoIconContainer")
}
// put the hardcoded image in there
if (lassoIconContainer.select("svg").empty()) {
lassoIconSVGContainer = lassoIconContainer.append("svg")
}
lassoIconSVGContainer = lassoIconContainer.select("svg")
lassoIconSVGContainer.node().innerHTML = lassoIconSvg
// add an invisible rectangle as otherwise mousedown doesnt register
if (lassoIconContainer.select("rect.hiddenBox").empty()) {
lassoIconContainer.append("rect").attr("class", 'hiddenBox')
}
lassoIconContainer.select("rect.hiddenBox")
.attr("width", pixelsRequiredByOthers.toolbar)
.attr("height", pixelsRequiredByOthers.toolbar)
.attr("fill", "white").style("opacity",0)
.on("mousedown", lassoIconMousedown)
function lassoIconMousedown(d, i) {
if (LASSO) {
LASSO = false // toggle lasso ability on and off
lassoIconSVGContainer.selectAll("circle")
.style("fill", "white") // inside of lasso is white
// when it goes off, reset all cells to default style
svg.selectAll(".cellContainer").selectAll("rect")
.attr("stroke-width", 1)
.style("opacity", 1)
// remove lasso class from all cells
svg.selectAll(".cellContainer").classed("inLasso", false);
for (var i = 0; i < config.linkedSVGS.length; i++) {
config.linkedSVGS[i](data)
}
} else {
LASSO = true
// color the icon to give user notification
lassoIconSVGContainer.selectAll("circle")
.style("fill", "cyan")
}
}
}
// track the cells in the lasso
function lassoCells(svg, chartContainer, config) {
if (!LASSO) {return null} // only proceed if Lasso is turned on
// store the points where the mouse was at
lassoPointData = [];
// function for making the polygon
var lassoLine = d3.line().x(function(d, i) { return d[0]; }).y(function(d, i) { return d[1]; });
// container for the lasso
lassoContainer = chartContainer.append("g").attr("class", "lassoContainer")
// stylize the lasso
lasso = lassoContainer.append("path") // dont worry about the append because
.data([lassoPointData]) // the container will be removed on mouse up
.attr("class", "line")
.attr("d", lassoLine)
.attr("fill", "blue")
.style("opacity", 0.3)
.attr("stroke", "blue")
.attr("stroke-width", 3)
// on move, append another point
svg.on("mousemove", function() {
var pt = d3.mouse(this);
lassoTick(pt, lassoPointData, lasso, lassoLine, svg, config);
});
} // end lassoCells
function checkWhichCellsInLasso(svg, lassoPointData) {
// for each cell of the heatmap
svg.selectAll(".cellContainer").each(function(d, i) {
// get current cell of the heatmap
currentCell = d3.select(this)
// get absolute position of this cell in the svg
currentBox = currentCell.absolutePosition()
// get the four corners of the bounding box
pts = [
[currentBox.left, currentBox.top],
[currentBox.right, currentBox.top],
[currentBox.left, currentBox.bottom],
[currentBox.right, currentBox.bottom]
]
// helper function to see if all the points are in the lasso
function allPointsInLassoHullQ(element, index, array) {
return d3.polygonContains(lassoPointData, element)
}
// boolean using above function
allPointsInLasso = pts.every(allPointsInLassoHullQ)
// if the entire cell is inside the lasso, change the class and style
if (allPointsInLasso) {
currentCell.classed("inLasso", true);
} else {
currentCell.classed("inLasso", false);
}
}) // end each
}
function applyToCellsInLasso(svg, config) {
subsetData = []
svg.selectAll(".cellContainer").each(function(){
currentCell = d3.select(this)
if (currentCell.classed("inLasso")) {
currentCell.select("rect").attr("stroke-width", 3)
currentCell.raise()
subsetData.push(currentCell.data()[0])
} else {
currentCell.select("rect")
.attr("stroke-width", 1)
.style("opacity", 1)
currentCell.classed("inLasso", false);
}
})
for (var i = 0; i < config.linkedSVGS.length; i++) {
config.linkedSVGS[i](subsetData)
}
}
// when a new point is pushed
function lassoTick(pt, lassoPointData, lasso, lassoLine, svg, config) {
// push a new data point onto the back
lassoPointData.push(pt);
// Redraw the path:
lasso.attr("d", function(d) { return lassoLine(d);})
// make the hull of the lasso and test to see what is inside if more than
// 3 points
if (lassoPointData.length < 3) { return null }
checkWhichCellsInLasso(svg, lassoPointData)
applyToCellsInLasso(svg, config)
} // end lassoTick
function getChartGroups(svg) {
if (svg.selectAll("g").empty()) {
/******************************************************************
* *
* set up groups *
* *
******************************************************************/
// contains all chart elements, e.g. labels, dendogram, buttons, etc
var chartContainer = svg.append("g").attr("class", "chartContainer")
// contains the cells for the heat map
var plotContainer = chartContainer.append("g").attr("class", "plotContainer")
// container for the labels along the axes
var axesLabelContainer = chartContainer.append("g").attr("class", "axesLabelContainer")
var xAxisLabelContainer = axesLabelContainer.append("g").attr("class", "xAxisLabelContainer")
var yAxisLabelContainer = axesLabelContainer.append("g").attr("class", "yAxisLabelContainer")
// container for the axes themselves
var axesContainer = chartContainer.append("g").attr("class", "axesContainer")
var xAxisContainer = axesContainer.append("g").attr("class", "xAxisContainer")
var yAxisContainer = axesContainer.append("g").attr("class", "yAxisContainer")
// Title container
var titleContainer = chartContainer.append("g").attr("class", "titleContainer")
// svg button container (for toggling between data)
var buttonContainer = chartContainer.append("g").attr("class", "buttonContainer")
// svg button container (for toggling between data)
var toolbarContainer = chartContainer.append("g").attr("class", "toolbarContainer")
// color legend
var legendContainer = chartContainer.append("g").attr("class", "legendContainer")
// container for the dendograms along the axes
var dendogramContainer = chartContainer.append("g").attr("class", "dendogramContainer")
var xAxisdendogramContainer = dendogramContainer.append("g").attr("class", "xAxisdendogramContainer")
var yAxisdendogramContainer = dendogramContainer.append("g").attr("class", "yAxisdendogramContainer")
} else {
/******************************************************************
* *
* select groups *
* *
******************************************************************/
// contains all chart elements, e.g. labels, dendogram, buttons, etc
var chartContainer = svg.select("g.chartContainer")
// contains the cells for the heat map
var plotContainer = chartContainer.select("g.plotContainer")
// container for the labels along the axes
var axesLabelContainer = chartContainer.select("g.axesLabelContainer")
var xAxisLabelContainer = axesLabelContainer.select("g.xAxisLabelContainer")
var yAxisLabelContainer = axesLabelContainer.select("g.yAxisLabelContainer")
// container for the axes themselves
var axesContainer = chartContainer.select("g.axesContainer")
var xAxisContainer = axesContainer.select("g.xAxisContainer")
var yAxisContainer = axesContainer.select("g.yAxisContainer")
// Title container
var titleContainer = chartContainer.select("g.titleContainer")
// svg button container (for toggling between data)
var buttonContainer = chartContainer.select("g.buttonContainer")
// svg button container (for toggling between data)
var toolbarContainer = chartContainer.select("g.toolbarContainer")
// color legend
var legendContainer = chartContainer.select("g.legendContainer")
// container for the dendograms along the axes
var dendogramContainer = chartContainer.select("g.dendogramContainer")
var xAxisdendogramContainer = dendogramContainer.select("g.xAxisdendogramContainer")
var yAxisdendogramContainer = dendogramContainer.select("g.yAxisdendogramContainer")
}
// return the selections
return [
chartContainer, plotContainer,
axesLabelContainer, xAxisContainer, yAxisContainer,
axesLabelContainer, xAxisLabelContainer, yAxisLabelContainer,
titleContainer, toolbarContainer, buttonContainer, legendContainer,
dendogramContainer, xAxisdendogramContainer, yAxisdendogramContainer
]
}
function moveChartGroups(config, drawingSpace, pixelsRequiredByOthers,
chartContainer, plotContainer,
axesLabelContainer, xAxisContainer, yAxisContainer,
axesLabelContainer, xAxisLabelContainer, yAxisLabelContainer,
titleContainer, toolbarContainer, buttonContainer, legendContainer,
dendogramContainer, xAxisdendogramContainer, yAxisdendogramContainer
) {
plotContainer.attr("transform",function(d, i) {
x = (pixelsRequiredByOthers.dendogram.y + pixelsRequiredByOthers.axes.y + pixelsRequiredByOthers.labels.y + pixelsRequiredByOthers.spaceBetween.y * 4)
// pixelsRequiredByOthers.spaceBetween.y * 3 + pixelsRequiredByOthers.dendogram.y + pixelsRequiredByOthers.labels.y + pixelsRequiredByOthers.axes.y
y = (pixelsRequiredByOthers.title + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.buttons + pixelsRequiredByOthers.spaceBetween.x * 4)
trans = "translate("+(x)+","+(y)+")"
return trans
})
titleContainer.attr("transform", function(d, i) {
x = parseFloat(svg.style("width")) / 2
y = (pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.spaceBetween.y * 2)
trans = "translate("+(x)+","+(y)+")"
return trans
})
legendContainer.attr("transform", function(d, i) {
x = (pixelsRequiredByOthers.spaceBetween.x * 1)
y = (pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.title + pixelsRequiredByOthers.spaceBetween.y * 3)
trans = "translate("+(x)+","+(y)+")"
return trans
}) // move it over
yAxisdendogramContainer.attr("transform",function(){
x = (pixelsRequiredByOthers.dendogram.x + pixelsRequiredByOthers.labels.x + pixelsRequiredByOthers.spaceBetween.x * 3)
y = (pixelsRequiredByOthers.title + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.buttons + pixelsRequiredByOthers.labels.y + pixelsRequiredByOthers.spaceBetween.y * 6 + drawingSpace.y + pixelsRequiredByOthers.dendogram.y)
xCent = (x + drawingSpace.x / 2)
yCent = (y + pixelsRequiredByOthers.dendogram.y / 2)
trans = "translate("+(x)+","+(y)+")"
// rot = "rotate(90 "+(xCent)+" "+(yCent)+")";
return trans
})
xAxisdendogramContainer.attr("transform",function(){
x = (pixelsRequiredByOthers.spaceBetween.x * 1)
y = (pixelsRequiredByOthers.title + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.buttons + pixelsRequiredByOthers.spaceBetween.y * 4)
xCent = (x + drawingSpace.x / 2)
yCent = (y + pixelsRequiredByOthers.dendogram.y / 2)
trans = "translate("+(x)+","+(y)+")"
// rot = "rotate(90 "+(xCent)+" "+(yCent)+")";
return trans
})
legendContainer.attr("transform", function(d, i) {
x = (pixelsRequiredByOthers.dendogram.y + pixelsRequiredByOthers.labels.y + drawingSpace.x + pixelsRequiredByOthers.spaceBetween.y * 5)
y = (pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.title + pixelsRequiredByOthers.spaceBetween.y * 4)
trans = "translate("+(x)+","+(y)+")"
return trans
})
toolbarContainer.attr("transform", function(d, i) {
x = (pixelsRequiredByOthers.spaceBetween.x * 1)
y = (pixelsRequiredByOthers.spaceBetween.y * 1)
trans = "translate("+(x)+","+(y)+")"
return trans
})
}
function makeLinkedBarChart(config, data, metaKey, metaSubkey) {
configurateSVG(config)
// select the svg
var svg = d3.select("svg#"+config.svg.id)
// extract the relevant meta data
var metaData = getMetaData(data, metaKey, metaSubkey)
var tallies = tally(metaData)
var labels = d3.keys(tallies)
// temp title element for demo purposes
var title = "My Linked Barchart"
// calculate space needed for all elements
var pixelsRequiredByOthers = calculatePixelsNeeded(config.svg.id, config.percentElementsTake)
var drawingSpace = getDrawingSpace(config.svg.id, pixelsRequiredByOthers)
// get min / max of data
var dataExtent = {
min: d3.min(labels.map(function(d){return tallies[d]})),
max: d3.max(labels.map(function(d){return tallies[d]}))
}
/******************************************************************
* get main groups *
******************************************************************/
var [
chartContainer, plotContainer,
axesLabelContainer, xAxisContainer, yAxisContainer,
axesLabelContainer, xAxisLabelContainer, yAxisLabelContainer,
titleContainer, toolbarContainer, buttonContainer, legendContainer,
dendogramContainer, xAxisdendogramContainer, yAxisdendogramContainer
] = getChartGroups(svg) // makes / gets groups
// move groups to their respective positions within the chart
moveChartGroups(config, drawingSpace, pixelsRequiredByOthers,
chartContainer, plotContainer,
axesLabelContainer, xAxisContainer, yAxisContainer,
axesLabelContainer, xAxisLabelContainer, yAxisLabelContainer,
titleContainer, toolbarContainer, buttonContainer, legendContainer,
dendogramContainer, xAxisdendogramContainer, yAxisdendogramContainer
)
// parameters for the bars in this bar chart
var bar = {
width: drawingSpace.x / (labels.length + 1),
// one bar's width will be used for the spacers between the bars
spacer: (drawingSpace.x / (labels.length + 1)) / (labels.length + 1),
// in total there will be (numberOfBars + 1) spacers
scale: d3.scaleLinear()
.domain([0, dataExtent.max])
.range([0,drawingSpace.y]),
color: d3.scaleLinear()
.domain([dataExtent.min, dataExtent.max])
.range(["cyan", "blue"])
}
/******************************************************************
* *
* set up barchart proper *
* *
******************************************************************/
// current number of bars
numberOfBars = plotContainer.selectAll(".barContainer").size()
// number of bars needed
numberOfBarsNeeded = labels.length
if (numberOfBarsNeeded > numberOfBars) { // need more bars
plotContainer.selectAll(".barContainer")
.data(labels) // bind data
.enter() // will only produce (numberOfBarsNeeded - numberOfBars) # of bars
.append("g").attr("class", "barContainer")
.append("rect")
.attr("stroke", "black") // all bars will have the following styles
.attr("stroke-width", "1px") // as they will originally be created here
.attr("rx", "10px")
.attr("ry", "10px")
} else { // remove excess bars
plotContainer.selectAll(".barContainer")
.data(labels) // bind data
.exit().remove() // remove excess
}
// adjust things for pre-existing bars
plotContainer.selectAll(".barContainer")
.attr("transform", function(d, i) {
x = i * bar.width + bar.spacer * (i+1) // + 1 for the leading spacer
y = drawingSpace.y - bar.scale(tallies[d])
trans = "translate("+(x)+","+(y)+")"
return trans
}) // move bars to respective position
.select("rect")
.attr("width", bar.width)
.attr("height", function (d, i) {
return bar.scale(tallies[d])
}) // reset the height of all bars
.attr("fill", function (d, i) {
return bar.color(tallies[d])
}) // bar cell
.on("mousemove", mousemoveBar)
.on("mouseout", mouseoutBar)
/******************************************************************
* set up title *
******************************************************************/
makeTitle(svg, titleContainer, pixelsRequiredByOthers, title)
makeBarChartAxes(svg, labels, dataExtent.max, pixelsRequiredByOthers, drawingSpace)
function mousemoveBar(d, i) { // simple tooltip for demo purposes
var tooltip = svg.select("g.tooltip")
if (tooltip.empty()) {
tooltip = svg.append("g").attr("class","tooltip")
}
// move tooltip to mouse location
tooltip.attr("transform", function(d, i) {
x = d3.event.pageX - document.getElementById(config.svg.id).getBoundingClientRect().x + 10
y = d3.event.pageY - document.getElementById(config.svg.id).getBoundingClientRect().y + 10
trans = "translate("+(x)+","+(y)+")"
return trans
})
// add a background for the tooltip rather than use an external div
tooltipRect = tooltip.append("rect").attr("fill", "white")
// add the text
tooltipText = tooltip.append("g").attr("class","tooltipText")
tooltipText.append("text").text(tallies[d])
.attr("text-anchor","middle")
tooltipText.attr("transform","translate("+(tooltipText.node().getBBox().width)+","+(tooltipText.node().getBBox().height)+")")
// stylize the rectangle
tooltipRect.attr("fill", "white")
.attr("width", tooltipText.node().getBBox().width * 2)
.attr("height", tooltipText.node().getBBox().height * 2)
.attr("rx", 10)
.attr("ry", 10)
.attr("stroke","black")
tooltip.raise()
}
function mouseoutBar(d, i) {
svg.selectAll("g.tooltip").remove()
}
}
function makeTitle(svg, titleContainer, pixelsRequiredByOthers, title) {
if ( titleContainer.select("text").empty() ) {
titleContainer.append("text")
}
titleContainer.select("text").text(title) // move the title to position
.attr('text-anchor',"middle") // lazy centering
.attr("font-size", pixelsRequiredByOthers.title + "px")
.style("user-select", "none") // disable user select as it interfers with lasso
.style("pointer-events", "none")
}
// A sloppy temp for the make heatmap function
function makeHeatmap(config, data, labelKeys) {
// select the svg
var svg = d3.select("svg#"+config.svg.id)
// start to fill the heatmap
var labels = getHeatmapLabels(data, labelKeys.x, labelKeys.y)
// a temp title title to see spacing
var title = "My Heatmap"
// The pixel space used up by all other elements besides the actual cells of
// the heatmap
var pixelsRequiredByOthers = calculatePixelsNeeded(config.svg.id, config.percentElementsTake)
// the pixel space required by the actual heatmap
var drawingSpace = getDrawingSpace(config.svg.id, pixelsRequiredByOthers)
// min / max values of the cells
var dataExtent = {
min: d3.min(data.map(function(d){return d.val;})),
max: d3.max(data.map(function(d){return d.val;}))
}
// number of cells wide x number of cells high
cellDimensions = getheatmapCellDimensions(labels.xAxis, labels.yAxis)
// the size of a single cell in the heatmap
cellSize = {x: drawingSpace.x / cellDimensions.x, y: drawingSpace.y / cellDimensions.y}
// the color interpolation function
cellColors = d3.scaleSequential(d3.interpolateGnBu).domain([dataExtent.min, dataExtent.max])
/******************************************************************
* get main groups *
******************************************************************/
var [
chartContainer, plotContainer,
axesLabelContainer, xAxisContainer, yAxisContainer,
axesLabelContainer, xAxisLabelContainer, yAxisLabelContainer,
titleContainer, toolbarContainer, buttonContainer, legendContainer,
dendogramContainer, xAxisdendogramContainer, yAxisdendogramContainer
] = getChartGroups(svg) // makes / gets groups
// move groups to their respective positions within the chart
moveChartGroups(config, drawingSpace, pixelsRequiredByOthers,
chartContainer, plotContainer,
axesLabelContainer, xAxisContainer, yAxisContainer,
axesLabelContainer, xAxisLabelContainer, yAxisLabelContainer,
titleContainer, toolbarContainer, buttonContainer, legendContainer,
dendogramContainer, xAxisdendogramContainer, yAxisdendogramContainer
)
/******************************************************************
* *
* set up heatmap proper *
* *
******************************************************************/
// current number of cells
numberOfCells = plotContainer.selectAll(".cellContainer").size()
// number of cells needed
numberOfCellsNeeded = cellDimensions.x * cellDimensions.y
if (numberOfBarsNeeded > numberOfCells) {
plotContainer.selectAll(".cellContainer")
.data(data) // bind data
.enter()
.append("g").attr("class", "cellContainer")
.append("rect")
.attr("stroke", "black")
.attr("stroke-width", "1px")
.attr("rx", "10px")
.attr("ry", "10px")
} else {
plotContainer.selectAll(".cellContainer")
.data(data) // bind data
.exit().remove() // remove extra containers
}
plotContainer.selectAll(".cellContainer")
.attr("transform", function(d, i) {
xAxislabelIndex = labels.xAxis.indexOf(d[labelKeys.x]);
yAxislabelIndex = labels.yAxis.indexOf(d[labelKeys.y]);
x = cellSize.x * xAxislabelIndex;
y = cellSize.y * yAxislabelIndex;
trans = "translate("+(x)+","+(y)+")"
return trans
}) // move cells to respective position
.select("rect")
.attr("width", cellSize.x)
.attr("height", cellSize.y)
.attr("fill", function(d, i) {return cellColors(d.val)}) // color cell
.on("mousemove", mousemoveCell) // add tooltip
.on("mouseout", mouseoutCell) // remove tooltip
function mousemoveCell(d, i) { // simple tooltip for demo purposes
tooltip = d3.select("g.tooltip")
if (tooltip.empty()) {
tooltip = svg.append("g").attr("class","tooltip")
}
// move tooltip to mose location
tooltip.attr("transform", function(d, i) {
x = d3.event.pageX - document.getElementById(config.svg.id).getBoundingClientRect().x + 10
y = d3.event.pageY - document.getElementById(config.svg.id).getBoundingClientRect().y + 10
trans = "translate("+(x)+","+(y)+")"
return trans
})
// add a background for the tooltip rather than use an external div
tooltipRect = tooltip.append("rect").attr("fill", "white")
// add the text
tooltipText = tooltip.append("g").attr("class",".tooltipText")
tooltipText.append("text").text(d.val.toFixed(4))
.attr("text-anchor","middle")
tooltipText.attr("transform","translate("+(tooltipText.node().getBBox().width)+","+(tooltipText.node().getBBox().height)+")")
.style("user-select", "none")
.style("pointer-events", "none")
// stylize the rectangle
tooltipRect.attr("fill", "white")
.attr("width", tooltipText.node().getBBox().width * 2)
.attr("height", tooltipText.node().getBBox().height * 2)
.attr("rx", 10)
.attr("ry", 10)
.attr("stroke","black")
}
function mouseoutCell(d, i) {
svg.selectAll("g.tooltip").remove()
}
/******************************************************************
* set up title *
******************************************************************/
makeTitle(svg, titleContainer, pixelsRequiredByOthers, title)
/******************************************************************
* set up LASSO *
******************************************************************/
setUpLasso(data, config, svg, chartContainer, toolbarContainer, pixelsRequiredByOthers)
/******************************************************************
* *
* set up colored legend *
* *
******************************************************************/
addGnBuVerticalGradient(svg)
if ( legendContainer.select("rect.colorRect") ) {
colorLegendRect = legendContainer.append("rect").attr("class", "colorRect")
}
if ( legendContainer.select("text.legendMin") ) {
legendContainer.append("text").attr("class", "legendMin")
}
if ( legendContainer.select("text.legendMax") ) {
legendContainer.append("text").attr("class", "legendMax")
}
// the colored rectangle
colorLegendRect = legendContainer.select("rect")
.attr("width", pixelsRequiredByOthers.legend)
.attr("height", drawingSpace.y * 0.8)
.attr("rx", 10)
.attr("ry", 10)
.attr("transform", "translate(0,"+(drawingSpace.y * 0.1)+")")
.style("fill", "url(#GnBuVerticalGradient)")
.attr("stroke","black")
// text for min
legendContainer.select("text.legendMin")
.attr("transform", "translate("+(pixelsRequiredByOthers.legend * 0.5)+","+(drawingSpace.y * 0.1 - 6)+")")
.attr("class","legendText").text(dataExtent.min.toFixed(4))
.attr("font-size", 12)
.attr("text-anchor", "middle")
.style("user-select", "none") // no user interaction with text
.style("pointer-events", "none")
// text for max
legendContainer.select("text.legendMax")
.attr("transform", "translate("+(pixelsRequiredByOthers.legend * 0.5)+","+(drawingSpace.y * .9 + 12)+")")
.attr("class","legendText").text(dataExtent.max.toFixed(4))
.attr("font-size", 12)
.attr("text-anchor", "middle")
.style("user-select", "none")
.style("pointer-events", "none")
/******************************************************************
* *
* set up labels *
* *
******************************************************************/
xLabels = xAxisLabelContainer.selectAll(".xLabel").data(labels.xAxis).enter()
.append("g").attr("class","xLabel")
.attr("transform", function(e, i) {
x = (pixelsRequiredByOthers.dendogram.x + pixelsRequiredByOthers.labels.x + pixelsRequiredByOthers.spaceBetween.x * 3)
y = (pixelsRequiredByOthers.title + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.buttons + pixelsRequiredByOthers.spaceBetween.y * 5)
x += cellSize.x * i + cellSize.x / 2;
y += cellSize.y * cellDimensions.y + pixelsRequiredByOthers.labels.y / 2 ;
trans = "translate("+(x)+","+(y)+")";
return trans
}) // move the labels over
xLabels.append("text").text(function(d){return d})
.style("user-select", "none")
.style("pointer-events", "none") // no user interaction with the text
yLabels = xAxisLabelContainer.selectAll(".yLabel").data(labels.yAxis).enter()
.append("g").attr("class","yLabel")
.attr("transform", function(e, i) {
x = (pixelsRequiredByOthers.dendogram.x + pixelsRequiredByOthers.labels.x + pixelsRequiredByOthers.spaceBetween.x * 2)
y = (pixelsRequiredByOthers.title + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.buttons + pixelsRequiredByOthers.spaceBetween.y * 4)
x += -pixelsRequiredByOthers.labels.x / 2
y += cellSize.y * i + cellSize.y / 2;
trans = "translate("+(x)+","+(y)+")";
return trans
}) // move the labels over
yLabels.append("text").text(function(d){return d})
.style("user-select", "none")
.style("pointer-events", "none") // no user interaction with the text
/******************************************************************
* *
* set up dendogram *
* *
******************************************************************/
var yRoot = d3.stratify()
.id(function(d) { return d.name; })
.parentId(function(d) { return d.parent; })
(yDend); // make the data heirarchical
// make the tree
var yDendoTree = d3.tree().size([drawingSpace.x, pixelsRequiredByOthers.dendogram.y]);
// make the links
var yLinks = yAxisdendogramContainer.selectAll(".link")
.data(yDendoTree(yRoot).links())
.enter().append("path")
.attr("class", "link")
.attr("d", d3.linkVertical()
.x(function(d) { return d.x; })
.y(function(d) { return -d.y; }))
.attr("fill", "none")
.attr("stroke", "black")
// move the tree
yAxisdendogramContainer.attr("transform",function(){
x = (pixelsRequiredByOthers.dendogram.x + pixelsRequiredByOthers.labels.x + pixelsRequiredByOthers.spaceBetween.x * 3)
y = (pixelsRequiredByOthers.title + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.buttons + pixelsRequiredByOthers.labels.y + pixelsRequiredByOthers.spaceBetween.y * 6 + drawingSpace.y + pixelsRequiredByOthers.dendogram.y)
xCent = (x + drawingSpace.x / 2)
yCent = (y + pixelsRequiredByOthers.dendogram.y / 2)
trans = "translate("+(x)+","+(y)+")"
// rot = "rotate(90 "+(xCent)+" "+(yCent)+")";
return trans
})
// hierarchical
var xRoot = d3.stratify()
.id(function(d) { return d.name; })
.parentId(function(d) { return d.parent; })
(xDend);
// make the tree
var xDendoTree = d3.tree().size([drawingSpace.y,pixelsRequiredByOthers.dendogram.x]);
// make the links
var xLinks = xAxisdendogramContainer.selectAll(".link")
.data(xDendoTree(xRoot).links())
.enter().append("path")
.attr("class", "link")
.attr("d", d3.linkHorizontal()
.x(function(d) { return d.y; })
.y(function(d) { return d.x; }))
.attr("fill", "none")
.attr("stroke", "black")
// move the tree
xAxisdendogramContainer.attr("transform",function(){
x = (pixelsRequiredByOthers.spaceBetween.x * 1)
y = (pixelsRequiredByOthers.title + pixelsRequiredByOthers.toolbar + pixelsRequiredByOthers.buttons + pixelsRequiredByOthers.spaceBetween.y * 4)
xCent = (x + drawingSpace.x / 2)
yCent = (y + pixelsRequiredByOthers.dendogram.y / 2)
trans = "translate("+(x)+","+(y)+")"
// rot = "rotate(90 "+(xCent)+" "+(yCent)+")";
return trans
})
}
function calculatePixelsNeeded(svgID, percentElementsTake) {
/*
this turns the percentages of the elements around the HeatMap
(title, labels, dendogram, legend, etc) into pixel values the function
getDrawingSpace calculates how much space is left over for the heatmap proper
*/
var svg = d3.select("svg#"+svgID)
var w = parseFloat(svg.style("width"))
var h = parseFloat(svg.style("height"))
var pixelsRequired = {
axes: {
x: percentElementsTake.axes.x * w,
y: percentElementsTake.axes.y * h
},
dendogram: {
x: percentElementsTake.dendogram.x * w,
y: percentElementsTake.dendogram.y * h
},
labels: {
x: percentElementsTake.labels.x * w,
y: percentElementsTake.labels.y * h,
},
title: percentElementsTake.title * h,
toolbar: percentElementsTake.toolbar * h,
buttons: percentElementsTake.buttons * h,
legend: percentElementsTake.legend * w,
spaceBetween: {
x: percentElementsTake.spaceBetween.x * w,
y: percentElementsTake.spaceBetween.x * h
}
}
return pixelsRequired
}
function getDrawingSpace(svgID, pixelsRequiredByOtherElements) {
/*
this uses the pixel values of other elements in the chart (e.g. title, labels)
and calculates how much space is left over for the heatmap proper
*/
var svg = d3.select("svg#"+svgID)
var w = parseFloat(svg.style("width"))
var h = parseFloat(svg.style("height"))
// for convience
var margins = pixelsRequiredByOtherElements
var drawingSpace = {
x: w - (margins.axes.x + margins.dendogram.x + margins.labels.x + margins.legend + margins.spaceBetween.y * 6),
y: h - (margins.axes.y + margins.dendogram.y + margins.labels.y + margins.title + margins.toolbar + margins.buttons + margins.spaceBetween.x * 8)
}
return drawingSpace
}
function addGnBuVerticalGradient(svg) {
//Append a defs (for definition) element to your SVG
if ( svg.select("defs").empty() ) {
var defs = svg.append("defs");
}
var defs = svg.select("defs");
//Append a linearGradient element to the defs and give it a unique id
if (defs.select("#legendLinearGradient").empty()) {
var legendLinearGradient = defs.append("linearGradient")
.attr("id", "GnBuVerticalGradient");
}
var legendLinearGradient = defs.select("linearGradient")
// vertical gradient
legendLinearGradient.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "0%")
.attr("y2", "100%");
// A color scale (same colors as interpolateGnBu)
var legendColorScale = d3.scaleLinear().range(["#f7fcf0","#e0f3db","#ccebc5","#a8ddb5","#7bccc4","#4eb3d3","#2b8cbe","#0868ac","#084081"]);
//Append multiple color stops by binding data
legendLinearGradient.selectAll("stop")
.data( legendColorScale.range() )
.enter().append("stop")
.attr("offset", function(d,i) { return i/(legendColorScale.range().length-1); })
.attr("stop-color", function(d) { return d; });
}
configurateHeatmap(config)
makeLinkedBarChart(barConfig, data, 'meta', "sex")
makeHeatmap(config, data, {x:"xName",y:"yName"})
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>HeatMap</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
<!-- for the interpolated colors -->
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<!-- for the polygon hull -->
<script src="https://d3js.org/d3-polygon.v1.min.js"></script>
<script type="text/javascript">
data = [
{"xName":"a", "yName":"a", "val": Math.random(), "meta":{"sex":"male", "age": Math.random() * 10}},
{"xName":"a", "yName":"b", "val": Math.random(), "meta":{"sex":"female", "age": Math.random() * 10}},
{"xName":"a", "yName":"c", "val": Math.random(), "meta":{"sex":"female", "age": Math.random() * 10}},
{"xName":"a", "yName":"d", "val": Math.random(), "meta":{"sex":"male", "age": Math.random() * 10}},
{"xName":"a", "yName":"e", "val": Math.random(), "meta":{"sex":"male", "age": Math.random() * 10}},
{"xName":"a", "yName":"f", "val": Math.random(), "meta":{"sex":"female", "age": Math.random() * 10}},
{"xName":"b", "yName":"a", "val": Math.random(), "meta":{"sex":"female", "age": Math.random() * 10}},
{"xName":"b", "yName":"b", "val": Math.random(), "meta":{"sex":"male", "age": Math.random() * 10}},
{"xName":"b", "yName":"c", "val": Math.random(), "meta":{"sex":"female", "age": Math.random() * 10}},
{"xName":"b", "yName":"d", "val": Math.random(), "meta":{"sex":"female", "age": Math.random() * 10}},
{"xName":"b", "yName":"e", "val": Math.random(), "meta":{"sex":"female", "age": Math.random() * 10}},
{"xName":"b", "yName":"f", "val": Math.random(), "meta":{"sex":"male", "age": Math.random() * 10}},
{"xName":"c", "yName":"a", "val": Math.random(), "meta":{"sex":"male", "age": Math.random() * 10}},
{"xName":"c", "yName":"b", "val": Math.random(), "meta":{"sex":"female", "age": Math.random() * 10}},
{"xName":"c", "yName":"c", "val": Math.random(), "meta":{"sex":"male", "age": Math.random() * 10}},
{"xName":"c", "yName":"d", "val": Math.random(), "meta":{"sex":"female", "age": Math.random() * 10}},
{"xName":"c", "yName":"e", "val": Math.random(), "meta":{"sex":"male", "age": Math.random() * 10}},
{"xName":"c", "yName":"f", "val": Math.random(), "meta":{"sex":"female", "age": Math.random() * 10}}
]
yDend = [
{"name": "b", "parent": ""},
{"name": "a", "parent": "b"},
{"name": "c", "parent": "b"}
]
xDend = [
{"name": "a", "parent": ""},
{"name": "c", "parent": "a"},
{"name": "b", "parent": "a"},
{"name": "e", "parent": "d"},
{"name": "d", "parent": "c"},
{"name": "f", "parent": "d"}
]
</script>
</head>
<body>
<svg id="heatmapSVG" style="border: 1px solid black;"></svg>
<svg id="barchartSVG" style="border: 1px solid black;"></svg>
</body>
<script type="text/javascript" src="./heatmap.js"></script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment