heatmap = function(matrix, options = {}){
const {
width: width = 200,
height: height = 200,
clusterCols: clusterCols = false,
clusterRows: clusterRows = false,
clusteringDistanceRows: clusteringDistanceRows = "euclidean",
clusteringDistanceCols: clusteringDistanceCols = "euclidean",
clusteringMethodCols: clusteringMethodCols = "complete",
clusteringMethodRows: clusteringMethodRows = "complete",
marginTop: marginTop = clusterCols ? 80 : 30,
marginLeft: marginLeft = clusterRows ? 120 : 30,
colPadding: colPadding = clusterCols ? 20 : 0,
rowPadding: rowPadding = clusterRows ? 20 : 0,
color: color = "red",
colorScale: colorScale = [0,100],
decimal: decimal = 2,
fontFamily: fontFamily= 'monospace'
} = options;
const margin = ({ top: marginTop,bottom:0,left:marginLeft,right:0})
const svg = d3.create("svg")
const data = matrix.data ? matrix.data: matrix
const colHclustTree = new hclust.agnes(dist(transpose(data), distance[clusteringDistanceCols]), {
method:clusteringMethodCols,
isDistanceMatrix: true})
const root = d3.hierarchy(colHclustTree)
const clusterLayout = d3.cluster()
clusterLayout(root)
const rowHclustTree2 = new hclust.agnes(dist(data, distance[clusteringDistanceRows]), {
method: clusteringMethodRows,
isDistanceMatrix: true})
const root2 = d3.hierarchy(rowHclustTree2)
const clusterLayout2 = d3.cluster()
clusterLayout2(root2)
let colIdx = clusterCols ? root.leaves().map(x=>x.data.index): d3.range(data[0].length)//col clust
let rowIdx = clusterRows ? root2.leaves().map(x=>x.data.index) : d3.range(data.length)//row clust
console.log("rowIdx",rowIdx)
const newMatrix2 = transpose(colIdx.map(i => transpose(rowIdx.map(e => data[e])) [i]))
// if labels (truncated length) are not provided, indices are used
let colNames2 = matrix.colNames ? trimText(colIdx, matrix.colNames) : Array.from(new Array(data[0].length),(x,i)=> i + 1)
let rowNames2 = matrix.colNames ? trimText(rowIdx, matrix.rowNames) : Array.from(new Array(data[0].length),(x,i)=> i + 1)
console.log("rowNames2",rowNames2)
// max x and y label lengths to be used in dendogram heights
const colNames2Lengths = d3.max(colNames2.map(e => e.length))
const rowNames2Lengths = d3.max(rowNames2.map(e => e.length))
const color_scale = d3.scaleLinear()
.domain(colorScale)
.range(['#fff', `${color}`])
let x_scale = d3.scaleBand()
.domain(colNames2)
.range([0, width-margin.left-margin.right])
let y_scale = d3.scaleBand()
.domain(rowNames2)
.range([ 0, height-margin.top])
const g = svg
.attr('width', width )
.attr('height', height )
.append('g')
// move the entire graph down and right to accomodate labels
.attr('transform', `translate(${margin.left+margin.right}, ${margin.top+margin.bottom})`)
//text x axis
const xAxis = g.append('g')
.call(d3.axisTop(x_scale))
xAxis.selectAll('.tick').selectAll('line').remove()
xAxis.selectAll("text")
.style("text-anchor", "start")
.attr("dx", "2px")
.attr("dy", "1.1em")
.attr("transform", "rotate(-90)")
.attr("class", "xa")
//text y axis
let yAxis = g.append('g')
.call(d3.axisLeft(y_scale))
.attr("id", "ya")
yAxis.selectAll('.tick').selectAll('line').remove()
yAxis.selectAll("text")
.attr("dx", "7px")
.attr("dy", "0.3em")
.attr("class", "yaa")
const gPoints = g.append("g").attr("class", "gPoints");
const tooltip = d3tip()
.style('border', 'solid 3px black')
.style('background-color', 'white')
.style('border-radius', '10px')
.style('float', 'left')
.style('font-family', fontFamily)
.html((event, d) => `
<div style='float: right'>
value:${d.value.toFixed(decimal)} <br/>
row:${rowNames2[d.n]}, col:${colNames2[d.t] }
</div>`)
// Apply tooltip to our SVG
svg.call(tooltip)
gPoints.selectAll()
.data(buildData(newMatrix2))
.enter()
.append('rect')
.attr('x', (d) => x_scale(colNames2[d.t]))
.attr('y', (d) => y_scale(rowNames2[d.n]))
.attr('width', width/data[0].length)
.attr('height', height/data.length)
.attr('fill', (d) => color_scale(d.value))
.on('mouseover', tooltip.show)
.on('mouseout', tooltip.hide)
// Top dendogram---------------------------------
if (clusterCols== true){
// console.log(root.links())
const colMaxHeight = root.data.height;
const allNodes = root.descendants().reverse()
const leafs = allNodes.filter(d => !d.children)
leafs.sort((a,b) => a.x - b.x)
const leafHeight = (width-margin.left)/ leafs.length // spacing between leaves
leafs.forEach((d,i) => d.x = i*leafHeight + leafHeight/2)
allNodes.forEach(node => {
if (node.children) {
node.x = d3.mean(node.children, d => d.x)
}})
root.links().forEach((link,i) => {
svg
.append("path")
.attr("class", "link")
.attr("stroke", link.source.color || "blue")
.attr("stroke-width", `${5}px`)
.attr("fill", 'none')
.attr("transform", `translate(${margin.left},7)`)
.attr("d", colElbow(link,colMaxHeight,margin.top,colNames2Lengths))
})
}
// bottom dendogram----------------------
if (clusterRows== true){
const dendoTooltip = d3tip()
.style('border', 'solid 3px black')
.style('background-color', 'white')
.style('border-radius', '10px')
.style('float', 'left')
.style('font-family', 'monospace')
.html((event, d) => `
<div style='float: right'>
Height:${d.source.data.height.toFixed(3)} <br/>
</div>`)
const rowMaxHeight = root2.data.height;
const clusterLayout2 = d3.cluster()
clusterLayout2(root2)
const allNodes2 = root2.descendants().reverse()
const leafs2 = allNodes2.filter(d => !d.children)
leafs2.sort((a,b) => a.x - b.x)
const leafHeight2 = (height-margin.top)/ leafs2.length
leafs2.forEach((d,i) => d.x = i*leafHeight2 + leafHeight2/2)
allNodes2.forEach(node => {
if (node.children) {
node.x = d3.mean(node.children, d => d.x)
}})
// Apply tooltip to our SVG
svg.call(dendoTooltip)
// console.log(root2.links())
root2.links().forEach((link,i) => {
svg
.append("path")
.attr("class", "link")
.attr("stroke", link.source.color || "red")
.attr("stroke-width", `${5}px`)
.attr("fill", 'none')
.attr("transform", `translate(7,${margin.top})`)
.attr("d", rowElbow(link,rowMaxHeight,margin.left,rowNames2Lengths))
.on('mouseover', dendoTooltip.show)
// Hide the tooltip when "mouseout"
.on('mouseout', dendoTooltip.hide)
})
svg.selectAll('path')
.data(root2.links())
}
return svg.node()
}