<!DOCTYPE html> <meta charset="utf-8"> <head> <script src="https://d3js.org/d3.v5.min.js"></script> <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"> </script> <script> $('html').click(function(e) { $('.genecontext-menu').css('opacity', 0).css('z-index', '-1'); $('.brushcontext-menu').css('opacity', 0).css('z-index', '-1'); }); </script> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.0/css/bootstrap.min.css" integrity="sha384-PDle/QlgIONtM1aqA2Qemk5gPOE7wFq8+Em+G/hmo5Iq0CCmYZLv3fVRDJ4MMwEA" crossorigin="anonymous"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.0/css/bootstrap.min.css" integrity="sha384-PDle/QlgIONtM1aqA2Qemk5gPOE7wFq8+Em+G/hmo5Iq0CCmYZLv3fVRDJ4MMwEA" crossorigin="anonymous"> <style> #oncoprint { width: 1100px; margin: auto; } .genecontext-menu:hover { background-color: #bfbfbf !important; color: #333 !important; border: 1px #333 solid; } .brushcontext-menu li:hover { background-color: #bfbfbf !important; color: #333 !important; border: 1px #333 solid; } .brushcontext-menu ul { list-style: none; margin-bottom: 0px; padding: 0px; } .brushcontext-menu li { padding: 0px 5px; } .outer-holder .brushed. .selection { opacity: 0.5; } .tooltip:after { right: 100%; top: 25%; border: solid transparent; content: " "; height: 0; width: 0; position: absolute; pointer-events: none; border-color: rgba(255, 255, 255, 0); border-right-color: rgba(51, 51, 51, 0.9); border-width: 10px; margin-top: -10px; } .tooltip .lab { padding-right:0.6em; text-align:right; } body { width: 100%; min-height: 100%; } </style> </head> <body> <!-- Create a div where the graph will take place --> <div id="oncoprint"></div> <script> // set the dimensions and margins of the graph var origmargin = {top: 10, right: 10, bottom: 10, left: 10}, width = 1000 - origmargin.left - origmargin.right, height = 250 - origmargin.top - origmargin.bottom; // set the dimensions and margins of the graph var margin = {top: 5, right: 285, bottom: 30, left: 40}, newwidth = width - margin.left - margin.right, newheight = height - margin.top - margin.bottom; // the colors in the legend and the barplot are pulled from here //// this could (should) come from the data var alterations = [ {name:"High level amplification", color:"#8B0000", type:"Amp"}, {name:"Low level gain", color:"#ff5733", type:"Gain"}, {name:"Shallow deletion", color:"#00b8ff", type:"HetLoss"}, {name:"Deep deletion", color:"#00008B", type:"HomDel"}, {name:"Mutation", color:"#169e35", type:"Mutation"}, {name:"Fusion", color:"#ffc125", type:"Fusion"} ]; // append the svg object to the body of the page var svg = d3.select("#oncoprint") .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); //Read the data d3.csv("https://raw.githubusercontent.com/jonkatz2/jonkatz2.github.io/master/assets/blog/oncoprint/genedata2.csv").then(function(data) { var g = svg.append("g") .attr("class", "outer-holder") .attr("transform", "translate(" + margin.left + "," + (margin.top+45) + ")"); // get unique values of models and genes var myGroups = d3.map(data, function(d){return d.group;}).keys() var myVars = d3.map(data, function(d){return d.variable;}).keys() // use a fixed bar height until the box is full, then compress it newheight = Math.min(newheight, 31 * myVars.length); var plotArea = g.append("g") .attr("clip-path", "url(#oncoprintAreaClip)"); // update: set width and height of clippath rect plotArea.append("clipPath") .attr("id", "oncoprintAreaClip") .append("rect") .attr('width', newwidth) .attr('height', newheight); // Build X scales and axis: var x = d3.scaleLinear() .range([ 0, newwidth ]) .domain([0, myGroups.length]); // Build Y scales and axis: var y = d3.scaleBand() .range([ newheight, 0 ]) .domain(myVars) .padding(0.05); g.append("g") .attr("class", "y1-axis") .style("font-size", "1em") .call(d3.axisLeft(y).tickSize(0)) .select(".domain").remove(); // create a context menu for the gene right-click d3.select("body").selectAll(".genecontext-menu").remove(); var genemenu = d3.select("body") .append("div") .style("opacity", 0) .attr("class", "genecontext-menu") .attr("data-toggle", "modal") .attr("data-target", "#lollipopmodal") .style("background-color", "rgb(51, 51, 51, 0.9)") .style("color", "#FFF") .style("padding", "5px") .style("position", "absolute") .style("z-index", "-1"); // show the gene context menu on right-click and feed it data var showgenecontext = function(d) { genemenu .html("Lollipop plot for "+ d) .style("opacity", 1) .style("z-index", "10000") .style("left", (d3.event.pageX + 15) + "px") .style("top", (d3.event.pageY - 15) + "px") // .on("click", function (i) { // Shiny.setInputValue("mutation-lolliplot_gene", d, {priority: "event"}); // }) } // this selection group contains model names selected by brushing var selgrp = []; // create a context menu for the brush right-click d3.select("body").selectAll(".brushcontext-menu").remove(); var brushmenu = d3.select("body") .append("div") .style("opacity", 0) .attr("class", "brushcontext-menu") .style("background-color", "rgb(51, 51, 51, 0.9)") .style("color", "#FFF") // .style("padding", "5px") .style("position", "absolute") .style("z-index", "-1"); // the ul container var brushbuttons = brushmenu.append("ul") .style("list-style", "none") .style("margin-bottom", "0px") .style("padding", "0px") var brushlist = brushbuttons.selectAll("li") .data(["Add selection to cart", "Replace cart with selection"]) brushlist.exit().remove() brushlist.enter().append("li") .html(function(d) {return d;}) .style("padding", "3px 5px") .attr("class", function(d, i) { return "brushitem"+i }) // show the brush context menu on right-click var showbrushcontext = function(d) { // turn off any prior click events $(".brushitem0").off("click"); $(".brushitem1").off("click"); // un-hide the brush menu brushmenu .style("opacity", 1) .style("z-index", "10000") .style("left", (d3.event.pageX + 15) + "px") .style("top", (d3.event.pageY - 15) + "px"); // // set the click event using jQuery // $(".brushitem1").click(function() { // Shiny.setInputValue("mutation-oncoprintselectionreplace", selgrp, {priority: "event"}) // }); // $(".brushitem0").click(function() { // Shiny.setInputValue("mutation-oncoprintselectionadd", selgrp, {priority: "event"}) // }); } // Add links to the gene/y-axis g.selectAll(".y1-axis").selectAll('.tick') .data(myVars) .attr("gene", function(d) { return d }) // .on('click', function (d) { // zoom.transform(oncoprint, d3.zoomIdentity); // Shiny.setInputValue("mutation-gene_priority", d, {priority: "event"}); // }) .on("contextmenu", function (d, i) { d3.event.preventDefault() showgenecontext(d) }) .exit(); // remove all tooltips d3.select("body").selectAll(".oncoprinttooltip").remove(); // create a tooltip var tooltip = d3.select("body") .append("div") .style("opacity", 0) .attr("class", "oncoprinttooltip") .style("background-color", "rgb(51, 51, 51, 0.9)") .style("color", "#FFF") .style("padding", "5px") .style("position", "absolute"); // Three functions that change the tooltip when user hover / move / leave a cell var mouseover = function(d) { tooltip .style("opacity", 1) .style("z-index", 1000) d3.select(this) .style("stroke", "black") .style("opacity", 1) } var mousemove = function(d) { if (d.mutation == "") {var mut = ""} else {var mut = "<tr><td class='lab' style='vertical-align:top;'>Mutation</td><td>" + d.mutation + "</td></tr>"} tooltip .html("<table><tr><td class='lab'>Model</td><td>" + d.group + "</td></tr><tr><td class='lab'>Gene</td><td>" + d.variable + "</td></tr><tr><td class='lab'>Alteration</td><td>" + d.value + "</td></tr>" + mut + "</table>") .style("left", (d3.event.pageX + 15) + "px") .style("top", (d3.event.pageY - 15) + "px") } var mouseleave = function(d) { tooltip .style("opacity", 0) .style("z-index", -1) d3.select(this) .style("stroke", "none") .style("opacity", 0.8) } // brush region under oncoprint, extends top and bottom for area to grab var brush = d3.brushX() .extent([[-10, -20], [newwidth+10, newheight+20]]) .on("start", brushstart) .on("brush", brushing) .on("end", brushend); var brushgroup = g.append("g") .attr("class", "brushed") .on("contextmenu", function (d) { d3.event.preventDefault() showbrushcontext() }) .call(brush); // zoom region exactly matches the oncoprint area var zoom = d3.zoom() .scaleExtent([1, Infinity]) .translateExtent([[0, 0], [newwidth, newheight]]) .extent([[0, 0], [newwidth, newheight]]) .on("zoom", zooming) // .on("end", zoomend); // record the alterations per gene as we draw the rectangles var altcount = {}; myVars.forEach(function(d) { altcount = {...altcount, [d]:{Amp:0,Gain:0,HetLoss:0,HomDel:0,Fusion:0,Mutation:0}}; }); // record the samples with alterations per gene as we draw the rectangles var samplecount = {}; myVars.forEach(function(d) { samplecount = {...samplecount, [d]:[]}; }); var oncoprint = g.append("g") .attr("class", "oncoprint") .call(zoom); // .on("dblclick.zoom", null) // .call(brush); // add the primary squares var alterationrect = oncoprint.selectAll() .data(data) .enter() .append("rect") .attr("class", "cna") .attr("x", function(d) { return x(myGroups.indexOf(d.group)); }) .attr("y", function(d) { return y(d.variable) }) .attr("width", function(d) { return (x(myGroups.indexOf(d.group)+0.9) - x(myGroups.indexOf(d.group))); }) .attr("height", y.bandwidth() ) .style("fill", function(d) { if (d.value == "Amp") {altcount[d.variable].Amp += 1; samplecount[d.variable].push(d.group); return "#8B0000"} else if (d.value == "Gain" ) {altcount[d.variable].Gain += 1; samplecount[d.variable].push(d.group); return "#ff5733"} else if (d.value == "HetLoss") {altcount[d.variable].HetLoss += 1; samplecount[d.variable].push(d.group); return "#00b8ff"} else if (d.value == "HomDel") {altcount[d.variable].HomDel += 1; samplecount[d.variable].push(d.group); return "#00008B"} else { return "#D3D3D3" } }) .style("stroke-width", 4) .style("stroke", "none") .style("opacity", 0.8) .on("mouseover", mouseover) .on("mousemove", mousemove) .on("mouseleave", mouseleave) .exit() // add the mutation squares var mutationrect = oncoprint.selectAll() .data(data) .enter() .append("rect") .attr("class", "mut") .attr("x", function(d) { return x(myGroups.indexOf(d.group)); }) .attr("y", function(d) { return y(d.variable) }) .attr("width", function(d) { return (x(myGroups.indexOf(d.group)+0.9) - x(myGroups.indexOf(d.group))); }) .attr("height", function(d) { if (d.mutation == "") { return 0 } else { return y.bandwidth()/3 } }) .attr("transform", "translate(0," + y.bandwidth()/3 + ")") .style("fill", function(d) { if (d.mutation == "") {return "#transparent"} else if (d.mutation.match(/Fusion/) != null) {altcount[d.variable].Fusion += 1; samplecount[d.variable].push(d.group); return "#ffc125"} else { altcount[d.variable].Mutation += 1; samplecount[d.variable].push(d.group); return "#169e35" } }) .on("mouseover", mouseover) .on("mousemove", mousemove) .on("mouseleave", mouseleave) .exit() // this brush fn before any zooming function brushing() { selgrp = []; if (d3.event.sourceEvent.type === "brush") return; var selection = d3.event.selection; if (selection == null) { selgrp = []; } else { var x0 = Math.round(x.invert(selection[0])), x1 = Math.round(x.invert(selection[1])); // snap to boundaries d3.select(this).call(d3.event.target.move, [x(x0), x(x1+0.9)]); myGroups.forEach(function (d, i) { if (x0 <= i && i <= x1) selgrp.push(d); }); } } // clear the selectd models before the next selection function brushstart() { selgrp = []; } function zooming() { if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush var rescaleX = d3.event.transform.rescaleX(x); // confine the brush selection on zoom brush = d3.brushX() .extent([[-10, -20], [newwidth+10, newheight+20]]) .on("start", brushstart) .on("brush", brushing) .on("end", brushend); brushgroup.call(brush); // this brush fn is used after zooming function brushing() { selgrp = []; if (d3.event.sourceEvent.type === "brush") return; var selection = d3.event.selection; if (selection == null) { selgrp = []; } else { var x0 = Math.round(rescaleX.invert(selection[0])), x1 = Math.round(rescaleX.invert(selection[1])); // snap to boundaries d3.select(this).call(d3.event.target.move, [rescaleX(x0), rescaleX(x1+0.9)]); myGroups.forEach(function (d, i) { if (x0 <= i && i <= x1) selgrp.push(d); }); } } // maintain brush position on zoom g.select(".brushed").call(brush.move, function(d) { if(selgrp.length) { var selection = d3.event.selection; var x0 = rescaleX(myGroups.indexOf(selgrp[0])), x1 = rescaleX(myGroups.indexOf(selgrp[selgrp.length-1])); x0 = Math.max(x0, 0); x1 = Math.min(x1, newwidth); // snap to boundaries return [x0, x1]; } }); // redraw rectangles on zoom oncoprint.selectAll("rect.cna") .attr('clip-path', 'url(#oncoprintAreaClip)') .attr("x",function(d){ return rescaleX(myGroups.indexOf(d.group)); }) .attr("y",function(d){ return y(d.variable); }) .attr('width', function(d) { return (rescaleX(myGroups.indexOf(d.group)+0.9)-rescaleX(myGroups.indexOf(d.group))); }) .attr("height", function(d) { return y.bandwidth(); }) .style("fill", function(d) { if (d.value == "Amp") {altcount[d.variable].Amp += 1; return "#8B0000"} else if (d.value == "Gain" ) {altcount[d.variable].Gain += 1; return "#ff5733"} else if (d.value == "HetLoss") {altcount[d.variable].HetLoss += 1; return "#00b8ff"} else if (d.value == "HomDel") {altcount[d.variable].HomDel += 1; return "#00008B"} else { return "#D3D3D3" } }) oncoprint.selectAll("rect.mut") .attr('clip-path', 'url(#oncoprintAreaClip)') .attr("x",function(d){ return rescaleX(myGroups.indexOf(d.group)); }) .attr("y",function(d){ return y(d.variable); }) .attr('width', function(d) { return (rescaleX(myGroups.indexOf(d.group)+0.9)-rescaleX(myGroups.indexOf(d.group))); }) .attr("height", function(d) { if (d.mutation == "") { return 0 } else { return y.bandwidth()/3 } }) .attr("transform", "translate(0," + y.bandwidth()/3 + ")") .style("fill", function(d) { if (d.mutation == "") {return "#transparent"} else if (d.mutation.match(/Fusion/) != null) {altcount[d.variable].Fusion += 1; return "#ffc125"} else { altcount[d.variable].Mutation += 1; return "#169e35" } }) // zoom status svg.select(".zoom-status").remove() svg.append("text") .attr("class", "zoom-status") .attr("x", 25) .attr("y", 25) .attr("text-anchor", "left") .style("font-size", "0.9em") .style("fill", "#333;") .style("max-width", newwidth/2) .text("Zoom: " + Math.round(rescaleX(myGroups.length-1)/newwidth*10)/10 + "x"); } // send the brushed selection to the DOM element on zoomend -- function brushend() { if (selgrp.length) { // don't resend on zoom if (d3.event.sourceEvent.type == "zoom") return; // Shiny.setInputValue("mutation-brush_selection", selgrp, {priority: "event"}); } } // the stacked barplot of total alterations by type // // tooltip shows the count of the color-within-bar on hover var barmousemove = function(d) { tooltip .html((d[1]-d[0])) .style("left", (d3.event.pageX + 15) + "px") .style("top", (d3.event.pageY - 15) + "px") } // convert the alteration counter to an array var altmat = [] myVars.forEach(function(d) { var temp = altcount[d] temp["name"] = d; altmat.push(temp); }); // make a barplot area to the right of the percentages var barplot = g.append("g") .attr("transform", "translate(" + (newwidth+35) + ",0)") .attr("class", "barplot") // stack bars var barstack = d3.stack() .keys(["Amp", "Gain", "HetLoss", "HomDel", "Mutation", "Fusion"]) .order(d3.stackOrderNone) .offset(d3.stackOffsetNone); // convert stacked bars to series-based array var barseries = barstack(altmat); // identify the highest alteration count in any gene var altctmax = d3.max(barseries, function(d) {return d3.max(d, function(j) { return +j[1];});}); //var altctmax = d3.max(d3.max(barseries), function(d) {return d[1];}); // Scale the barplot to fit in the alotted 65px var bary = d3.scaleLinear() .domain([0, altctmax]) .range([0, 65]); // add an axis barplot.append("g") .attr("transform", "translate(0,-1)") .style("font-size", 6) .call(d3.axisTop(bary).ticks(3).tickSize(3).tickValues([0, Math.round(altctmax/2), altctmax])) .select(".domain").remove() // Draw the barplot rectangles barplot.selectAll() .data(barseries) .enter().append("g") .attr("class", "serie") .attr("fill", function(d, i) { return alterations[i].color; }) .selectAll() .data(function(d) { return d; }) .enter().append("rect") .attr("x", function(d) { return bary(d[0]); }) .attr("y", function(d, i) { return y(myVars[i]); }) .attr("width", function(d) { return bary((d[1] - d[0])); }) .attr("height", y.bandwidth()) .on("mouseover", mouseover) .on("mousemove", barmousemove) .on("mouseleave", mouseleave); // plot title svg.select(".title").remove() svg.append("text") .attr("class", "title") .attr("x", newwidth/2) .attr("y", 25) .attr("text-anchor", "left") .style("font-size", "1.1em") .style("fill", "#333;") .style("max-width", width) .text(myGroups.length + " Samples"); // the second y axis is not a real axis because the values may not be unique var y2 = g.append("g") .attr("transform", "translate(" + newwidth + ",0)") .attr("class", "y2-axis") .style("font-size", "0.9em"); // labels y2.selectAll() .data(myVars) .enter() .append("text") .style("font-size", "0.7em") .attr("x", 3) .attr("y", function(d) { return y(d) + y.bandwidth()/2 + 2 }) .text(function(d) { return Math.round([...new Set(samplecount[d])].length/myGroups.length*100) + "%" }) .exit() // data out }) //// add a legend // group the legend svg.selectAll(".legend").remove() var legend = svg.append("g") .attr("transform", "translate(" + (newwidth+margin.left+70+45) + "," + (margin.top+45) + ")") .attr("class", "legend") // legend title legend.append("text") .attr("x", 0) .attr("y", 10) .style("font-size", "0.9em") .text("Alterations") .exit(); // colored rects legend.selectAll() .data(alterations) .enter() .append("rect") .attr("x", 0) .attr("y", function(d, i) { return (16 * i + 20) }) .attr("width", 8 ) .attr("height", 14 ) .style("fill", function(d) { return d.color }) .exit(); // labels legend.selectAll() .data(alterations) .enter() .append("text") .style("font-size", "0.8em") .attr("x", 16) .attr("y", function(d, i) { return (16 * i + 31) }) .text(function(d) { return d.name }) .exit() </script> </body>