function chainChart(data) {
const length = _.reduce(data, (sum, n) => sum + n.dat.length / 50, 0);
const svgHeight = height * (length + 2.2);
const svg = d3.select(DOM.svg(width, svgHeight));
const scores = _.flatMap(data, (d) => _.map(d.dat, (d) => d.score));
const z = d3.scaleSequential(d3.interpolateOrRd).domain([0, d3.max(scores)]);
let accHeight = 0;
data.forEach((data, idx) => {
const dat = _.chunk(data.dat, 50);
dat.forEach((chunk, index) => {
const residues = chunk.map((d) => d.residue);
const x = d3
.scaleBand()
.domain(residues)
.range([margin.left, 18 * chunk.length])
.padding(0.1);
const y = d3
.scaleBand()
.domain([data.name])
.range([margin.top, height - margin.bottom])
.padding(0.1);
const xAxis = (g) =>
g
.attr("transform", `translate(0, ${accHeight + height * index + 50})`)
.call(d3.axisBottom(x).tickSizeOuter(0))
.call((g) => g.select(".domain").remove())
.selectAll("text")
.attr("y", 0)
.attr("x", -9)
.attr("dy", ".35em")
.attr("transform", "rotate(270)")
.style("text-anchor", "end")
.style("fill", "#444");
const yAxis = (g) =>
g
.attr("transform", `translate(${margin.left},${accHeight})`)
.call(d3.axisLeft(y).tickSize(0).tickPadding(4))
.call((g) => g.select(".domain").remove())
.selectAll("text")
.style("fill", "#444");
svg.append("g").call(xAxis);
if (index === 0) {
svg.append("g").call(yAxis);
}
const serie = svg
.append("g")
.selectAll("g")
.data([{ key: data.name, dat: chunk }])
.enter()
.append("g")
.attr(
"transform",
(d) => `translate(0,${y(d.key) + 1 + accHeight + height * index})`
);
const tip = d3tip()
.attr("class", "d3-tip")
.html((d, x, i) => x.residue + ": " + x.score.toFixed(2));
svg.call(tip);
function check(e, d) {
const prefix = d.residue.substring(0, 3);
// console.log(prefix);
const parent = e.target.parentElement.parentElement.parentElement;
// console.log(parent);
svg
.selectAll("rect")
.attr("stroke", (f) =>
prefix && f.residue && f.residue.startsWith(prefix)
? "black"
: undefined
);
}
serie
.append("g")
.selectAll("rect")
.data((d) => d.dat)
.enter()
.append("rect")
.attr("fill", (d) => z(d.score))
.attr("x", (d, i) => x(residues[i]))
.attr("y", 0)
.attr("height", y.bandwidth())
.attr("width", x.bandwidth())
.on("mouseover", tip.show)
.on("mouseout", tip.hide)
.on("click", (e, d) => check(e, d));
});
accHeight += height * dat.length;
});
// Legend
const legendBins = [...Array(9).keys()].map((x) =>
d3.quantile([0, d3.max(scores)], x * 0.1)
);
const legendElementWidth = 56;
const legendHeight = 20;
const legend = svg
.append("g")
.attr("transform", (d) => `translate(${margin.left}, ${svgHeight - 100})`);
legend
.selectAll("rect")
.data(legendBins)
.enter()
.append("rect")
.attr("x", (d, i) => legendElementWidth * i)
.attr("y", height - 2 * legendHeight)
.attr("width", legendElementWidth)
.attr("height", legendHeight)
.style("fill", (d) => z(d));
legend
.selectAll("text")
.data(legendBins)
.enter()
.append("text")
.text((d) => "≥ " + d.toFixed(1))
.attr("x", (d, i) => legendElementWidth * i)
.attr("y", height - legendHeight / 2)
.style("font-size", "9pt")
.style("font-family", "Consolas, courier")
.style("fill", "#aaa");
return svg.node();
}