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: