diff --git a/.gitignore b/.gitignore index b9d6bd9..410bf65 100644 --- a/.gitignore +++ b/.gitignore @@ -213,3 +213,6 @@ pip-log.txt #Mr Developer .mr.developer.cfg + +#misc +js/ga.js diff --git a/PSFP.html b/PSFP.html new file mode 100644 index 0000000..1a0cdee --- /dev/null +++ b/PSFP.html @@ -0,0 +1,84 @@ + + + + Fluorescent protein properties + + + + + + + + + + + + + +

Photoswitchable Fluorescent Protein Properties

+
+
+ +
+ +
+ +
+ +
+ +
+
+

X Axis

+
+ + + + + + + + + +
+
+ +
+

Y Axis

+
+ + + + + + + + + +
+
+
+ +
+
+

Each fluorescent protein begins plotted with excitation wavelength on the x-axis and emission wavelength on the y-axis. The color is set + based on its emission wavelength, and fades to gray as the brightness (product of exctinction coefficient and quantum yield) decreases. + Mouseover each circle to see info on that protein or click on any datapoint to see the corresponding reference on PubMed. You can zoom using the mouse scroll wheel and pan by clicking and dragging. Use the filter sliders at the top right to select a subset of fluorescent proteins based on certain criteria. Use the X and Y axis toggle boxes to change what is plotted on each axis.

+
+

Bibliography

+
+ + + + + \ No newline at end of file diff --git a/PSFPs.csv b/PSFPs.csv new file mode 100644 index 0000000..ca5b238 --- /dev/null +++ b/PSFPs.csv @@ -0,0 +1,16 @@ +"UID","Name","type","lambda_ex","lambda_em","lambda_sw","E","QY","brightness","Aggregation","pka","DOI" +"kaede_1","Kaede","pc",508,518,,98800,0.88,86.9,"Tetramer",5.6, +"kaede_2","Kaede","pc",572,580,,60400,0.33,19.9,"Tetramer",5.6, +"kikgr1_1","KikGR1","pc",507,517,,53700,0.7,37.6,"Tetramer",7.8, +"kikgr1_2","KikGR1","pc",583,593,,35100,0.65,22.8,"Tetramer",5.5, +"pscfp2_1","PS-CFP2","pc",400,468,,43000,0.2,8.6,"Monomer",, +"pscfp2_2","PS-CFP2","pc",490,511,,47000,0.23,10.8,"Monomer",, +"meos2_1","mEos2","pc",506,519,,56000,0.84,47,"Monomer",5.6, +"meos2_2","mEos2","pc",573,584,,46000,0.66,30.4,"Monomer",6.4, +"meos3.2_1","Meos3.2","pc",507,516,,63400,0.7,53,"Monomer",5.4, +"meos3.2_2","Meos3.2","pc",572,580,,32200,0.55,18,"Monomer",5.8, +"psmorange_1","PSmOrange","pc",548,565,,113300,0.51,57.8,"Monomer",6.2,"10.1038/nmeth.1664 " +"psmorange_2","PSmOrange","pc",634,662,,32700,0.28,9.2,"Monomer",5.6,"10.1038/nmeth.1664 " +"PA-GFP","PA-GFP","pa",504,517,400,17400,0.79,13.7,"Monomer",,"10.1126/science.1074952 " +"PAmCherry1","PAmCherry1","pa",564,595,405,18000,0.46,8.3,"Monomer",6.3,"10.1038/nmeth.1298 " +"PATagRFP","PATagRFP","pa",562,595,405,66000,0.38,25.1,"Monomer",5.3,"10.1021/ja100906g " diff --git a/css/style.css b/css/style.css index a2610d8..2970bae 100644 --- a/css/style.css +++ b/css/style.css @@ -7,6 +7,16 @@ body { } /* Graph Styles */ +.FP, .PSFP +{ + shape-rendering: geometricPrecision; + opacity: 0.7; +} + +.PSFP +{ + stroke-width: 1.5px; +} .label { @@ -68,7 +78,6 @@ circle { line-height: 20px; } - /* Content */ .left { diff --git a/js/psfpvis.js b/js/psfpvis.js new file mode 100644 index 0000000..006545b --- /dev/null +++ b/js/psfpvis.js @@ -0,0 +1,533 @@ +//global variables to hold the current variables plotted on each axis +var currentX = "lambda_ex" +var currentY = "lambda_em" +var symbolsize = 8; //radius of circle +//global varable to set the ranges over which the data is filtered. +var filters = { + "lambda_ex" : [350,800,1], // array values represent [min range, max range, step (for the range slider)] + "lambda_em" : [350,800,1], + "E" : [10000,140000,1000], + "QY" : [0,1,0.01], + "brightness": [0,100,1] +} +//string variables for updating the axis labels +var strings = { + "lambda_em" : "Emission Wavelength (nm)", + "lambda_ex" : "Excitation Wavelength (nm)", + "stokes" : "Stokes Shift (nm)", + "E" : "Extinction Coefficient", + "QY" : "Quantum Yield", + "brightness": "Brightness", + "pka" : "pKa", + "bleach" : "Bleaching Half-life (s)", + "mature" : "Maturation Half-time (min)", + "lifetime" : "Lifetime (ns)", +} + +//shorter strings for the table +var tableStrings = { + "Name" : "Protein", + "lambda_ex" : "λex (nm)", + "lambda_em" : "λem (nm)", + "E" : "EC", + "QY" : "QY", + "brightness": "Brightness", + "pka" : "pKa", + "bleach" : "Bleaching (s)", + "mature" : "Maturation (min)", + "lifetime" : "Lifetime (ns)", + "RefNum" : "Reference" +} + +//Protein classes for tables +var FPgroups = [ + {"Name" : "UV", "ex_min" : 0, "ex_max" : 380, "em_min" : 0, "em_max" : 1000, "color" : "#C080FF"}, + {"Name" : "Blue", "ex_min" : 380, "ex_max" : 421, "em_min" : 0, "em_max" : 470, "color" : "#8080FF"}, + {"Name" : "Cyan", "ex_min" : 421, "ex_max" : 473, "em_min" : 0, "em_max" : 530, "color" : "#80FFFF"}, + {"Name" : "Green", "ex_min" : 473, "ex_max" : 507, "em_min" : 480, "em_max" : 530, "color" : "#80FF80"}, + {"Name" : "Yellow", "ex_min" : 507, "ex_max" : 531, "em_min" : 500, "em_max" : 1000, "color" : "#FFFF80"}, + {"Name" : "Orange", "ex_min" : 531, "ex_max" : 556, "em_min" : 530, "em_max" : 569, "color" : "#FFC080"}, + {"Name" : "Red", "ex_min" : 556, "ex_max" : 600, "em_min" : 570, "em_max" : 620, "color" : "#FFA080"}, + {"Name" : "Far Red", "ex_min" : 585, "ex_max" : 631, "em_min" : 620, "em_max" : 1000, "color" : "#FF8080"}, + {"Name" : "Near IR", "ex_min" : 631, "ex_max" : 800, "em_min" : 661, "em_max" : 1000, "color" : "#B09090"}, + {"Name" : "Sapphire-type", "ex_min" : 380, "ex_max" : 420, "em_min" : 480, "em_max" : 530, "color" : "#8080FF"}, + {"Name" : "Long Stokes Shift", "ex_min" : 430, "ex_max" : 480, "em_min" : 580, "em_max" : 640, "color" : "#80A0FF"} +] + +//on page load, listen to slider events and respond by updating the filter ranges (and updating the ui) +//this uses jQuery and jQuery UI which have been added to the head of the document. +$(function() { + + //dynamically generate filter sliders based on "filters" object + $.each(filters, function(i,v){ + var label = $("").appendTo("#sliders"); + var slider = $("
").appendTo("#sliders"); + + slider.rangeSlider({ + bounds:{min: v[0], max: v[1]}, + defaultValues:{min: v[0], max: v[1]}, + step: v[2], + arrows: false, + formatter:function(val){ + return (Math.round(val * 100) / 100); + } + }); + }); + + // update filter settings when user changes slider + $(".rangeSlider").on("valuesChanging", function(e, data){ + var filtID = $(this).attr('id'); + filters[filtID][0] = data.values.min; + filters[filtID][1] = data.values.max; + plot(); + }); + + + $("#Xradio").buttonsetv(); + $("#Yradio").buttonsetv(); + + $( "#Xradio input" ).click(function() { + currentX = $(this).val(); + plot(); + }); + $( "#Yradio input" ).click(function() { + currentY = $(this).val(); + plot(); + }); + + //easter egg + $("#doalittledance").click(function(){doalittledance(1600);}); + }); + +//load the bibliography +$("#bibliography").load('bibliography.html'); + +// Chart dimensions. +var margin = {top: 20, right: 30, bottom: 20, left: 50}, +width = 700 - margin.right, +height = 700 - margin.top - margin.bottom; + +//Scales and axes +var xScale = d3.scale.linear() + .range ([0, width]); + +var yScale = d3.scale.linear() + .range ([height, 0]); + +//This scale will set the saturation (gray to saturated color). We will use it for mapping brightness. +var saturationScale = d3.scale.linear() + .range([0, 1]) + .domain([0, 100]); + +//This scale will set the hue. We will use it for mapping emission wavelength. +var hueScale = d3.scale.linear() + .range([300, 300, 240, 0, 0]) + .domain([200, 405, 440, 650, 850]); + +//X and Y axes +var xAxis_bottom = d3.svg.axis().scale(xScale).tickSize(5).tickSubdivide(true); +var yAxis_left = d3.svg.axis().scale(yScale).tickSize(5).orient("left").tickSubdivide(true); + +//top and right axes are identical but without tick labels +var xAxis_top = d3.svg.axis().scale(xScale).tickSize(5).orient("top").tickSubdivide(true).tickFormat(function (d) { return ''; });;; +var yAxis_right = d3.svg.axis().scale(yScale).tickSize(5).orient("right").tickSubdivide(true).tickFormat(function (d) { return ''; });; + +// Create the SVG container and set the origin. +var svg = d3.select("#graph").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 + ")"); + +//Add the axes +svg.append("g") + .attr("class", "x axis bottom") + .attr("transform", "translate(0," + height + ")") + .call(xAxis_bottom); +svg.append("g") + .attr("class", "y axis left") + .call(yAxis_left); +svg.append("g") + .attr("class", "x axis top") + .call(xAxis_top); +svg.append("svg:g") + .attr("class", "y axis right") + .attr("transform", "translate(" + width + ",0)") + .call(yAxis_right); + +// Add an x-axis label. +svg.append("text") + .attr("class", "x label") + .attr("text-anchor", "middle") + .attr("x", width/2 ) + .attr("y", height-10) + .text("Excitation wavelength (nm)"); + +// Add a y-axis label. +svg.append("text") + .attr("class", "y label") + .attr("text-anchor", "middle") + .attr("x", -height/2) + .attr("y", margin.left-30) + .attr("transform", "rotate(-90)") + .text("Emission wavelength (nm)"); + +//Add a clipping path so that data points don't go outside of frame +svg.append("clipPath") //Make a new clipPath + .attr("id", "chart-area") //Assign an ID + .append("rect") + .attr("width", width) + .attr("height", height); + +//enable zooming +var zoom = d3.behavior.zoom() + .x(xScale) + .y(yScale) + .scaleExtent([1, 10]) + .on("zoom", draw_graph); + +svg.append("rect") + .attr("class", "pane") + .attr("width", width) + .attr("height", height) + .call(zoom); + +plotarea = svg.append("g") + .attr("clip-path", "url(#chart-area)"); + +var FPdata = []; //Where the fluorescent protein data table will end up. +var linkdata = []; //links between photoconvertible states + +// load the csv file and plot it +d3.csv("PSFPs.csv", function (data) { + data.forEach(function(d){ + d.lambda_em = +d.lambda_em; // typing these variables here for simplicity of code later on + d.lambda_ex = +d.lambda_ex; + d.E = +d.E; + d.QY = +d.QY; + d.brightness = +d.brightness; + + //caclulate Stokes shift + d.stokes = d.lambda_em - d.lambda_ex; + + }) + + FPdata = data; + + //Only update max of saturation scale, so that gray corresponds to 0 brightness + //Use 80th percentile as max saturation so that not everything is muddy gray + saturationScale.domain([0, + d3.quantile(FPdata.map(function(a) {return (+a.brightness)}).sort(function(a,b){return a-b}),0.8) + ]); + + d3.csv("links.csv", function (links) { + + links.forEach(function(link){ + //populate link data with appropriate starting and ending coordinates + link.lambda = +link.lambda; + var startFP = $.grep(FPdata, function(e){ return e.UID == link.state1; }); + var endFP = $.grep(FPdata, function(e){ return e.UID == link.state2; }); + startFP = startFP[0]; + endFP = endFP[0]; + link.lambda_ex = [startFP.lambda_ex, endFP.lambda_ex]; + link.lambda_em = [startFP.lambda_em, endFP.lambda_em]; + link.E = [startFP.E, endFP.E]; + link.QY= [startFP.QY, endFP.QY]; + link.brightness = [startFP.brightness, endFP.brightness]; + link.pka = [startFP.pka, endFP.pka]; + link.stokes = [startFP.stokes, endFP.stokes] + link.Name = startFP.Name; + }); + + linkdata = links; + plot(); + draw_table(); + }); +}); + +function draw_graph(){ + //redraw axes with new domains + svg.select(".x.axis.bottom").call(xAxis_bottom); + svg.select(".y.axis.left").call(yAxis_left); + svg.select(".x.axis.top").call(xAxis_top); + svg.select(".y.axis.right").call(yAxis_right); + + svg.selectAll("circle.PSFP") + .attr("cx", function (d) { return xScale (d[currentX]); }) + .attr("cy", function (d) { return yScale (d[currentY]); }); + + svg.selectAll("rect.PSFP") + .attr("x", function (d) { return xScale (d[currentX]) - symbolsize; }) + .attr("y", function (d) { return yScale (d[currentY]) -symbolsize; }); + + svg.selectAll("line.PSFP") + .attr("x1", function (d) { return xScale (d[currentX][0]); }) + .attr("x2", function (d) { return xScale (d[currentX][1]); }) + .attr("y1", function (d) { return yScale (d[currentY][0]); }) + .attr("y2", function (d) { return yScale (d[currentY][1]); }); +} + +//i added this more flexible plotting function to be able to plot different variables on each axis. It takes three optional parameters: the data array, and two axes variables. +function plot(xvar,yvar,data,links){ + //set default values... if plot() is called without arguments, these default values will be used. + xvar = xvar || currentX; + yvar = yvar || currentY; + data = data || FPdata; + links = links || linkdata; + + //filter the data according to the user settings for EC, QY, and brightness range + //we want to keep proteins where any state satisfies the criteria and only remove proteins where all states fail + + var goodFPs = []; + for (var i=0; i < data.length; i++){ + //must pass filtercheck, be non-empty, and not previously recorded. + d = data[i]; + if (filtercheck(d) && d[xvar] > 0 && d[yvar] > 0 && goodFPs.indexOf(d.Name) == -1) { + //add Name to keeplist + goodFPs.push(d.Name); + } + } + data = data.filter(function(d) { return goodFPs.indexOf(d.Name) > -1; }); + links = links.filter(function(d) { return goodFPs.indexOf(d.Name) > -1; }); + + // helper function to iterate through all of the data filters (without having to type them all out) + function filtercheck(data){ + for (f in filters){ + v = filters[f]; + if( data[f] < v[0] || data[f] > v[1] ) {return false;} + } + return true; + } + + //update scale domains based on data + xScale.domain([ + d3.min (data, function(d) { return .99 * d[xvar]; }), + d3.max (data, function(d) { return 1.01 * d[xvar]; }) + ]) + .nice(); + zoom.x(xScale); + + yScale.domain([ + d3.min (data, function(d) { return .99 * d[yvar]; }), + d3.max (data, function(d) { return 1.01 * d[yvar]; }) + ]) + .nice(); + zoom.y(yScale); + + //relabel X and Y axes + svg.select(".x.label").text(strings[xvar]) + svg.select(".y.label").text(strings[yvar]) + + //filter out just photoactivatible proteins, plot them as circles + PAdata = data.filter(function(d) {return d.type == "pa"; }); + // Join new data with old elements, if any. + var circle = plotarea.selectAll("circle.PSFP").data(PAdata, function (d){ return d.UID;}); + + // Create new elements as needed. + circle.enter().append("circle") + .attr("class", "PSFP") + .attr("r", symbolsize) + .attr("stroke", function (d) { return d3.hsl(hueScale (d.lambda_sw), 1, 0.5)}) + .style("fill", function (d) { return d3.hsl(hueScale (d.lambda_em), saturationScale (d.brightness), 0.5)}) + .on('click', function(e){ + if(e.DOI){window.location = "http://dx.doi.org/" + e.DOI;} + }) + .on("mouseover", function(d) { draw_tooltip(d, this);}) + .on("mouseout", function() { + d3.select(this).transition().duration(200).attr("r",8) + //Hide the tooltip + d3.select("#tooltip").classed("hidden", true); + }) + .call(zoom) //so we can zoom while moused over circles as well + + // Remove old elements as needed. + circle.exit().remove(); + + // move circles to their new positions (based on axes) with transition animation + circle.transition() + .attr("cx", function (d) { return xScale (d[xvar]); }) + .attr("cy", function (d) { return yScale (d[yvar]); }) + .duration(800); //change this number to speed up or slow down the animation + + //filter out just photoconvertible proteins, plot them as squares + PCdata = data.filter(function(d) {return d.type == "pc"; }); + // Join new data with old elements, if any. + var square = plotarea.selectAll("rect.PSFP").data(PCdata, function (d){ return d.UID;}); + + // Create new elements as needed. + square.enter().append("rect") + .attr("class", "PSFP") + .attr("width", symbolsize*2) + .attr("height", symbolsize*2) + .attr("stroke", "#000") + .style("fill", function (d) { return d3.hsl(hueScale (d.lambda_em), saturationScale (d.brightness), 0.5)}) + .on('click', function(e){ + if(e.DOI){window.location = "http://dx.doi.org/" + e.DOI;} + }) + .on("mouseover", function(d) { draw_tooltip(d, this);}) + .on("mouseout", function() { + d3.select(this).transition().duration(200).attr("r",8) + //Hide the tooltip + d3.select("#tooltip").classed("hidden", true); + }) + .call(zoom) //so we can zoom while moused over circles as well + + // Remove old elements as needed. + square.exit().remove(); + + // move squares to their new positions (based on axes) with transition animation + square.transition() + .attr("x", function (d) { return xScale (d[xvar]) - symbolsize; }) + .attr("y", function (d) { return yScale (d[yvar]) - symbolsize; }) + .duration(800); //change this number to speed up or slow down the animation + + //Add links for photoconvertible proteins + var line = plotarea.selectAll("line.PSFP").data(links, function (d){ return d.state1;}); + line.enter().append("line") + .attr("class", "PSFP") + .attr("stroke", function (d) { return d3.hsl(hueScale (d.lambda_sw), 1, 0.5)}) + .call(zoom); + + line.exit().remove(); + + line.transition() + .attr("x1", function (d) { return xScale (d[currentX][0]); }) + .attr("x2", function (d) { return xScale (d[currentX][1]); }) + .attr("y1", function (d) { return yScale (d[currentY][0]); }) + .attr("y2", function (d) { return yScale (d[currentY][1]); }) + .duration(800); //change this number to speed up or slow down the animation + + // these two lines cause the transition animation on the axes... they are also cause chopiness in the user interface when the user slides the range sliders on the right side... uncomment to see their effect. + svg.select(".x.axis.bottom").call(xAxis_bottom); + svg.select(".y.axis.left").call(yAxis_left); +} + +function draw_tooltip(d, target) { + d3.select(target).transition().duration(100).attr("r",11) + d3.select(target).text("hi") + //Get target bar's x/y values, then augment for the tooltip + var xPosition = parseFloat(d3.select(target).attr("cx")) + var yPosition = parseFloat(d3.select(target).attr("cy")) + if (xPosition520){ + yPosition =520; + } + //Update the tooltip position and value + d3.select("#tooltip") + .style("left", xPosition + "px") + .style("top", yPosition + "px") + .select("#exvalue") + .text(d.lambda_ex) + d3.select("#tooltip") + .select("#emvalue") + .text(d.lambda_em); + d3.select("#tooltip") + .select("#ecvalue") + .text(d.E); + d3.select("#tooltip") + .select("#qyvalue") + .text(d.QY); + d3.select("#tooltip") + .select("h3") + .text(d.Name); + d3.select("#tooltip") + .select("#brightnessvalue") + .text(d.brightness); + + //Show the tooltip + d3.select("#tooltip").classed("hidden", false); + + } + +function draw_table() { +columns = Object.keys(tableStrings); //column names +//split up fluorescent proteins by type and add the relevant tables +FPgroups.forEach( function(FPtype) { + function testfilt(element){ + return element.lambda_ex >= FPtype.ex_min && element.lambda_ex < FPtype.ex_max + && element.lambda_em >= FPtype.em_min && element.lambda_em < FPtype.em_max; + } + + // var table = d3.select("#table").append("h4") + // .attr("class", "tablename") + // .text(FPtype.Name + " Proteins"); + var table = d3.select("#table").append("table"); + //add title row + table.append("tr").append("th") + .attr("colspan", columns.length) + .attr("class", "tabletitle") + .style("background-color", FPtype.color) + .text(FPtype.Name + " Proteins"); + + tdata = FPdata.filter(testfilt); + table.append("tr") + .attr("class", "header") + .selectAll("th") + .data(columns) + .enter().append("th") + .html(function(d,i) { return tableStrings[columns[i]]; }) + .attr("class", function(d,i) { return (d == "Name") ? "col head protein" : "col head numeric"; }); // conditional here to limit the use of unneccesary global variables + + //populate the table + table.selectAll("tr.data") + .data(tdata) + .enter().append("tr") + .attr("class", "data") + .selectAll("td") + .data(function(d) { + return columns.map(function(column, colstyles) { + var sty = (column == "Name") ? "col protein" : "col numeric"; // conditional here removes need for another "styles" table + return {column: column, value: d[column], style: sty}; + }); + }) + .enter().append("td") + .html(function(d) { + if (d.column == "RefNum"){ + //add links to bibliography + return "" + d.value + ""; + } + else{ + return d.value; + } + }) + .attr("class", function(d) { return d.style; }); + } + ); +} + + +function doalittledance(int) { + var s = ["QY","E","lambda_em","lambda_ex","brightness"]; + setInterval(function() { + var x = s[Math.floor(Math.random() * s.length)]; + do{ + var y = s[Math.floor(Math.random() * s.length)]; + } while (x == y); + plot(x,y); + }, int); + +} + + +//this bit is just a jQuery plugin to make the radio checkboxes on the right side vertical +(function( $ ){ +//plugin buttonset vertical +$.fn.buttonsetv = function() { + $(':radio, :checkbox', this).wrap('
'); + $(this).buttonset(); + $('label:first', this).removeClass('ui-corner-left').addClass('ui-corner-top'); + $('label:last', this).removeClass('ui-corner-right').addClass('ui-corner-bottom'); + mw = 0; // max witdh + $('label', this).each(function(index){ + w = $(this).width(); + if (w > mw) mw = w; + }) + $('label', this).each(function(index){ + $(this).width(mw); + }) +}; +})( jQuery ); \ No newline at end of file diff --git a/links.csv b/links.csv new file mode 100644 index 0000000..b8daf0d --- /dev/null +++ b/links.csv @@ -0,0 +1,7 @@ +"state1","state2","lambda_sw" +"kaede_1","kaede_2",405 +"kikgr1_1","kikgr1_2",405 +"pscfp2_1","pscfp2_2",405 +"meos2_1","meos2_2",405 +"meos3.2_1","meos3.2_2",405 +"psmorange_1","psmorange_2",480