createHeatmap = async function (dataInput, clinicalData, divObject) {
let myGroups = d3.map(dataInput, d => d.tcga_participant_barcode).keys();
let myVars = d3.map(dataInput, d => d.gene).keys();
const cohortIDs = d3.map(dataInput, d => d.cohort).keys();
var unique_ids = d3.map(dataInput, d => d.tcga_participant_barcode).keys();
var data_merge = mergeExpression(dataInput);
var doCluster = false, clusterReady = false, clust_results, sortOrder;
function sortGroups() {
if (doCluster && !clusterReady) {
clust_results = hclust.clusterData({ data: data_merge, key: 'exps' });
sortOrder = clust_results.order;
clusterReady = true;
} else if (doCluster && clusterReady) {
sortOrder = clust_results.order;
}
else {
const ngene = data_merge[0].genes.length;
const means = data_merge.map(el => (el.exps.reduce((acc, val) => acc + val, 0)) / ngene);
sortOrder = new Array(data_merge.length);
for (var i = 0; i < data_merge.length; ++i) sortOrder[i] = i;
sortOrder.sort((a, b) => { return means[a] > means[b] ? -1 : 1; });
}
myGroups = unique_ids;
myGroups = sortOrder.map(i => myGroups[i]);
};
sortGroups();
var margin = { top: 80, right: 30, space: 5, bottom: 30, left: 100 },
frameWidth = 950,
heatWidth = frameWidth - margin.left - margin.right,
legendWidth = 50,
heatHeight = 300,
sampTrackHeight = 25,
dendHeight = Math.round(heatHeight / 2),
frameHeight = margin.top + heatHeight + margin.space + dendHeight + margin.bottom;
var svg_frame = divObject.append("svg")
.attr('width', frameWidth)
.attr('height', frameHeight);
svg_frame.append("text")
.attr('id', 'heatmapTitle')
.attr("x", margin.left)
.attr("y", margin.top - 25)
.style("font-size", "26px")
.text("Gene Expression Heatmap for " + cohortIDs.join(' and '));
var svg_dendrogram = svg_frame
.append("svg")
.attr("class", "dendrogram")
.attr("width", heatWidth)
.attr("height", dendHeight)
.attr("x", margin.left)
.attr("y", margin.top);
var svg_sampletrack = svg_frame
.append("svg")
.attr("class", "sampletrack")
.attr("width", frameWidth)
.attr("height", margin.space + sampTrackHeight)
.attr("y", margin.top + dendHeight + margin.space)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.space + ")");
var svg_heatmap = svg_frame
.append("svg")
.attr("class", "heatmap")
.attr("width", frameWidth)
.attr("height", heatHeight + margin.space + margin.bottom)
.attr("y", margin.top + dendHeight + margin.space + sampTrackHeight + margin.space)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.space + ")");
var tooltip = divObject
.append("div")
.style("opacity", 0)
.attr("class", "tooltip")
.style("border", "solid")
.style("border-width", "2px")
.style("border-radius", "5px")
.style("padding", "5px")
.style('width', frameWidth + 'px');
var div_sampLegend = divObject
.append("div")
.attr("class", "legend")
.style("border", "solid")
.style("border-width", "2px")
.style("border-radius", "5px")
.style("padding", "5px")
.style('width', frameWidth + 'px');
div_sampLegend
.append("text")
.style("font-size", "18px")
.text("Clinical Feature Sample Tracks Legend:");
var svg_sampLegend = div_sampLegend
.append("div")
.attr('id', 'legend')
.attr('class', 'viewport')
.style('overflow', 'scroll')
.append("svg")
.attr("class", "sampLegend")
.attr("width", frameWidth)
.attr("height", sampTrackHeight + 2 * margin.space)
.append("g")
.attr("transform", "translate(" + margin.space + "," + margin.space + ")");
var sortOptionDiv = divObject
.append('div')
.text('Sort options: ');
var sortCurrentText = sortOptionDiv
.append('tspan')
.text('mean expression (default)');
var sortOptionTable = sortOptionDiv.append('td');
sortOptionTable
.append('tspan')
.text('hclust\xa0')
.append('input')
.attr('type', 'checkbox')
.style('opacity', 1)
.style('pointer-events', 'auto')
.on('change', function () {
sortCurrentText.text(this.checked ? 'hierarchical clustering' : 'mean expression (default)')
doCluster = (this.checked ? true : false)
});
sortOptionDiv.append('button')
.attr('type', 'button')
.attr('id', 'updateHeatmapButton')
.text('Update heatmap');
let x = d3.scaleBand()
.range([0, heatWidth - legendWidth])
.domain(myGroups);
let y = d3.scaleBand()
.range([0, heatHeight])
.domain(myVars);
let minZ = -2,
maxZ = 2;
let zScale = d3.scaleLinear().domain([minZ, maxZ]).range([heatHeight, 0]);
let zArr = [];
let step = (maxZ - minZ) / (1000 - 1);
for (var i = 0; i < 1000; i++) {
zArr.push(minZ + (step * i));
};
let interpCol_exp = d3.interpolateRgbBasis(["blue", "white", "red"])
let colorScale_exp = d3.scaleSequential()
.interpolator(interpCol_exp)
.domain([minZ, maxZ]);
var cluster, data;
var root = { data: { height: [] } };
function elbow(d) {
const scale = dendHeight / root.data.height;
return "M" + d.parent.x + "," + (dendHeight - d.parent.data.height * scale) + "H" + d.x + "V" + (dendHeight - d.data.height * scale);
};
let mouseover = function (d) {
tooltip.style("opacity", 1);
d3.select(this).style("fill", "black");
let id_ind = unique_ids.indexOf(d.tcga_participant_barcode);
svg_dendrogram.selectAll('path')
.filter(d => d.data.indexes.includes(id_ind))
.style("stroke-width", "2px");
};
const spacing = "\xa0\xa0\xa0\xa0|\xa0\xa0\xa0\xa0";
let mousemove = function (d) {
tooltip.html("\xa0\xa0" +
"Cohort: " + d.cohort + spacing +
"TCGA Participant Barcode: " + d.tcga_participant_barcode + spacing +
"Gene: " + d.gene + spacing +
"Expression Level (log2): " + d.expression_log2.toFixed(5) + spacing +
"Expression Z-Score: " + d["z-score"].toFixed(5))
};
let mouseleave = function (d) {
tooltip.style("opacity", 0);
d3.select(this).style("fill", d => colorScale_exp(d["z-score"]));
let id_ind = unique_ids.indexOf(d.tcga_participant_barcode);
svg_dendrogram.selectAll('path')
.filter(d => d.data.indexes.includes(id_ind))
.style("stroke-width", "0.5px");
};
let mousemove_samp = function (d) {
let v = d3.select(this).attr("var")
tooltip.html("\xa0\xa0" +
"Cohort: " + d.cohort + spacing +
"TCGA Participant Barcode: " + d.tcga_participant_barcode + spacing +
v + ": " + d[v])
}
let mouseleave_samp = function (d) {
tooltip.style("opacity", 0);
d3.select(this).style("fill", d3.select(this).attr("fill0"))
let id_ind = unique_ids.indexOf(d.tcga_participant_barcode);
svg_dendrogram.selectAll('path')
.filter(d => d.data.indexes.includes(id_ind))
.style("stroke-width", "0.5px");
};
svg_heatmap.append("g")
.style("font-size", 9.5)
.call(d3.axisLeft(y).tickSize(0))
.select(".domain").remove();
svg_heatmap.selectAll()
.data(zArr)
.enter()
.append('rect')
.attr('x', heatWidth - margin.right)
.attr('y', d => zScale(d))
.attr("width", legendWidth / 2)
.attr("height", 1 + (heatHeight / zArr.length))
.style("fill", d => colorScale_exp(d));
svg_heatmap.append("g")
.style("font-size", 10)
.attr("transform", "translate(" + heatWidth + ",0)")
.call(d3.axisRight().scale(zScale).tickSize(5).ticks(5));
function updateHeatmap() {
x = x.domain(myGroups);
svg_heatmap.selectAll()
.data(dataInput, d => (d.tcga_participant_barcode + ':' + d.gene))
.enter()
.append("rect")
.attr("x", d => x(d.tcga_participant_barcode))
.attr("y", d => y(d.gene))
.attr("width", x.bandwidth())
.attr("height", y.bandwidth())
.style("fill", d => colorScale_exp(d["z-score"]))
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseleave", mouseleave);
let sampTrackVars = getClinvarSelection();
let colorScale_all = sampTrackVars.reduce((acc, v) => {
let var_domain = d3.map(clinicalData, d => d[v]).keys().sort().filter(el => el !== "NA");
acc[v] = d3.scaleOrdinal()
.domain(var_domain)
.range(d3.schemeCategory10)
.unknown("lightgray"); return acc
}, {})
let sampTrackHeight_total = (sampTrackHeight + margin.space) * sampTrackVars.length;
svg_frame.select('.sampletrack').attr('height', sampTrackHeight_total)
let y_samp = d3.scaleBand()
.range([0, sampTrackHeight_total])
.domain(sampTrackVars);
svg_sampletrack.html("");
sampTrackVars.forEach(v => {
svg_sampletrack.selectAll()
.data(clinicalData, d => (d.tcga_participant_barcode + ":" + v))
.enter()
.append("rect")
.attr("var", v)
.attr("x", d => x(d.tcga_participant_barcode))
.attr("y", y_samp(v))
.attr("width", x.bandwidth())
.attr("height", sampTrackHeight)
.style("fill", d => colorScale_all[v](d[v]))
.attr("fill0", d => colorScale_all[v](d[v]))
.on("mouseover", mouseover)
.on("mousemove", mousemove_samp)
.on("mouseleave", mouseleave_samp);
})
svg_sampletrack.select('#sampLabels').remove();
svg_sampletrack.append("g")
.attr('id', 'sampLabels')
.style('font-size', 9.5)
.call(d3.axisLeft(y_samp).tickSize(0))
.select(".domain").remove();
let svg_temp = divObject.append("svg")
function getTextWidth(str, fs) {
let text_temp = svg_temp
.append('text')
.style('font-size', fs + "px")
.text(str);
var dim = text_temp.node().getBBox();
return dim.width
}
let var_summary = sampTrackVars.map(v => {
let myLabs = d3.map(clinicalData, d => d[v]).keys().sort().filter(el => el !== "NA").map(el => ({ val: el }));
let var_width = getTextWidth(v + ":\xa0", 15);
let lab_width = Math.max(...myLabs.map(el => getTextWidth("\xa0" + el.val, 10)));
return { var: v, labs: myLabs, nlab: myLabs.length, max_width: Math.ceil(Math.max(lab_width + sampTrackHeight, var_width)) }
})
svg_temp.html("")
const cumulativeSum = (sum => value => sum += value)(0);
let x_spacing = var_summary.map(el => el.max_width + margin.space).map(cumulativeSum);
var_summary = var_summary.map(o => { o.x = x_spacing[var_summary.indexOf(o)] - o.max_width; return o });
svg_sampLegend.html("");
var_summary.forEach(v => {
svg_sampLegend
.append("text")
.attr("x", v.x)
.attr("alignment-baseline", "hanging")
.style("font-size", "15px")
.attr("text-decoration", "underline")
.text(v.var + ":");
svg_sampLegend.selectAll()
.data(v.labs, d => v.var + ":" + d.val + "_box")
.enter()
.append("rect")
.attr("x", v.x)
.attr("y", (d, i) => 20 + i * (sampTrackHeight + margin.space))
.attr("width", sampTrackHeight)
.attr("height", sampTrackHeight)
.style("fill", d => colorScale_all[v.var](d.val))
.style("stroke", "black");
svg_sampLegend.selectAll()
.data(v.labs, d => v.var + ":" + d.val + "_text")
.enter()
.append("text")
.attr("x", v.x + sampTrackHeight)
.attr("y", (d, i) => 20 + i * (sampTrackHeight + margin.space) + sampTrackHeight / 2)
.attr("alignment-baseline", "central")
.style("font-size", "10px")
.text(d => "\xa0" + d.val);
});
let sampLegendHeight = 20 + (sampTrackHeight + margin.space) * Math.max(...var_summary.map(el => el.nlab));
div_sampLegend.select(".sampLegend")
.attr("height", sampLegendHeight + margin.space)
.attr("width", var_summary.reduce((a, b) => a + b.max_width + sampTrackHeight + margin.space, 0))
if (sampLegendHeight < 200) {
div_sampLegend.select('#legend')
.attr('height', sampLegendHeight + 'px')
} else {
div_sampLegend.select('#legend')
.style('height', '200px')
}
if (sampTrackVars.length == 0) {
svg_sampLegend
.append("text")
.attr("alignment-baseline", "hanging")
.style("font-size", "18px")
.text("No clinical features selected");
div_sampLegend.select(".sampLegend")
.attr("height", 20)
.attr("width", 250)
div_sampLegend.select('#legend')
.style('height', '20px')
};
if (doCluster && clusterReady) {
cluster = d3.cluster().size([heatWidth - legendWidth, dendHeight]);
data = clust_results.clusters;
root = d3.hierarchy(data);
cluster(root);
svg_dendrogram.selectAll('path')
.data(root.descendants().slice(1))
.enter()
.append('path')
.attr("d", elbow)
.style("fill", 'none')
.style("stroke-width", "0.5px")
.attr("stroke", 'black')
svg_dendrogram.attr("height", dendHeight);
svg_frame.select(".sampletrack")
.attr("y", margin.top + dendHeight)
svg_frame.select(".heatmap")
.attr("y", margin.top + dendHeight + sampTrackHeight_total);
frameHeight = margin.top + dendHeight + margin.space + heatHeight + sampTrackHeight_total + margin.bottom;
} else {
svg_dendrogram.attr("height", 0);
svg_frame.select(".sampletrack")
.attr("y", margin.top)
svg_frame.select(".heatmap")
.attr("y", margin.top + sampTrackHeight_total);
frameHeight = margin.top + heatHeight + sampTrackHeight_total + margin.bottom;
}
svg_frame.attr('height', frameHeight)
}
updateHeatmap()
sortOptionDiv.select('#updateHeatmapButton')
.on('click', function () {
sortGroups();
updateHeatmap();
})
};