diff --git a/package-lock.json b/package-lock.json
index 9fd1fe5..8937794 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "safety-outlier-explorer",
- "version": "2.5.5",
+ "version": "2.6.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1002,12 +1002,13 @@
},
"jsesc": {
"version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
"dev": true
},
"json5": {
"version": "0.5.1",
- "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
"dev": true
},
@@ -1103,7 +1104,7 @@
},
"printj": {
"version": "1.1.2",
- "resolved": "http://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
+ "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
"integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ=="
},
"private": {
@@ -1148,6 +1149,7 @@
},
"regexpu-core": {
"version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz",
"integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=",
"dev": true,
"requires": {
@@ -1158,11 +1160,13 @@
},
"regjsgen": {
"version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
"integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=",
"dev": true
},
"regjsparser": {
"version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
"integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=",
"dev": true,
"requires": {
@@ -1275,6 +1279,7 @@
},
"trim-right": {
"version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
"dev": true
},
@@ -1286,6 +1291,7 @@
},
"util-deprecate": {
"version": "1.0.2",
+ "resolved": false,
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"optional": true
},
diff --git a/package.json b/package.json
index bf6ab7e..54d1b23 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "safety-outlier-explorer",
- "version": "2.5.5",
+ "version": "2.6.0",
"description": "Chart showing participant trajectories of lab measures, vital signs and other related measures in clinical trials.",
"module": "./src/index.js",
"main": "./safetyOutlierExplorer.js",
diff --git a/safetyOutlierExplorer.js b/safetyOutlierExplorer.js
index 77b49bb..0d17de1 100644
--- a/safetyOutlierExplorer.js
+++ b/safetyOutlierExplorer.js
@@ -5,7 +5,7 @@
? define(['d3', 'webcharts'], factory)
: ((global = global || self),
(global.safetyOutlierExplorer = factory(global.d3, global.webCharts)));
-})(this, function(d3, webcharts) {
+})(this, function(d3$1, webcharts) {
'use strict';
if (typeof Object.assign != 'function') {
@@ -133,14 +133,27 @@
return Math.log(x) * Math.LOG10E;
};
+ (function() {
+ if (typeof window.CustomEvent === 'function') return false;
+
+ function CustomEvent(event, params) {
+ params = params || { bubbles: false, cancelable: false, detail: null };
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
+ return evt;
+ }
+
+ window.CustomEvent = CustomEvent;
+ })();
+
// https://github.com/wbkd/d3-extended
- d3.selection.prototype.moveToFront = function() {
+ d3$1.selection.prototype.moveToFront = function() {
return this.each(function() {
this.parentNode.appendChild(this);
});
};
- d3.selection.prototype.moveToBack = function() {
+ d3$1.selection.prototype.moveToBack = function() {
return this.each(function() {
var firstChild = this.parentNode.firstChild;
if (firstChild) {
@@ -503,7 +516,7 @@
var _this = this;
this.participantCount = {
- N: d3
+ N: d3$1
.set(
this.raw_data.map(function(d) {
return d[_this.config.id_col];
@@ -531,7 +544,7 @@
});
//Nest missing and nonmissing results by participant.
- var participantsWithMissingResults = d3
+ var participantsWithMissingResults = d3$1
.nest()
.key(function(d) {
return d[_this.config.id_col];
@@ -540,7 +553,7 @@
return d.length;
})
.entries(missingResults);
- var participantsWithNonMissingResults = d3
+ var participantsWithNonMissingResults = d3$1
.nest()
.key(function(d) {
return d[_this.config.id_col];
@@ -571,7 +584,7 @@
);
//Count the number of records with missing results.
- this.removedRecords.missing = d3.sum(
+ this.removedRecords.missing = d3$1.sum(
participantsWithMissingResults.filter(function(d) {
return _this.removedRecords.placeholderRecords.indexOf(d.key) < 0;
}),
@@ -659,7 +672,7 @@
function participant() {
var _this = this;
- this.IDOrder = d3
+ this.IDOrder = d3$1
.set(
this.raw_data.map(function(d) {
return d[_this.config.id_col];
@@ -693,7 +706,7 @@
_this.raw_data[0].hasOwnProperty(time_settings.order_col)
) {
//Define a unique set of visits with visit order concatenated.
- visits = d3
+ visits = d3$1
.set(
_this.raw_data.map(function(d) {
return (
@@ -709,7 +722,7 @@
var aOrder = a.split('|')[0],
bOrder = b.split('|')[0],
diff = +aOrder - +bOrder;
- return diff ? diff : d3.ascending(a, b);
+ return diff ? diff : d3$1.ascending(a, b);
})
.map(function(visit) {
return visit.split('|')[1];
@@ -717,7 +730,7 @@
} else {
//Otherwise sort a unique set of visits alphanumerically.
//Define a unique set of visits.
- visits = d3
+ visits = d3$1
.set(
_this.raw_data.map(function(d) {
return d[time_settings.value_col];
@@ -749,7 +762,7 @@
function measure() {
var _this = this;
- this.measures = d3
+ this.measures = d3$1
.set(
this.initial_data.map(function(d) {
return d[_this.config.measure_col];
@@ -757,7 +770,7 @@
)
.values()
.sort();
- this.soe_measures = d3
+ this.soe_measures = d3$1
.set(
this.initial_data.map(function(d) {
return d.soe_measure;
@@ -817,7 +830,7 @@
' ] filter has been removed because the variable does not exist.'
);
} else {
- var levels = d3
+ var levels = d3$1
.set(
_this.raw_data.map(function(d) {
return d[input.value_col];
@@ -896,7 +909,7 @@
return d.label.toLowerCase().replace(' ', '-');
})
.each(function(d) {
- d3.select(this).classed(d.type, true);
+ d3$1.select(this).classed(d.type, true);
});
//Give y-axis controls a common class name.
@@ -1039,7 +1052,7 @@
normalRangeInputs.select('input').attr('step', 0.01);
normalRangeMethodControl.on('change', function() {
- var normal_range_method = d3
+ var normal_range_method = d3$1
.select(this)
.select('option:checked')
.text();
@@ -1138,7 +1151,6 @@
.append('div')
.classed('multiples', true)
.style({
- 'border-top': '1px solid #ccc',
'padding-top': '10px'
}),
id: null
@@ -1185,7 +1197,7 @@
.sort(function(a, b) {
return a - b;
});
- this.measure.domain = d3.extent(this.measure.results);
+ this.measure.domain = d3$1.extent(this.measure.results);
this.measure.range = this.measure.domain[1] - this.measure.domain[0];
this.measure.log10range = Math.log10(this.measure.range);
this.raw_data = this.measure.data.filter(function(d) {
@@ -1199,7 +1211,7 @@
if (!this.config.visits_without_data)
this.config.x.domain = this.config.x.domain.filter(function(visit) {
return (
- d3
+ d3$1
.set(
_this.raw_data.map(function(d) {
return d[_this.config.time_settings.value_col];
@@ -1331,20 +1343,20 @@
this.lln = function(d) {
return d instanceof Object
? +d[_this.config.normal_col_low]
- : d3.median(_this.measure.data, function(d) {
+ : d3$1.median(_this.measure.data, function(d) {
return +d[_this.config.normal_col_low];
});
};
this.uln = function(d) {
return d instanceof Object
? +d[_this.config.normal_col_high]
- : d3.median(_this.measure.data, function(d) {
+ : d3$1.median(_this.measure.data, function(d) {
return +d[_this.config.normal_col_high];
});
};
} else if (this.config.normal_range_method === 'Standard Deviation') {
- this.mean = d3.mean(this.measure.results);
- this.sd = d3.deviation(this.measure.results);
+ this.mean = d3$1.mean(this.measure.results);
+ this.sd = d3$1.deviation(this.measure.results);
this.lln = function() {
return _this.mean - _this.config.normal_range_sd * _this.sd;
};
@@ -1353,10 +1365,13 @@
};
} else if (this.config.normal_range_method === 'Quantiles') {
this.lln = function() {
- return d3.quantile(_this.measure.results, _this.config.normal_range_quantile_low);
+ return d3$1.quantile(_this.measure.results, _this.config.normal_range_quantile_low);
};
this.uln = function() {
- return d3.quantile(_this.measure.results, _this.config.normal_range_quantile_high);
+ return d3$1.quantile(
+ _this.measure.results,
+ _this.config.normal_range_quantile_high
+ );
};
} else {
this.lln = function(d) {
@@ -1407,14 +1422,14 @@
var _this = this;
//count the number of unique ids in the current chart and calculate the percentage
- this.participantCount.n = d3
+ this.participantCount.n = d3$1
.set(
this.filtered_data.map(function(d) {
return d[_this.config.id_col];
})
)
.values().length;
- this.participantCount.percentage = d3.format('0.1%')(
+ this.participantCount.percentage = d3$1.format('0.1%')(
this.participantCount.n / this.participantCount.N
);
@@ -1435,6 +1450,7 @@
function resetChart() {
this.svg.selectAll('.line,.point').remove();
+ this.wrap.select('div.overlapNote').remove();
//delete this.hovered_id;
//delete this.selected_id;
//if (this.multiples.chart)
@@ -1512,7 +1528,7 @@
.select('circle')
.attr({
r: function r(d) {
- return d.radius * 1.5;
+ return d.radius;
},
stroke: 'black',
'stroke-width': function strokeWidth(d) {
@@ -1549,39 +1565,23 @@
}
}
- function orderPoints() {
- var _this = this;
-
- this.marks
- .filter(function(mark) {
- return mark.type === 'circle';
- })
- .forEach(function(mark) {
- mark.groups.each(function(d, i) {
- d.order = _this.IDOrder.find(function(di) {
- return d.key.indexOf(di.ID) === 0;
- }).order;
- });
- });
- }
-
function clearHovered() {
this.lines
.filter(function() {
- return !d3.select(this).classed('selected');
+ return !d3$1.select(this).classed('selected');
})
.select('path')
.each(function(d) {
- d3.select(this).attr(d.attributes);
+ d3$1.select(this).attr(d.attributes);
});
this.points
.filter(function() {
- return !d3.select(this).classed('selected');
+ return !d3$1.select(this).classed('selected');
})
.select('circle')
.each(function(d) {
- d3.select(this).attr(d.attributes);
- d3.select(this).attr('r', d.radius);
+ d3$1.select(this).attr(d.attributes);
+ d3$1.select(this).attr('r', d.radius);
});
delete this.hovered_id;
}
@@ -1606,6 +1606,7 @@
function addOverlayEventListener() {
var _this = this;
+ var context = this;
this.overlay
.on('mouseover', function() {
clearHovered.call(_this);
@@ -1613,6 +1614,7 @@
.on('click', function() {
clearHovered.call(_this);
clearSelected.call(_this);
+ context.wrap.select('div.overlapNote').remove();
});
}
@@ -1650,7 +1652,7 @@
.select('circle')
.attr({
r: function r(d) {
- return d.radius * 1.25;
+ return d.radius;
},
stroke: 'black',
'stroke-width': function strokeWidth(d) {
@@ -1659,30 +1661,6 @@
});
}
- function reorderMarks() {
- var _this = this;
-
- //Move selected line behind all other lines.
- this.lines
- .each(function(d, i) {
- if (d.key.indexOf(_this.selected_id) === 0) d.order = _this.IDOrder.length - 1;
- else if (d.order > _this.selected_id_order) d.order = d.order - 1;
- })
- .sort(function(a, b) {
- return b.order - a.order;
- });
-
- //Move selected points behind all other points.
- this.points
- .each(function(d, i) {
- if (d.key.indexOf(_this.selected_id) === 0) d.order = _this.IDOrder.length - 1;
- else if (d.order > _this.selected_id_order) d.order = d.order - 1;
- })
- .sort(function(a, b) {
- return b.order - a.order;
- });
- }
-
var _typeof =
typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol'
? function(obj) {
@@ -1887,7 +1865,7 @@
);
//Calculate range of data.
- var ylo = d3.min(
+ var ylo = d3$1.min(
filtered_data
.map(function(m) {
return +m[_this.config.y.column];
@@ -1896,7 +1874,7 @@
return +f || +f === 0;
})
);
- var yhi = d3.max(
+ var yhi = d3$1.max(
filtered_data
.map(function(m) {
return +m[_this.config.y.column];
@@ -1926,7 +1904,7 @@
function rangePolygon() {
var _this = this;
- var area = d3.svg
+ var area = d3$1.svg
.area()
.x(function(d) {
return (
@@ -2015,7 +1993,7 @@
.style('width', null)
.style('max-width', '10%')
.on('change', function(d) {
- context.multiples.id = d3
+ context.multiples.id = d3$1
.select(this)
.selectAll('option:checked')
.text();
@@ -2023,6 +2001,7 @@
context.selected_id = context.multiples.id;
highlightSelected.call(context);
smallMultiples.call(context);
+ context.wrap.select('div.overlapNote').remove();
//Trigger participantsSelected event
context.participantsSelected = [context.selected_id];
@@ -2073,11 +2052,7 @@
clearHovered.call(_this);
clearSelected.call(_this);
_this.selected_id = d.values[0].values.raw[0][_this.config.id_col];
- _this.selected_id_order = _this.IDOrder.find(function(di) {
- return di.ID === _this.selected_id;
- }).order;
highlightSelected.call(_this);
- reorderMarks.call(_this);
smallMultiples.call(_this);
//Trigger participantsSelected event
@@ -2087,33 +2062,202 @@
});
}
+ function checkPointOverlap(d, chart) {
+ // Get the position of the clicked point
+ var click_x = d3
+ .select(this)
+ .select('circle')
+ .attr('cx');
+ var click_y = d3
+ .select(this)
+ .select('circle')
+ .attr('cy');
+ var click_r = d3
+ .select(this)
+ .select('circle')
+ .attr('r');
+ var click_id = d.values.raw[0][chart.config.id_col];
+
+ // See if any other points overlap
+ var overlap_ids = chart.points
+ .filter(function(f) {
+ var point_id = f.values.raw[0][chart.config.id_col];
+ var point_x = d3
+ .select(this)
+ .select('circle')
+ .attr('cx');
+ var point_y = d3
+ .select(this)
+ .select('circle')
+ .attr('cy');
+ var distance_x2 = Math.pow(click_x - point_x, 2);
+ var distance_y2 = Math.pow(click_y - point_y, 2);
+ var distance = Math.sqrt(distance_x2 + distance_y2);
+
+ var max_distance = click_r * 2;
+ var overlap = distance <= max_distance;
+ var diff_id = point_id != click_id;
+ return diff_id & overlap;
+ })
+ .data()
+ .map(function(d) {
+ return d.values.raw[0][chart.config.id_col];
+ });
+
+ return overlap_ids;
+ }
+
+ function addOverlapNote(d, chart) {
+ function showID(d) {
+ //click an overlapping ID to see details for that participant
+ var participantDropdown = chart.multiples.controls.wrap
+ .style('margin', 0)
+ .selectAll('.control-group')
+ .filter(function(d) {
+ return d.option === 'selected_id';
+ })
+ .select('select')
+ .property('value', d);
+
+ //participantDropdown.on("change")() // Can't quite get this to work, so copy/pasting for now ...
+
+ var context = chart;
+ chart.multiples.id = d;
+ clearSelected.call(context);
+ context.selected_id = context.multiples.id;
+ highlightSelected.call(context);
+ smallMultiples.call(context);
+
+ //Trigger participantsSelected event
+ context.participantsSelected = [context.selected_id];
+ context.events.participantsSelected.data = context.participantsSelected;
+ context.wrap.node().dispatchEvent(context.events.participantsSelected);
+ }
+
+ chart.wrap.select('div.overlapNote').remove();
+
+ // check for overlapping points
+ chart.overlap_ids = checkPointOverlap.call(this, d, chart);
+
+ // If there are overlapping points, add a note in the details section.
+
+ if (chart.overlap_ids.length) {
+ var click_id = d.values.raw[0][chart.config.id_col];
+ var overlap_div = chart.wrap
+ .insert('div', 'div.multiples')
+ .attr('class', 'overlapNote')
+ .style('background-color', '#eee')
+ .style('border', '1px solid #999')
+ .style('padding', '0.5em')
+ .style('border-radius', '0.2em')
+ .style('margin', '0 0.1em');
+
+ overlap_div
+ .append('span')
+ .html(
+ 'Note: ' +
+ chart.overlap_ids.length +
+ (' point' +
+ (chart.overlap_ids.length === 1 ? '' : 's') +
+ ' overlap' +
+ (chart.overlap_ids.length === 1 ? 's' : '') +
+ ' the clicked point for ') +
+ click_id +
+ '. Click an ID for details: '
+ );
+ overlap_div
+ .select('span.idLink')
+ .datum(click_id)
+ .style('color', 'blue')
+ .style('text-decoration', 'underline')
+ .style('cursor', 'pointer')
+ .on('click', showID)
+ .on('mouseover', function(d) {
+ clearHovered.call(chart);
+ chart.hovered_id = d;
+ if (chart.hovered_id !== chart.selected_id) highlightHovered.call(chart);
+ })
+ .on('mouseout', function(d) {
+ clearHovered.call(chart);
+ });
+ var overlap_ul = overlap_div
+ .append('ul')
+ .style('list-style', 'none')
+ .style('padding', '0')
+ .style('display', 'inline-block');
+
+ overlap_ul
+ .selectAll('li')
+ .data(chart.overlap_ids)
+ .enter()
+ .append('li')
+ .style('display', 'inline-block')
+ .style('padding-right', '.5em')
+ .attr('class', 'idLink')
+ .style('color', 'blue')
+ .style('text-decoration', 'underline')
+ .style('cursor', 'pointer')
+ .text(function(d) {
+ return d;
+ })
+ .on('click', showID)
+ .on('mouseover', function(d) {
+ clearHovered.call(chart);
+ chart.hovered_id = d;
+ if (chart.hovered_id !== chart.selected_id) highlightHovered.call(chart);
+ })
+ .on('mouseout', function(d) {
+ clearHovered.call(chart);
+ });
+ }
+ }
+
+ function addOverlapTitle(d, chart) {
+ // check for overlapping points
+ var overlap = checkPointOverlap.call(this, d, chart);
+
+ // If there are overlapping points, add a note in the details section.
+
+ if (overlap.length > 0) {
+ var titleEl = d3.select(this).select('title');
+ var currentTitle = titleEl.text();
+ var hasOverlapNote = currentTitle.search('overlapping'); //minor hack ...
+ if (hasOverlapNote == -1) {
+ var newTitle =
+ currentTitle + '\nNumber of overlapping point(s) = ' + overlap.length;
+ titleEl.text(newTitle);
+ }
+ }
+ }
+
function addPointEventListeners() {
var _this = this;
+ var chart = this;
this.points
.on('mouseover', function(d) {
- clearHovered.call(_this);
- _this.hovered_id = d.values.raw[0][_this.config.id_col];
- if (_this.hovered_id !== _this.selected_id) highlightHovered.call(_this);
+ addOverlapTitle.call(this, d, chart);
+ clearHovered.call(chart);
+ chart.hovered_id = d.values.raw[0][chart.config.id_col];
+ if (chart.hovered_id !== chart.selected_id) highlightHovered.call(chart);
})
.on('mouseout', function(d) {
clearHovered.call(_this);
})
.on('click', function(d) {
- clearHovered.call(_this);
- clearSelected.call(_this);
- _this.selected_id = d.values.raw[0][_this.config.id_col];
- _this.selected_id_order = _this.IDOrder.find(function(di) {
- return di.ID === _this.selected_id;
- }).order;
- highlightSelected.call(_this);
- reorderMarks.call(_this);
- smallMultiples.call(_this);
+ clearHovered.call(chart);
+ clearSelected.call(chart);
+ chart.selected_id = d.values.raw[0][chart.config.id_col];
+ highlightSelected.call(chart);
+ smallMultiples.call(chart);
//Trigger participantsSelected event
- _this.participantsSelected = [_this.selected_id];
- _this.events.participantsSelected.data = _this.participantsSelected;
- _this.wrap.node().dispatchEvent(_this.events.participantsSelected);
+ chart.participantsSelected = [chart.selected_id];
+ chart.events.participantsSelected.data = chart.participantsSelected;
+ chart.wrap.node().dispatchEvent(chart.events.participantsSelected);
+
+ //check for overlapping points
+ addOverlapNote.call(this, d, chart);
});
}
@@ -2134,18 +2278,18 @@
.map(function(d) {
return +d.values.y;
})
- .sort(d3.ascending);
+ .sort(d3$1.ascending);
var height = this.plot_height;
var width = 1;
var domain = this.y_dom;
var boxPlotWidth = 10;
var boxColor = '#bbb';
var boxInsideColor = 'white';
- var fmt = d3.format('.3r');
+ var fmt = d3$1.format('.3r');
//set up scales
- var x = d3.scale.linear().range([0, width]);
- var y = d3.scale.linear().range([height, 0]);
+ var x = d3$1.scale.linear().range([0, width]);
+ var y = d3$1.scale.linear().range([height, 0]);
{
y.domain(domain);
@@ -2153,7 +2297,7 @@
var probs = [0.05, 0.25, 0.5, 0.75, 0.95];
for (var i = 0; i < probs.length; i++) {
- probs[i] = d3.quantile(results, probs[i]);
+ probs[i] = d3$1.quantile(results, probs[i]);
}
var boxplot = this.svg
@@ -2216,7 +2360,7 @@
.append('circle')
.attr('class', 'boxplot mean')
.attr('cx', x(0.5))
- .attr('cy', y(d3.mean(results)))
+ .attr('cy', y(d3$1.mean(results)))
.attr('r', x(boxPlotWidth / 3))
.style('fill', boxInsideColor)
.style('stroke', boxColor);
@@ -2225,7 +2369,7 @@
.append('circle')
.attr('class', 'boxplot mean')
.attr('cx', x(0.5))
- .attr('cy', y(d3.mean(results)))
+ .attr('cy', y(d3$1.mean(results)))
.attr('r', x(boxPlotWidth / 6))
.style('fill', boxColor)
.style('stroke', 'None');
@@ -2236,31 +2380,31 @@
d.values.length +
'\n' +
'Min = ' +
- d3.min(d.values) +
+ d3$1.min(d.values) +
'\n' +
'5th % = ' +
- fmt(d3.quantile(d.values, 0.05)).replace(/^ */, '') +
+ fmt(d3$1.quantile(d.values, 0.05)).replace(/^ */, '') +
'\n' +
'Q1 = ' +
- fmt(d3.quantile(d.values, 0.25)).replace(/^ */, '') +
+ fmt(d3$1.quantile(d.values, 0.25)).replace(/^ */, '') +
'\n' +
'Median = ' +
- fmt(d3.median(d.values)).replace(/^ */, '') +
+ fmt(d3$1.median(d.values)).replace(/^ */, '') +
'\n' +
'Q3 = ' +
- fmt(d3.quantile(d.values, 0.75)).replace(/^ */, '') +
+ fmt(d3$1.quantile(d.values, 0.75)).replace(/^ */, '') +
'\n' +
'95th % = ' +
- fmt(d3.quantile(d.values, 0.95)).replace(/^ */, '') +
+ fmt(d3$1.quantile(d.values, 0.95)).replace(/^ */, '') +
'\n' +
'Max = ' +
- d3.max(d.values) +
+ d3$1.max(d.values) +
'\n' +
'Mean = ' +
- fmt(d3.mean(d.values)).replace(/^ */, '') +
+ fmt(d3$1.mean(d.values)).replace(/^ */, '') +
'\n' +
'StDev = ' +
- fmt(d3.deviation(d.values)).replace(/^ */, '');
+ fmt(d3$1.deviation(d.values)).replace(/^ */, '');
return tooltip;
});
}
@@ -2275,9 +2419,6 @@
//Draw normal range.
drawNormalRange.call(this);
- //Add initial ordering to points; ordering will update as points are clicked.
- orderPoints.call(this);
-
//Add event listeners to lines, points, and overlay.
addEventListeners.call(this);
diff --git a/src/callbacks/onDraw/resetChart.js b/src/callbacks/onDraw/resetChart.js
index f13cfba..ce5d75a 100644
--- a/src/callbacks/onDraw/resetChart.js
+++ b/src/callbacks/onDraw/resetChart.js
@@ -1,5 +1,6 @@
export default function resetChart() {
this.svg.selectAll('.line,.point').remove();
+ this.wrap.select('div.overlapNote').remove();
//delete this.hovered_id;
//delete this.selected_id;
//if (this.multiples.chart)
diff --git a/src/callbacks/onLayout/addSmallMultiplesContainer.js b/src/callbacks/onLayout/addSmallMultiplesContainer.js
index df8467d..afe4018 100644
--- a/src/callbacks/onLayout/addSmallMultiplesContainer.js
+++ b/src/callbacks/onLayout/addSmallMultiplesContainer.js
@@ -4,7 +4,6 @@ export default function addSmallMultiplesContainer() {
.append('div')
.classed('multiples', true)
.style({
- 'border-top': '1px solid #ccc',
'padding-top': '10px'
}),
id: null
diff --git a/src/callbacks/onResize.js b/src/callbacks/onResize.js
index b466d06..d0d670b 100644
--- a/src/callbacks/onResize.js
+++ b/src/callbacks/onResize.js
@@ -1,7 +1,6 @@
import attachMarks from './onResize/attachMarks';
import maintainHighlight from './onResize/maintainHighlight';
import drawNormalRange from './onResize/drawNormalRange';
-import orderPoints from './onResize/orderPoints';
import addEventListeners from './onResize/addEventListeners';
import addBoxPlot from './onResize/addBoxPlot';
import adjustTicks from './onResize/adjustTicks';
@@ -16,9 +15,6 @@ export default function onResize() {
//Draw normal range.
drawNormalRange.call(this);
- //Add initial ordering to points; ordering will update as points are clicked.
- orderPoints.call(this);
-
//Add event listeners to lines, points, and overlay.
addEventListeners.call(this);
diff --git a/src/callbacks/onResize/addEventListeners/addLineEventListeners.js b/src/callbacks/onResize/addEventListeners/addLineEventListeners.js
index 075f50e..4c88f94 100644
--- a/src/callbacks/onResize/addEventListeners/addLineEventListeners.js
+++ b/src/callbacks/onResize/addEventListeners/addLineEventListeners.js
@@ -2,7 +2,6 @@ import clearHovered from './functions/clearHovered';
import highlightHovered from './functions/highlightHovered';
import clearSelected from './functions/clearSelected';
import highlightSelected from './functions/highlightSelected';
-import reorderMarks from './functions/reorderMarks';
import smallMultiples from './functions/smallMultiples';
export default function addLineEventListeners() {
@@ -19,9 +18,7 @@ export default function addLineEventListeners() {
clearHovered.call(this);
clearSelected.call(this);
this.selected_id = d.values[0].values.raw[0][this.config.id_col];
- this.selected_id_order = this.IDOrder.find(di => di.ID === this.selected_id).order;
highlightSelected.call(this);
- reorderMarks.call(this);
smallMultiples.call(this);
//Trigger participantsSelected event
diff --git a/src/callbacks/onResize/addEventListeners/addOverlayEventListener.js b/src/callbacks/onResize/addEventListeners/addOverlayEventListener.js
index 12da58d..3d6636a 100644
--- a/src/callbacks/onResize/addEventListeners/addOverlayEventListener.js
+++ b/src/callbacks/onResize/addEventListeners/addOverlayEventListener.js
@@ -2,6 +2,7 @@ import clearHovered from './functions/clearHovered';
import clearSelected from './functions/clearSelected';
export default function addOverlayEventListener() {
+ var context = this;
this.overlay
.on('mouseover', () => {
clearHovered.call(this);
@@ -9,5 +10,6 @@ export default function addOverlayEventListener() {
.on('click', () => {
clearHovered.call(this);
clearSelected.call(this);
+ context.wrap.select('div.overlapNote').remove();
});
}
diff --git a/src/callbacks/onResize/addEventListeners/addPointEventListeners.js b/src/callbacks/onResize/addEventListeners/addPointEventListeners.js
index 338569d..307e300 100644
--- a/src/callbacks/onResize/addEventListeners/addPointEventListeners.js
+++ b/src/callbacks/onResize/addEventListeners/addPointEventListeners.js
@@ -2,31 +2,35 @@ import clearHovered from './functions/clearHovered';
import highlightHovered from './functions/highlightHovered';
import clearSelected from './functions/clearSelected';
import highlightSelected from './functions/highlightSelected';
-import reorderMarks from './functions/reorderMarks';
import smallMultiples from './functions/smallMultiples';
+import addOverlapNote from './functions/addOverlapNote';
+import addOverlapTitle from './functions/addOverlapTitle';
export default function addPointEventListeners() {
+ var chart = this;
this.points
- .on('mouseover', d => {
- clearHovered.call(this);
- this.hovered_id = d.values.raw[0][this.config.id_col];
- if (this.hovered_id !== this.selected_id) highlightHovered.call(this);
+ .on('mouseover', function(d) {
+ addOverlapTitle.call(this, d, chart);
+ clearHovered.call(chart);
+ chart.hovered_id = d.values.raw[0][chart.config.id_col];
+ if (chart.hovered_id !== chart.selected_id) highlightHovered.call(chart);
})
.on('mouseout', d => {
clearHovered.call(this);
})
- .on('click', d => {
- clearHovered.call(this);
- clearSelected.call(this);
- this.selected_id = d.values.raw[0][this.config.id_col];
- this.selected_id_order = this.IDOrder.find(di => di.ID === this.selected_id).order;
- highlightSelected.call(this);
- reorderMarks.call(this);
- smallMultiples.call(this);
+ .on('click', function(d) {
+ clearHovered.call(chart);
+ clearSelected.call(chart);
+ chart.selected_id = d.values.raw[0][chart.config.id_col];
+ highlightSelected.call(chart);
+ smallMultiples.call(chart);
//Trigger participantsSelected event
- this.participantsSelected = [this.selected_id];
- this.events.participantsSelected.data = this.participantsSelected;
- this.wrap.node().dispatchEvent(this.events.participantsSelected);
+ chart.participantsSelected = [chart.selected_id];
+ chart.events.participantsSelected.data = chart.participantsSelected;
+ chart.wrap.node().dispatchEvent(chart.events.participantsSelected);
+
+ //check for overlapping points
+ addOverlapNote.call(this, d, chart);
});
}
diff --git a/src/callbacks/onResize/addEventListeners/functions/addOverlapNote.js b/src/callbacks/onResize/addEventListeners/functions/addOverlapNote.js
new file mode 100644
index 0000000..f74ec57
--- /dev/null
+++ b/src/callbacks/onResize/addEventListeners/functions/addOverlapNote.js
@@ -0,0 +1,106 @@
+import { select } from 'd3';
+import clearSelected from './clearSelected';
+import highlightSelected from './highlightSelected';
+import smallMultiples from './smallMultiples';
+import clearHovered from './clearHovered';
+import highlightHovered from './highlightHovered';
+import checkPointOverlap from './checkPointOverlap';
+
+export default function addOverlapNote(d, chart) {
+ function showID(d) {
+ //click an overlapping ID to see details for that participant
+ const participantDropdown = chart.multiples.controls.wrap
+ .style('margin', 0)
+ .selectAll('.control-group')
+ .filter(d => d.option === 'selected_id')
+ .select('select')
+ .property('value', d);
+
+ //participantDropdown.on("change")() // Can't quite get this to work, so copy/pasting for now ...
+
+ var context = chart;
+ chart.multiples.id = d;
+ clearSelected.call(context);
+ context.selected_id = context.multiples.id;
+ highlightSelected.call(context);
+ smallMultiples.call(context);
+
+ //Trigger participantsSelected event
+ context.participantsSelected = [context.selected_id];
+ context.events.participantsSelected.data = context.participantsSelected;
+ context.wrap.node().dispatchEvent(context.events.participantsSelected);
+ }
+
+ chart.wrap.select('div.overlapNote').remove();
+
+ // check for overlapping points
+ chart.overlap_ids = checkPointOverlap.call(this, d, chart);
+
+ // If there are overlapping points, add a note in the details section.
+
+ if (chart.overlap_ids.length) {
+ const click_id = d.values.raw[0][chart.config.id_col];
+ const overlap_div = chart.wrap
+ .insert('div', 'div.multiples')
+ .attr('class', 'overlapNote')
+ .style('background-color', '#eee')
+ .style('border', '1px solid #999')
+ .style('padding', '0.5em')
+ .style('border-radius', '0.2em')
+ .style('margin', '0 0.1em');
+
+ overlap_div
+ .append('span')
+ .html(
+ 'Note: ' +
+ chart.overlap_ids.length +
+ ` point${chart.overlap_ids.length === 1 ? '' : 's'} overlap${
+ chart.overlap_ids.length === 1 ? 's' : ''
+ } the clicked point for ` +
+ click_id +
+ '. Click an ID for details: '
+ );
+ overlap_div
+ .select('span.idLink')
+ .datum(click_id)
+ .style('color', 'blue')
+ .style('text-decoration', 'underline')
+ .style('cursor', 'pointer')
+ .on('click', showID)
+ .on('mouseover', d => {
+ clearHovered.call(chart);
+ chart.hovered_id = d;
+ if (chart.hovered_id !== chart.selected_id) highlightHovered.call(chart);
+ })
+ .on('mouseout', d => {
+ clearHovered.call(chart);
+ });
+ const overlap_ul = overlap_div
+ .append('ul')
+ .style('list-style', 'none')
+ .style('padding', '0')
+ .style('display', 'inline-block');
+
+ overlap_ul
+ .selectAll('li')
+ .data(chart.overlap_ids)
+ .enter()
+ .append('li')
+ .style('display', 'inline-block')
+ .style('padding-right', '.5em')
+ .attr('class', 'idLink')
+ .style('color', 'blue')
+ .style('text-decoration', 'underline')
+ .style('cursor', 'pointer')
+ .text(d => d)
+ .on('click', showID)
+ .on('mouseover', d => {
+ clearHovered.call(chart);
+ chart.hovered_id = d;
+ if (chart.hovered_id !== chart.selected_id) highlightHovered.call(chart);
+ })
+ .on('mouseout', d => {
+ clearHovered.call(chart);
+ });
+ }
+}
diff --git a/src/callbacks/onResize/addEventListeners/functions/addOverlapTitle.js b/src/callbacks/onResize/addEventListeners/functions/addOverlapTitle.js
new file mode 100644
index 0000000..ae14cdf
--- /dev/null
+++ b/src/callbacks/onResize/addEventListeners/functions/addOverlapTitle.js
@@ -0,0 +1,18 @@
+import checkPointOverlap from './checkPointOverlap';
+
+export default function addOverlapTitle(d, chart) {
+ // check for overlapping points
+ var overlap = checkPointOverlap.call(this, d, chart);
+
+ // If there are overlapping points, add a note in the details section.
+
+ if (overlap.length > 0) {
+ var titleEl = d3.select(this).select('title');
+ var currentTitle = titleEl.text();
+ var hasOverlapNote = currentTitle.search('overlapping'); //minor hack ...
+ if (hasOverlapNote == -1) {
+ var newTitle = currentTitle + '\nNumber of overlapping point(s) = ' + overlap.length;
+ titleEl.text(newTitle);
+ }
+ }
+}
diff --git a/src/callbacks/onResize/addEventListeners/functions/checkPointOverlap.js b/src/callbacks/onResize/addEventListeners/functions/checkPointOverlap.js
new file mode 100644
index 0000000..bf0171f
--- /dev/null
+++ b/src/callbacks/onResize/addEventListeners/functions/checkPointOverlap.js
@@ -0,0 +1,42 @@
+export default function checkPointOverlap(d, chart) {
+ // Get the position of the clicked point
+ const click_x = d3
+ .select(this)
+ .select('circle')
+ .attr('cx');
+ const click_y = d3
+ .select(this)
+ .select('circle')
+ .attr('cy');
+ const click_r = d3
+ .select(this)
+ .select('circle')
+ .attr('r');
+ const click_id = d.values.raw[0][chart.config.id_col];
+
+ // See if any other points overlap
+ var overlap_ids = chart.points
+ .filter(function(f) {
+ const point_id = f.values.raw[0][chart.config.id_col];
+ const point_x = d3
+ .select(this)
+ .select('circle')
+ .attr('cx');
+ const point_y = d3
+ .select(this)
+ .select('circle')
+ .attr('cy');
+ const distance_x2 = Math.pow(click_x - point_x, 2);
+ const distance_y2 = Math.pow(click_y - point_y, 2);
+ const distance = Math.sqrt(distance_x2 + distance_y2);
+
+ const max_distance = click_r * 2;
+ const overlap = distance <= max_distance;
+ const diff_id = point_id != click_id;
+ return diff_id & overlap;
+ })
+ .data()
+ .map(d => d.values.raw[0][chart.config.id_col]);
+
+ return overlap_ids;
+}
diff --git a/src/callbacks/onResize/addEventListeners/functions/highlightHovered.js b/src/callbacks/onResize/addEventListeners/functions/highlightHovered.js
index b071e69..9cd5a47 100644
--- a/src/callbacks/onResize/addEventListeners/functions/highlightHovered.js
+++ b/src/callbacks/onResize/addEventListeners/functions/highlightHovered.js
@@ -10,7 +10,7 @@ export default function highlightHovered() {
.filter(d => d.values.raw[0][this.config.id_col] === this.hovered_id)
.select('circle')
.attr({
- r: d => d.radius * 1.25,
+ r: d => d.radius,
stroke: 'black',
'stroke-width': d => d.attributes['stroke-width'] * 4
});
diff --git a/src/callbacks/onResize/addEventListeners/functions/highlightSelected.js b/src/callbacks/onResize/addEventListeners/functions/highlightSelected.js
index 744d86f..59867e2 100644
--- a/src/callbacks/onResize/addEventListeners/functions/highlightSelected.js
+++ b/src/callbacks/onResize/addEventListeners/functions/highlightSelected.js
@@ -19,7 +19,7 @@ export default function highlightSelected() {
.filter(d => d.values.raw[0][this.config.id_col] === this.selected_id)
.select('circle')
.attr({
- r: d => d.radius * 1.5,
+ r: d => d.radius,
stroke: 'black',
'stroke-width': d => d.attributes['stroke-width'] * 8
});
diff --git a/src/callbacks/onResize/addEventListeners/functions/reorderMarks.js b/src/callbacks/onResize/addEventListeners/functions/reorderMarks.js
deleted file mode 100644
index 49e58ce..0000000
--- a/src/callbacks/onResize/addEventListeners/functions/reorderMarks.js
+++ /dev/null
@@ -1,17 +0,0 @@
-export default function reorderMarks() {
- //Move selected line behind all other lines.
- this.lines
- .each((d, i) => {
- if (d.key.indexOf(this.selected_id) === 0) d.order = this.IDOrder.length - 1;
- else if (d.order > this.selected_id_order) d.order = d.order - 1;
- })
- .sort((a, b) => b.order - a.order);
-
- //Move selected points behind all other points.
- this.points
- .each((d, i) => {
- if (d.key.indexOf(this.selected_id) === 0) d.order = this.IDOrder.length - 1;
- else if (d.order > this.selected_id_order) d.order = d.order - 1;
- })
- .sort((a, b) => b.order - a.order);
-}
diff --git a/src/callbacks/onResize/addEventListeners/functions/smallMultiples/updateParticipantDropdown.js b/src/callbacks/onResize/addEventListeners/functions/smallMultiples/updateParticipantDropdown.js
index b3a5c90..ccaaeb8 100644
--- a/src/callbacks/onResize/addEventListeners/functions/smallMultiples/updateParticipantDropdown.js
+++ b/src/callbacks/onResize/addEventListeners/functions/smallMultiples/updateParticipantDropdown.js
@@ -27,6 +27,7 @@ export default function updateParticipantDropdown() {
context.selected_id = context.multiples.id;
highlightSelected.call(context);
smallMultiples.call(context);
+ context.wrap.select('div.overlapNote').remove();
//Trigger participantsSelected event
context.participantsSelected = [context.selected_id];
diff --git a/src/callbacks/onResize/orderPoints.js b/src/callbacks/onResize/orderPoints.js
deleted file mode 100644
index 1e67d2d..0000000
--- a/src/callbacks/onResize/orderPoints.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export default function orderPoints() {
- this.marks
- .filter(mark => mark.type === 'circle')
- .forEach(mark => {
- mark.groups.each((d, i) => {
- d.order = this.IDOrder.find(di => d.key.indexOf(di.ID) === 0).order;
- });
- });
-}
diff --git a/src/configuration/controlInputs.js b/src/configuration/controlInputs.js
old mode 100644
new mode 100755
diff --git a/src/configuration/index.js b/src/configuration/index.js
old mode 100644
new mode 100755
diff --git a/src/configuration/rendererSettings.js b/src/configuration/rendererSettings.js
old mode 100644
new mode 100755
diff --git a/src/configuration/syncControlInputs.js b/src/configuration/syncControlInputs.js
old mode 100644
new mode 100755
diff --git a/src/configuration/syncSettings.js b/src/configuration/syncSettings.js
old mode 100644
new mode 100755
diff --git a/src/configuration/webchartsSettings.js b/src/configuration/webchartsSettings.js
old mode 100644
new mode 100755
diff --git a/src/util/polyfills.js b/src/util/polyfills.js
index b8097c7..83a44f2 100644
--- a/src/util/polyfills.js
+++ b/src/util/polyfills.js
@@ -126,16 +126,15 @@ Math.log10 =
return Math.log(x) * Math.LOG10E;
};
-
- (function() {
- if (typeof window.CustomEvent === 'function') return false;
-
- function CustomEvent(event, params) {
- params = params || { bubbles: false, cancelable: false, detail: null };
- var evt = document.createEvent('CustomEvent');
- evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
- return evt;
- }
-
- window.CustomEvent = CustomEvent;
- })();
+(function() {
+ if (typeof window.CustomEvent === 'function') return false;
+
+ function CustomEvent(event, params) {
+ params = params || { bubbles: false, cancelable: false, detail: null };
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
+ return evt;
+ }
+
+ window.CustomEvent = CustomEvent;
+})();