1 export function init(datasetId, datasetName) {
6 // store phenotype id, trait, value
8 this.outlierCutoffs = new Set();
9 this.firstRefresh = true;
10 this.stdDevMultiplier = $("#outliers_range").slider("value");
11 this.selection = "default";
12 this.datasetId = datasetId;
13 this.observations = {};
18 this.storedOutliersIds = [];
19 this.metricValue = document.querySelector('input[name="dataset_metric"]:checked').value;
23 const LocalThis = this;
24 this.firstRefresh = false;
26 url: '/ajax/dataset/retrieve/' + this.datasetId + '/phenotypes?include_phenotype_primary_key=1',
27 success: function (response) {
28 LocalThis.observations = response.phenotypes;
29 LocalThis.setDropDownTraits();
31 error: function (response) {
38 const LocalThis = this;
39 this.firstRefresh = false;
41 url: '/ajax/dataset/retrieve/' + this.datasetId + '/traits',
42 success: function (response) {
43 LocalThis.traitsIds = response.traits.map(
46 // console.log(LocalThis.traitsIds);
48 error: function (response) {
55 const LocalThis = this;
58 url: '/ajax/dataset/retrieve_outliers/' + LocalThis.datasetId,
59 success: function (response) {
60 LocalThis.storedOutliersIds = response.outliers !== null ? response.outliers : [];
62 error: function (response) {
69 const keys = this.observations[0];
70 // Construct trait object
71 for (let i = 39; i < keys.length - 1; i++) {
72 if (i % 2 == 1 && i <= keys.length - 2) {
73 this.traits[keys[i]] = {};
77 for (let i = 1; i < this.observations.length; i++) {
78 // Goes through each observation, and populates the traits hash with each trait, using the phenotype id as the key, and the traitValue as the value.
79 for (let j = 39; j < this.observations[i].length - 1; j++) {
81 this.traits[keys[j]][this.observations[i][j + 1]] = this.observations[i][j];
85 // Use traits to set select options
86 const select = document.getElementById("trait_selection");
87 for (const traitName of Object.keys(this.traits)) {
88 const option = document.createElement("option");
89 option.value = traitName;
90 option.innerHTML = traitName;
91 select.appendChild(option);
97 if (this.selection != "default") {
98 // Gets a list of pheno ids, filters them so only the ones that have non null values are included and then sorts the ids by their value by looking up their values in traits hash.
100 // console.log(this.traits);
101 this.phenoIds = Object.keys(this.traits[this.selection])
102 .filter((phenoId) => !isNaN(parseFloat(this.traits[this.selection][phenoId])))
103 .sort((a, b) => this.traits[this.selection][a] - this.traits[this.selection][b]);
105 // console.log(this.phenoIds);
107 this.traitVals = this.phenoIds.map((id) => parseFloat(this.traits[this.selection][id]));
108 // console.log(this.traitVals);
109 // Debugging check: You should see a list of ids and the corresponding values, logs should be sorted by increasing values.
110 // for (let id of this.phenoIds) {
111 // console.log(id, this.traits[this.selection][id].value);
116 standardDeviation(values) {
117 var average = function (data) {
118 var sum = data.reduce(function (sum, value) {
122 var avg = sum / data.length;
126 var avg = average(values);
128 var squareDiffs = values.map(function (value) {
129 var diff = value - avg;
130 var sqrDiff = diff * diff;
134 var avgSquareDiff = average(squareDiffs);
136 var stdDev = Math.sqrt(avgSquareDiff);
137 return [avg, stdDev];
141 if (!Array.isArray(values)) return 0;
142 let sorted = [...values].sort((a, b) => a - b);
143 let middle = Math.floor(sorted.length / 2);
145 if (sorted.length % 2 === 0) {
146 return (sorted[middle - 1] + sorted[middle]) / 2;
149 return sorted[middle];
153 // MAD = Median(|Xi - Median(Xi)|)
154 if (!Array.isArray(values)) return 0;
155 let medianValue = this.median(values);
156 let medianMap = values.map(x => Math.abs(x - medianValue));
158 return this.median(medianMap);
162 quartile(values, factor) {
163 if (!Array.isArray(values)) return 0;
165 let sorted = [...values].sort((a, b) => a - b);
166 let index = (sorted.length - 1) * factor;
167 let lowerIndex = Math.floor(index);
168 let upperIndex = Math.ceil(index);
169 let interpolation = index - lowerIndex;
170 return sorted[lowerIndex] * (1 - interpolation) + sorted[upperIndex] * interpolation;
175 let q1 = this.quartile(values, 0.25);
176 let q3 = this.quartile(values, 0.75);
180 addEventListeners() {
181 let LocalThis = this;
183 // Handle Slider Events
184 var sliderSelector = $("#outliers_range");
185 sliderSelector.on("slidechange", (event, ui) => {
186 LocalThis.stdDevMultiplier = ui.value;
187 LocalThis.outliers = [];
188 LocalThis.outlierCutoffs = new Set();
189 d3.select("svg").remove();
192 // Handle Metric Radio
193 var metricSelectors = document.querySelectorAll('input[type=radio][name="dataset_metric"]');
195 Array.prototype.forEach.call(metricSelectors, (metricRadio) => {
196 metricRadio.addEventListener("change", (event) => {
199 LocalThis.metricValue = document.querySelector('input[name="dataset_metric"]:checked').value;
200 d3.select("svg").remove();
205 // Handle Select Events
206 let selection = document.getElementById("trait_selection");
207 selection.addEventListener("change", (event) => {
208 d3.select("svg").remove();
209 LocalThis.selection = event.target.value;
211 LocalThis.outliers = [];
212 LocalThis.outlierCutoffs = new Set();
213 sliderSelector.slider("option", "value", 3);
214 LocalThis.stdDevMultiplier = sliderSelector.slider("value");
215 if (!this.firstRefresh) {
216 d3.select("svg").remove();
218 removeRosnserTable();
222 let storeOutliersButton = document.getElementById("store_outliers");
223 storeOutliersButton.onclick = function () {
224 let allOutliers = new Set(LocalThis.storedOutliersIds.concat(LocalThis.outliers));
225 let stringOutliers = [...allOutliers].join(',');
226 let stringOutlierCutoffs = [...LocalThis.outlierCutoffs].join(',');
229 url: '/ajax/dataset/store_outliers/' + LocalThis.datasetId,
230 data: { outliers: stringOutliers, outlier_cutoffs: stringOutlierCutoffs },
231 success: function (response) {
232 alert('outliers successfully stored!');
233 LocalThis.storedOutliersIds = [...allOutliers];
234 d3.select("svg").remove();
237 error: function (response) {
243 let resetOutliersButton = document.getElementById("reset_outliers");
244 resetOutliersButton.onclick = function () {
247 url: '/ajax/dataset/store_outliers/' + LocalThis.datasetId,
248 data: { outliers: "", outlier_cutoffs: "" },
249 success: function (response) {
250 alert('outliers successfully reseted!');
251 LocalThis.storedOutliersIds = [];
252 d3.select("svg").remove();
254 removeRosnserTable();
256 error: function (response) {
262 let resetTraitOutliersButton = document.getElementById("reset_trait");
263 resetTraitOutliersButton.onclick = function () {
264 let filteredNonTrait = LocalThis.storedOutliersIds.filter(elem => !LocalThis.phenoIds.includes(elem));
265 let stringFilteredNonTrait = filteredNonTrait.join(',');
268 url: '/ajax/dataset/store_outliers/' + LocalThis.datasetId,
269 data: { outliers: stringFilteredNonTrait, outlier_cutoffs: "" },
270 success: function (response) {
271 alert('outliers successfully reseted!');
272 LocalThis.storedOutliersIds = filteredNonTrait;
273 d3.select("svg").remove();
275 removeRosnserTable();
277 error: function (response) {
283 let rosnersTestButton = document.getElementById("rosner_test");
284 rosnersTestButton.onclick = function () {
285 if (document.getElementById("trait_selection").value == 'default') return;
287 document.getElementById("loading-spinner").style.visibility = 'visible';
290 url: '/ajax/dataset/rosner_test/' + LocalThis.datasetId,
291 data: { dataset_trait: document.getElementById("trait_selection").value },
292 success: function (response) {
293 // alert(response.message);
294 createOutlierTable(response.file);
295 document.getElementById("loading-spinner").style.visibility = 'hidden';
297 error: function (response) {
299 document.getElementById("loading-spinner").style.visibility = 'visible';
304 function createOutlierTable(file) {
305 // check if exist and remove
306 let data_rows = file.slice(1, file.length);
307 let column_names = file[0].map(value => ({ "title": value }));
308 let outliers = data_rows.filter((elem) => { return elem[7] == 'TRUE' }).map((elem) => elem[4]);
310 removeRosnserTable();
312 let table = document.createElement("table");
313 table.setAttribute("id", "rosner_table");
314 table.classList = "display";
315 document.getElementById("statistic_tests").appendChild(table);
317 let addRosnerButton = document.createElement("button");
318 if (outliers.length == 0) {
319 addRosnerButton.disabled = true;
321 addRosnerButton.setAttribute("id", "rosner_add");
322 addRosnerButton.classList = "btn btn-sm btn-success btn-dataset";
323 addRosnerButton.textContent = "Add Rosner test outliers";
324 addRosnerButton.addEventListener('click', function () {
325 addRosnserOutliers(outliers);
328 document.getElementById("statistic_tests").appendChild(addRosnerButton);
330 jQuery('#rosner_table').DataTable({
331 columns: column_names,
341 function addRosnserOutliers(rosnerOutliers) {
342 if (rosnerOutliers.length == 0) {
345 // console.log(rosnerOutliers);
347 let allOutliers = new Set(LocalThis.storedOutliersIds.concat(rosnerOutliers));
348 let stringOutliers = [...allOutliers].join(',');
349 let stringOutlierCutoffs = [...LocalThis.outlierCutoffs].join(',');
352 url: '/ajax/dataset/store_outliers/' + LocalThis.datasetId,
353 data: { outliers: stringOutliers, outlier_cutoffs: stringOutlierCutoffs },
354 success: function (response) {
355 alert('outliers successfully stored!');
356 LocalThis.storedOutliersIds = [...allOutliers];
357 d3.select("svg").remove();
360 error: function (response) {
368 function removeRosnserTable() {
369 if ($.fn.DataTable.isDataTable("#rosner_table")) {
370 jQuery('#rosner_table').DataTable().destroy();
372 if (document.getElementById("rosner_table")) {
373 document.getElementById("rosner_table").remove();
374 document.getElementById("rosner_add").remove();
382 if (this.firstRefresh) {
383 this.getPhenotypes();
385 this.getStoredOutliers();
386 this.addEventListeners();
387 } else if (this.selection != "default") {
389 const LocalThis = this;
391 const [mean, stdDev] = this.standardDeviation(this.traitVals);
392 const [median, mad] = [this.median(this.traitVals), this.mad(this.traitVals)];
393 const [quartiles, factor] = [[this.quartile(this.traitVals, 0.25), this.quartile(this.traitVals, 0.75)], this.iqr(this.traitVals)];
396 switch (LocalThis.metricValue) {
398 settings = [mean, stdDev, "Mean", "Std Dev"];
401 settings = [median, mad, "Median", "MAD"];
404 settings = [quartiles, factor, "Q1,Q3", "IQR"];
407 settings = [median, mad, "Median", "MAD"];
409 const [metric, deviation, metricString, deviationString] = settings;
411 let filter = LocalThis.stdDevMultiplier;
413 function rightCutoffCalc() {
414 if (LocalThis.metricValue == "iqr") {
415 return quartiles[1] + factor * filter;
417 return metric + deviation * filter;
421 function leftCutoffCalc() {
422 if (LocalThis.metricValue == "iqr") {
423 return quartiles[0] - factor * filter;
425 return Math.max(metric - deviation * filter, 0);
429 let rightCutoff = rightCutoffCalc();
430 let leftCutoff = leftCutoffCalc();
434 const margin = { top: 10, right: 30, bottom: 30, left: 60 },
435 width = 1180 - margin.left - margin.right,
436 height = 600 - margin.top - margin.bottom;
438 var svg = d3.select("#trait_graph")
440 .attr("width", width + margin.left + margin.right)
441 .attr("height", height + margin.top + margin.bottom)
445 "translate(" + margin.left + "," + margin.top + ")"
448 const greenColor = "#00ba38"
449 , yellowColor = "#ffe531"
450 , redColor = "#f7756c"
451 , blueColor = "#337ab7";
453 var isOutlier = function (id, value, leftCutoff, rightCutoff) {
458 if (value >= leftCutoff && value <= rightCutoff) {
462 if (LocalThis.storedOutliersIds.includes(id.toString())) {
466 if (value <= leftCutoff || value >= rightCutoff) {
467 if (leftCutoff >= 0) { LocalThis.outlierCutoffs.add(leftCutoff) };
468 LocalThis.outlierCutoffs.add(rightCutoff);
469 LocalThis.outliers.push(id);
473 return [color, stroke];
476 // Add ackground ggplot2 like
480 .attr("height", height)
481 .attr("width", width)
482 .style("fill", "#ebebeb")
485 var x = d3.scaleLinear()
486 .domain([0, this.phenoIds.length])
490 var y = d3.scaleLinear()
491 .domain([Math.min(...this.traitVals), Math.max(...this.traitVals)])
494 .call(d3.axisLeft(y).tickSize(-width * 1).ticks(10))
495 .style("font", "16px arial")
498 svg.selectAll(".tick line").attr("stroke", "white")
501 .attr("transform", "rotate(-90)")
502 .attr("y", 0 - margin.left)
503 .attr("x", 0 - (height / 2))
505 .style("text-anchor", "middle")
506 .style("font", "16px arial")
507 .text("Trait Value");
511 var tooltip = d3.select("#trait_graph")
513 .attr("id", "tooltip")
514 .attr("class", "tooltip")
515 .style("background-color", "white")
516 .style("border", "solid")
517 .style("border-width", "2px")
518 .style("font-size", "15px")
519 .style("border-radius", "5px")
520 .style("padding", "5px")
521 .style("opacity", 0);
523 var mouseover = function (d) {
527 .style("fill", "white")
531 var mousemove = function (d) {
533 .html("id: " + LocalThis.phenoIds[d] + "<br>" + "val: " + LocalThis.traitVals[d])
534 .style("left", (d3.mouse(this)[0] + 50) + "px")
535 .style("top", (d3.mouse(this)[1] + 40) + "px")
538 var mouseleave = function (d) {
542 .style("fill", function (d) { return isOutlier(LocalThis.phenoIds[d], LocalThis.traitVals[d], leftCutoff, rightCutoff)[0] })
543 .style("stroke-width", 2)
544 .style("stroke", function (d) { return isOutlier(LocalThis.phenoIds[d], LocalThis.traitVals[d], leftCutoff, rightCutoff)[1] })
545 .style("fill-opacity", (d) => { return (LocalThis.traitVals[d] <= leftCutoff || LocalThis.traitVals[d] >= rightCutoff ? 0.2 : 0.8) })
546 .style("stroke-opacity", (d) => { return (LocalThis.traitVals[d] <= leftCutoff || LocalThis.traitVals[d] >= rightCutoff ? 0.4 : 0.8) })
551 .data([...Array(this.phenoIds.length).keys()])
554 .attr("cx", function (d) { return x(d); })
555 .attr("cy", function (d) { return y(LocalThis.traitVals[d]); })
557 .style("fill", function (d) { return isOutlier(LocalThis.phenoIds[d], LocalThis.traitVals[d], leftCutoff, rightCutoff)[0] })
558 .style("stroke-width", 2)
559 .style("stroke", function (d) { return isOutlier(LocalThis.phenoIds[d], LocalThis.traitVals[d], leftCutoff, rightCutoff)[1] })
560 .style("fill-opacity", (d) => { return (LocalThis.traitVals[d] <= leftCutoff || LocalThis.traitVals[d] >= rightCutoff ? 0.2 : 0.8) })
561 .style("stroke-opacity", (d) => { return (LocalThis.traitVals[d] <= leftCutoff || LocalThis.traitVals[d] >= rightCutoff ? 0.4 : 0.8) })
562 .on("mouseover", mouseover)
563 .on("mousemove", mousemove)
564 .on("mouseleave", mouseleave);
567 if (LocalThis.metricValue != "iqr") {
568 metricArray = [metric];
570 metricArray = metric;
573 metricArray.forEach((number) => {
575 .attr("class", "mean-line")
577 .attr("y1", y(number))
579 .attr("y2", y(number))
580 .attr("fill", "none")
581 .attr("stroke", "black");
585 .attr("class", "sd-line-top")
587 .attr("y1", y(rightCutoff))
589 .attr("y2", y(rightCutoff))
590 .attr("fill", "none")
591 .attr("stroke", "darkgrey");
594 .attr("class", "sd-line-bottom")
596 .attr("y1", y(leftCutoff >= 0 ? leftCutoff : 0))
598 .attr("y2", y(leftCutoff >= 0 ? leftCutoff : 0))
599 .attr("fill", "none")
600 .attr("stroke", "darkgrey");
615 const legend = svg.append("g")
616 .attr('id', 'legend')
617 .attr('height', legendSize.height)
618 .attr('width', legendSize.width)
619 .attr('transform', 'translate(5, 5)');
621 legend.append('rect')
622 .attr('height', legendSize.height)
623 .attr('width', legendSize.width)
626 .attr('fill', 'white')
627 .style("stroke", "lightgrey")
628 .style("stroke-width", 3);
630 legend.append('circle')
632 .attr('class', 'dot-legend')
633 .attr('fill', greenColor)
634 .attr('cx', legendSize.posX + 20)
635 .attr('cy', legendSize.posY + 5)
637 legend.append('circle')
639 .attr('stroke', "black")
640 .style("stroke-width", 2)
641 .style("fill", "none")
642 .attr('class', 'dot-legend')
643 .attr('cx', legendSize.posX + 20)
644 .attr('cy', legendSize.posY + 30)
646 legend.append('circle')
648 .attr('class', 'dot-legend')
649 .attr('fill', redColor)
650 .attr('cx', legendSize.posX + 20)
651 .attr('cy', legendSize.posY + 55)
655 .attr('x', legendSize.posX + 25 + dotSize + 5)
656 .attr('y', legendSize.posY + 10 + dotSize / 2 + 1)
657 .style("font", "arial")
658 .text('normal data point')
661 .attr('x', legendSize.posX + 25 + dotSize + 5)
662 .attr('y', legendSize.posY + 35 + dotSize / 2 + 1)
663 .style("font", "arial")
664 .text('outliers value stored in database')
667 .attr('x', legendSize.posX + 25 + dotSize + 5)
668 .attr('y', legendSize.posY + 60 + dotSize / 2 + 1)
669 .style("font", "arial")
670 .text('outliers from current cutoff')
673 legend.append("text")
674 .text(metricString + ": " + (LocalThis.metricValue == "iqr" ? "(" + metric[0].toFixed(1) + ", " + metric[1].toFixed(1) + ")" : metric.toFixed(2)))
675 .attr('x', legendSize.posX + 20 - dotSize / 2)
676 .attr('y', legendSize.posY + 85);
678 legend.append("text")
679 .text(deviationString + ": " + deviation.toFixed(2))
680 .style("font", "arial")
681 .attr('x', legendSize.posX + (LocalThis.metricValue == "iqr" ? 165 : 130) - dotSize / 2)
682 .attr('y', legendSize.posY + 85);
684 legend.append("text")
686 return "L. Cutoff: " + leftCutoff.toFixed(2);
688 .style("font", "arial")
689 .attr('x', legendSize.posX + 20 - dotSize / 2)
690 .attr('y', legendSize.posY + 110);
692 legend.append("text")
694 return "R. Cutoff: " + rightCutoff.toFixed(2);
696 .style("font", "arial")
697 .attr('x', legendSize.posX + 130 - dotSize / 2)
698 .attr('y', legendSize.posY + 110);
702 const dataset = new Dataset;