diff --git a/.gitignore b/.gitignore index 07f7eba..d13cfd8 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,6 @@ simple_cov_output thing.pdf -manuscript \ No newline at end of file +manuscript + +app/assets/javascripts/silly.js diff --git a/app/assets/javascripts/pd.js b/app/assets/javascripts/pd.js index 8381177..dd8923d 100644 --- a/app/assets/javascripts/pd.js +++ b/app/assets/javascripts/pd.js @@ -8,11 +8,199 @@ global.pd.html.id.upload_tree_submit = "pd-submit"; global.pd.html.id.upload_group_input = "pd-group-names"; -global.pd.html.id.results = "pd-results"; +global.pd.html.id.results = "pd-results"; +global.pd.html.id.results_status = "pd-table-status"; +global.pd.html.id.results_save = "pd-table-save"; + +global.pd.html.id.hist_container = "pd-hist-container"; +global.pd.html.id.hist_svg = "pd-hist-svg"; +global.pd.html.id.hist_status = "pd-hist-status"; +global.pd.html.id.hist_save = "pd-hist-save"; + +global.pd.hist = {}; +global.pd.hist.height = 500; +global.pd.hist.width = 500;//"100%"; +global.pd.hist.width_padding = 50; +global.pd.hist.height_padding = 50; + +global.pd.hist.jackknife_iters = 100; + +global.pd.all_table_data = []; global.pd.fn = {}; +global.pd.fn.save_svg = function (id) { + function svg_elem_to_string(id) { + var svg_elem = document.getElementById(id); + + if (svg_elem) { + return (new XMLSerializer()).serializeToString(svg_elem); + } + else { + return null; + } + } + + var str = svg_elem_to_string(id); + + if (str !== null) { + saveAs( + new Blob( + [str], + { type: "application/svg+xml" } + ), + "histogram.svg" + ); + } +}; + +global.pd.fn.save_table = function (table_data) { + if (table_data.length > 0) { + var header = [ + "Group", + "NodeCount", + "PairCount", + "PairDistTotal", + "PairDistTotalP", + "PairDistMean", + "PairDistMeanP", + "Dispersion", + "DispersionP", + ].join("\t"); + + var table = table_data.map(function (row_data) { + var row = []; + + row_data.forEach(function (dat, i) { + if (i < 3) { + // First three columns don't have p values + row.push(dat); + } + else { + // vals are like 1231.32 (p = 0.0021) + var m = dat.match(/^(.*) \(p = (.*)\)$/); + if (m) { + row.push(m[1]); + row.push(m[2]); + } + else { + // just push the whole value as it doesn't have p value + row.push(dat); + row.push(""); + } + } + }); + + return row.join("\t"); + }).join("\n"); + + var table_str = [header, table].join("\n"); + + var blob = new Blob( + [table_str], + { type: "text/plain;charset=utf-8" } + ); + + // Unicode standard does not recommend using the BOM for UTF-8, so pass in true to NOT put it in. + saveAs(blob, "pd_table.txt", true); + } + else { + alert("no data in the results table!"); + } +}; + +global.pd.fn.draw_jk_hist = function (stats, jk_stats, svg) { + jq(global.pd.html.id.hist_svg).remove(); + + var svg = d3.select("#" + global.pd.html.id.hist_container) + .append("svg") + .attr("width", global.pd.hist.width) + .attr("height", global.pd.hist.height) + .attr("id", global.pd.html.id.hist_svg); + + var radius = 3; + + stats.type = "actual"; + stats.r = radius * 2; + + var data = []; + data.push(stats); + + jk_stats.forEach(function (st) { + st.r = radius; + data.push(st); + }); + + var xmin = d3.min(data.map(function (st) { + return st.disp; + })); + + var xmax = d3.max(data.map(function (st) { + return st.disp; + })); + + + var x_scale = d3.scaleLinear() + .domain([xmin, xmax]) + .range([global.pd.hist.width_padding, global.pd.hist.width - global.pd.hist.width_padding]); + + // Add x axis + svg.append("g") + .attr("class", "x axis") + // Shove it to the bottom of the chart. + .attr("transform", "translate(0, " + Math.floor(global.pd.hist.height - global.pd.hist.height_padding) + ")") + .call(d3.axisBottom(x_scale)); + + // Add x axis label + svg.append("text") + .attr("transform", + "translate(" + (global.pd.hist.width / 2) + " ," + + (Math.floor(global.pd.hist.height - (global.pd.hist.height_padding / 4))) + ")") + .style("text-anchor", "middle") + .text("Dispersion"); + + + var simulation = d3.forceSimulation(data) + .force("x", d3.forceX(function (d) { + return x_scale(d.disp); + }).strength(1)) + .force("y", d3.forceY(Math.floor(global.pd.hist.height / 2))) + .force("collide", d3.forceCollide(function (d) { + return d.r + 1; + })) + .on("tick", ticked); + // .stop(); + // + // for (var i = 0; i < 500; ++i) { + // simulation.tick(); + // } + + // Note, if the number of jackknifes is big enough, we'd want to calculate the layout first and then just graph the result. (using the simulation tick for loop above) + + function ticked() { + var circles = svg.selectAll("circle").data(data); + + circles + .enter().append("circle") + .attr("r", function (d) { + return d.r; + }) + .attr("fill", function (d) { + return d.type === "actual" ? "red" : "#272727"; + }) + .merge(circles) + .attr("cx", function (d) { + return d.x; + }) + .attr("cy", function (d) { + return d.y; + }); + + circles.exit().remove(); + } +}; + global.pd.fn.main = function () { var tree_uploader = document.getElementById(global.pd.html.id.upload_tree_input); @@ -25,6 +213,21 @@ global.pd.fn.main = function () { var group_reader = new FileReader(); + var save_hist_button = + document.getElementById(global.pd.html.id.hist_save); + + save_hist_button.addEventListener("click", function () { + global.pd.fn.save_svg(global.pd.html.id.hist_svg); + }); + + var save_table_button = + document.getElementById(global.pd.html.id.results_save); + + save_table_button.addEventListener("click", function () { + global.pd.fn.save_table(global.pd.all_table_data); + }); + + tree_reader.onload = function tree_reader_onload(event) { var newick_string = event.target.result; var group_file = group_uploader.files[0]; @@ -55,6 +258,12 @@ global.pd.fn.main = function () { alert("Don't forget a tree file!"); } }); + + + // For easier testing, comment the above submit handler and use this one. + // submit_button.addEventListener("click", function () { + // global.pd.fn.handle_data(silly.tree, silly.name_graph); + // }); }; global.pd.fn.parse_group_membership = function (group_string) { @@ -88,59 +297,98 @@ global.pd.fn.parse_group_membership = function (group_string) { }; global.pd.fn.handle_data = function (newick_string, group_string) { + $("#" + global.pd.html.id.results_status).text("Calculating stats! (could take a while...)"); - var group_membership = global.pd.fn.parse_group_membership(group_string); - var parsed_newick = newick__parse(newick_string); + setTimeout(function () { + // Reset the table data + global.pd.all_table_data = []; - var tree = - d3.hierarchy(parsed_newick, function hiearchy_from_newick(d) { - return d.branchset; - }) - .sum(function (d) { - return d.branchset ? 0 : 1; - }) - .sort(sort_ascending); - - t = tree; - l = tree.leaves(); - global.pd.tree = tree; - global.pd.leaves = tree.leaves(); - - var results = $("#" + global.pd.html.id.results); - results.empty(); - - results.append( - "" + - "Group" + - "NodeCount" + - "PairCount" + - "PairDistTotal" + - "PairDistMean" + - "Dispersion" + - "" - ); - - var leaves = tree.leaves(); - - fn.obj.each(group_membership, function (group, names) { - var group_nodes = leaves.filter(function (node) { - return names.includes(node.data.name); - }); + var group_membership = global.pd.fn.parse_group_membership(group_string); + var parsed_newick = newick__parse(newick_string); - var stats = global.pd.fn.calc_stats(group_nodes); + var tree = + d3.hierarchy(parsed_newick, function hiearchy_from_newick(d) { + return d.branchset; + }) + .sum(function (d) { + return d.branchset ? 0 : 1; + }) + .sort(sort_ascending); - results.append(global.pd.fn.make_table_row(group, stats)); + t = tree; + l = tree.leaves(); + global.pd.tree = tree; + global.pd.leaves = tree.leaves(); - var jackknife_iters = 10; - var jackknife_stats = global.pd.fn.jackknife_stats(leaves, group_nodes.length, jackknife_iters); + var results = $("#" + global.pd.html.id.results); + results.empty(); results.append( - global.pd.fn.make_table_row( - group + "___", - global.pd.fn.collate_jackknife_stats(jackknife_stats) - ) + "" + + "Group" + + "NodeCount" + + "PairCount" + + "PairDistTotal" + + "PairDistMean" + + "Dispersion" + + "" ); - }); + + var leaves = tree.leaves(); + var group_idx = 0; + + fn.obj.each(group_membership, function (group, names) { + group_idx++; + + var group_nodes = leaves.filter(function (node) { + return names.includes(node.data.name); + }); + + var stats = global.pd.fn.calc_stats(group_nodes); + + + var jackknife_stats = global.pd.fn.jackknife_stats(leaves, group_nodes.length, global.pd.hist.jackknife_iters); + + + var pvals = global.pd.fn.compare_to_jackknife_stats(stats, jackknife_stats); + + var table_row_data = global.pd.fn.make_table_row_data(group, stats, pvals); + + global.pd.all_table_data.push(table_row_data); + + results.append(global.pd.fn.make_table_row(group_idx, table_row_data)); + + // Attach row group handler + $("#" + "pd-row-" + group_idx).on("click", function draw_hist() { + // Set status msg to rendering + $("#" + global.pd.html.id.hist_status).text("Rendering!"); + + setTimeout(function () { + // var jk_stats = global.pd.fn.jackknife_stats( + // leaves, + // group_nodes.length, + // global.pd.hist.jackknife_iters + // ); + + global.pd.fn.draw_jk_hist(stats, jackknife_stats, svg); + + // Set status msg to done + $("#" + global.pd.html.id.hist_status).text("Done!"); + + }, TIMEOUT); + }); + + + // results.append( + // global.pd.fn.make_table_row( + // group + "___", + // global.pd.fn.collate_jackknife_stats(jackknife_stats) + // ) + // ); + + $("#" + global.pd.html.id.results_status).text("Done!"); + }); + }, TIMEOUT); }; global.pd.fn.len_to_root = function (node) { @@ -268,24 +516,41 @@ global.pd.fn.fy_shuf = function (array) { return array; }; -global.pd.fn.make_table_row = function (group, stats) { +global.pd.fn.make_table_row_data = function (group, stats, pvals) { var precision = 3; - return "" + - group + - "" + - fn.math.round(stats.node_count, precision) + - "" + - fn.math.round(stats.pair_count, precision) + - "" + - fn.math.round(stats.sum, precision) + - "" + - fn.math.round(stats.mean, precision) + - "" + - fn.math.round(stats.disp, precision) + - ""; + var node_count = fn.math.round(stats.node_count, precision); + var pair_count = fn.math.round(stats.pair_count, precision); + var sum = fn.math.round(stats.sum, precision); + var mean = fn.math.round(stats.mean, precision); + var disp = fn.math.round(stats.disp, precision); + + if (!(pvals === undefined)) { + if (!(pvals.sum === undefined)) { + sum += " (p = " + fn.math.round(pvals.sum, precision) + ")"; + } + + if (!(pvals.mean === undefined)) { + mean += " (p = " + fn.math.round(pvals.mean, precision) + ")"; + } + if (!(pvals.disp === undefined)) { + disp += " (p = " + fn.math.round(pvals.disp, precision) + ")"; + } + } + + return [group, node_count, pair_count, sum, mean, disp]; }; +global.pd.fn.make_table_row = function (idx, ary) { + var row_html = ary.map(function (elem) { + return "" + elem + ""; + }); + + // The table row has the id pd-row-groupName + return "" + row_html + ""; +}; + + // I shuffle the original array in place, so make sure that's what you want! Also, I return shallow copies! global.pd.fn.random_sample = function (ary, size) { return global.pd.fn.fy_shuf(ary).slice(0, size); @@ -302,6 +567,35 @@ global.pd.fn.jackknife_stats = function (nodes, sample_size, iters) { return all_stats; }; +/** + * Want to get (sort of) p-values for your stats? Use this fn. P value represents number of samples where the jackknife stat was less than actual stat. + * @param stats + * @param jackknife_stats + */ +global.pd.fn.compare_to_jackknife_stats = function (stats, jackknife_stats) { + var sum = 0, mean = 0, disp = 0; + + jackknife_stats.forEach(function (jstats, i) { + if (jstats.sum < stats.sum) { + sum++; + } + + if (jstats.mean < stats.mean) { + mean++; + } + + if (jstats.disp < stats.disp) { + disp++; + } + }); + + return { + sum: sum / jackknife_stats.length, + mean: mean / jackknife_stats.length, + disp: disp / jackknife_stats.length, + }; +}; + global.pd.fn.collate_jackknife_stats = function (jackknife_stats) { var mean_stats = {}, node_count = 0, diff --git a/app/assets/stylesheets/pages.scss b/app/assets/stylesheets/pages.scss index 3ae5173..427efde 100644 --- a/app/assets/stylesheets/pages.scss +++ b/app/assets/stylesheets/pages.scss @@ -1,15 +1,12 @@ // Place all the styles related to the pages controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ - @import 'settings'; .hideme { display: none; } - - // This is for the viewer #show-length { position: absolute; @@ -123,19 +120,18 @@ label { border-right-width: 3px; } - #save-tab-button { border-left-width: 1.5px; } // See https://gist.github.com/corilam/5c935e6fb6bdef40b78b4c66e4b78dc9 .tabs-title { - float:none !important; - display:inline-block; + float: none !important; + display: inline-block; } .tabs { - text-align:center; + text-align: center; } #iroki-logo { @@ -160,6 +156,7 @@ hr.row { .thick-right-border { border-right-width: 3px; } + .thin-right-border { border-right-width: 1.5px; } @@ -167,6 +164,7 @@ hr.row { .thick-left-border { border-left-width: 3px; } + .thin-left-border { border-left-width: 1.5px; } @@ -221,7 +219,6 @@ hr.row { /* } */ /* } */ - /* .blink { */ /* animation-duration: 1s; */ /* animation-name: blink; */ @@ -237,3 +234,24 @@ hr.row { /* opacity: 0; */ /* } */ /* } */ + +.results-table { + max-height: 500px; + overflow-x: auto; + overflow-y: auto; +} + +#pd-hist-svg { + background-color: white; +} + +#pd-hist-svg { + line { + stroke: #272727; + stroke-width: 1px; + } + + text { + fill: #272727; + } +} diff --git a/app/views/pages/pd.html.slim b/app/views/pages/pd.html.slim index 7db4939..598bff9 100644 --- a/app/views/pages/pd.html.slim +++ b/app/views/pages/pd.html.slim @@ -1,5 +1,5 @@ .row - h1 Phylogenetic distance calculator + h1 Phylogenetic dispersion calculator .row h4 Upload tree @@ -17,7 +17,30 @@ h2 Results .row - table#pd-results + .small-12.medium-6.columns + .row + h3 Stats + .row + p#pd-table-status Upload your data and click submit! + .row + .small-12.columns + input id="pd-table-save" type="button" value="Save table!" + .row + br + .row.results-table + table#pd-results + .small-12.medium-6.columns + .row + h3 Jackknife info + .row + p#pd-hist-status Click a row to see jackknife results! + .row + .small-12.columns + input id="pd-hist-save" type="button" value="Save hist!" + .row + br + .row#pd-hist-container + javascript: