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; +})();