From 74a3d7740d65babf79eaf0a8ec206d7af4b990ba Mon Sep 17 00:00:00 2001 From: jwildfire Date: Tue, 13 Aug 2019 20:11:44 -0700 Subject: [PATCH 01/12] working overlap detection prototype. #132 --- safetyOutlierExplorer.js | 222 +++++++++++++----- .../addPointEventListeners.js | 28 ++- .../functions/checkOverlap.js | 73 ++++++ src/util/polyfills.js | 25 +- 4 files changed, 261 insertions(+), 87 deletions(-) create mode 100644 src/callbacks/onResize/addEventListeners/functions/checkOverlap.js diff --git a/safetyOutlierExplorer.js b/safetyOutlierExplorer.js index 77b49bb..9d0da4a 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(); @@ -1185,7 +1198,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 +1212,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 +1344,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 +1366,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 +1423,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 ); @@ -1568,20 +1584,20 @@ 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; } @@ -1887,7 +1903,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 +1912,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 +1942,7 @@ function rangePolygon() { var _this = this; - var area = d3.svg + var area = d3$1.svg .area() .x(function(d) { return ( @@ -2015,7 +2031,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(); @@ -2087,9 +2103,88 @@ }); } + function checkOverlap(d, chart) { + console.log(chart); + + chart.multiples.container.select('div.overlapNote').remove(); + 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]; + chart.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]; + }); + + console.log(chart.overlap_ids); + + if (chart.overlap_ids.length) { + var overlap_div = chart.multiples.container + .insert('div', '*') + .attr('class', 'overlapNote'); + overlap_div + .append('span') + .html( + 'Note:' + + chart.overlap_ids.length + + ' points overlap with the clicked point:' + ); + var overlap_ul = overlap_div + .append('ul') + .style('list-style', 'none') + .style('display', 'inline-block'); + overlap_ul + .selectAll('li') + .data(chart.overlap_ids) + .enter() + .append('li') + .style('display', 'inline-block') + .style('padding-right', '.5em') + .style('color', 'blue') + .style('text-decoration', 'underline') + .style('cursor', 'pointer') + .text(function(d) { + return d; + }) + .on('click', function(d) { + console.log('changing to participant:', d); + }); + } + } + function addPointEventListeners() { var _this = this; + var chart = this; this.points .on('mouseover', function(d) { clearHovered.call(_this); @@ -2100,20 +2195,23 @@ 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; + clearHovered.call(chart); + clearSelected.call(chart); + chart.selected_id = d.values.raw[0][chart.config.id_col]; + chart.selected_id_order = chart.IDOrder.find(function(di) { + return di.ID === chart.selected_id; }).order; - highlightSelected.call(_this); - reorderMarks.call(_this); - smallMultiples.call(_this); + highlightSelected.call(chart); + reorderMarks.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 + checkOverlap.call(this, d, chart); }); } @@ -2134,18 +2232,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 +2251,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 +2314,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 +2323,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 +2334,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; }); } diff --git a/src/callbacks/onResize/addEventListeners/addPointEventListeners.js b/src/callbacks/onResize/addEventListeners/addPointEventListeners.js index 338569d..2ffe09e 100644 --- a/src/callbacks/onResize/addEventListeners/addPointEventListeners.js +++ b/src/callbacks/onResize/addEventListeners/addPointEventListeners.js @@ -4,8 +4,9 @@ import clearSelected from './functions/clearSelected'; import highlightSelected from './functions/highlightSelected'; import reorderMarks from './functions/reorderMarks'; import smallMultiples from './functions/smallMultiples'; - +import checkOverlap from './functions/checkOverlap'; export default function addPointEventListeners() { + var chart = this; this.points .on('mouseover', d => { clearHovered.call(this); @@ -15,18 +16,21 @@ export default function addPointEventListeners() { .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]; + chart.selected_id_order = chart.IDOrder.find(di => di.ID === chart.selected_id).order; + highlightSelected.call(chart); + reorderMarks.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 + checkOverlap.call(this, d, chart); }); } diff --git a/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js b/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js new file mode 100644 index 0000000..2a9b0b6 --- /dev/null +++ b/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js @@ -0,0 +1,73 @@ +export default function checkOverlap(d, chart) { + console.log(chart); + + chart.multiples.container.select('div.overlapNote').remove(); + 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]; + chart.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]); + + console.log(chart.overlap_ids); + + if (chart.overlap_ids.length) { + const overlap_div = chart.multiples.container + .insert('div', '*') + .attr('class', 'overlapNote'); + overlap_div + .append('span') + .html( + 'Note:' + + chart.overlap_ids.length + + ' points overlap with the clicked point. Click an ID for details:' + ); + const overlap_ul = overlap_div + .append('ul') + .style('list-style', 'none') + .style('display', 'inline-block'); + overlap_ul + .selectAll('li') + .data(chart.overlap_ids) + .enter() + .append('li') + .style('display', 'inline-block') + .style('padding-right', '.5em') + .style('color', 'blue') + .style('text-decoration', 'underline') + .style('cursor', 'pointer') + .text(d => d) + .on('click', function(d) { + console.log('changing to participant:', d); + }); + } +} 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; +})(); From 7516f1a8a03c334a05aef055c3e64ef82058eb88 Mon Sep 17 00:00:00 2001 From: jwildfire Date: Tue, 13 Aug 2019 22:51:37 -0700 Subject: [PATCH 02/12] show details when clicking overlap id. #132 --- safetyOutlierExplorer.js | 49 +++++++++++++++--- .../functions/checkOverlap.js | 51 ++++++++++++++++--- .../updateParticipantDropdown.js | 1 + 3 files changed, 87 insertions(+), 14 deletions(-) diff --git a/safetyOutlierExplorer.js b/safetyOutlierExplorer.js index 9d0da4a..bf955d4 100644 --- a/safetyOutlierExplorer.js +++ b/safetyOutlierExplorer.js @@ -2039,6 +2039,7 @@ context.selected_id = context.multiples.id; highlightSelected.call(context); smallMultiples.call(context); + chart.wrap.select('div.overlapNote').remove(); //Trigger participantsSelected event context.participantsSelected = [context.selected_id]; @@ -2106,7 +2107,9 @@ function checkOverlap(d, chart) { console.log(chart); - chart.multiples.container.select('div.overlapNote').remove(); + chart.wrap.select('div.overlapNote').remove(); + + // Get the position of the clicked point var click_x = d3 .select(this) .select('circle') @@ -2120,6 +2123,8 @@ .select('circle') .attr('r'); var click_id = d.values.raw[0][chart.config.id_col]; + + // See if any other points overlap chart.overlap_ids = chart.points .filter(function(f) { var point_id = f.values.raw[0][chart.config.id_col]; @@ -2146,22 +2151,29 @@ }); console.log(chart.overlap_ids); - + // If there are overlapping points, add a note in the details section. if (chart.overlap_ids.length) { - var overlap_div = chart.multiples.container - .insert('div', '*') - .attr('class', 'overlapNote'); + var overlap_div = chart.wrap + .insert('div', 'div.multiples') + .attr('class', 'overlapNote') + .style('background-color', '#999') + .style('border', '1px solid #555') + .style('padding', '0.5em') + .style('border-radius', '0.2em') + .style('margin', '0 0.1em'); + overlap_div .append('span') .html( - 'Note:' + + 'Note: ' + chart.overlap_ids.length + - ' points overlap with the clicked point:' + ' points overlap with the clicked point. Click an ID for details: ' ); var overlap_ul = overlap_div .append('ul') .style('list-style', 'none') .style('display', 'inline-block'); + overlap_ul .selectAll('li') .data(chart.overlap_ids) @@ -2176,7 +2188,30 @@ return d; }) .on('click', function(d) { + //click an overlapping ID to see details for that participant console.log('changing to participant:', d); + 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); }); } } diff --git a/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js b/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js index 2a9b0b6..ea58e41 100644 --- a/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js +++ b/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js @@ -1,7 +1,14 @@ +import { select } from 'd3'; +import clearSelected from './clearSelected'; +import highlightSelected from './highlightSelected'; +import smallMultiples from './smallMultiples'; + export default function checkOverlap(d, chart) { console.log(chart); - chart.multiples.container.select('div.overlapNote').remove(); + chart.wrap.select('div.overlapNote').remove(); + + // Get the position of the clicked point const click_x = d3 .select(this) .select('circle') @@ -15,6 +22,8 @@ export default function checkOverlap(d, chart) { .select('circle') .attr('r'); const click_id = d.values.raw[0][chart.config.id_col]; + + // See if any other points overlap chart.overlap_ids = chart.points .filter(function(f) { const point_id = f.values.raw[0][chart.config.id_col]; @@ -39,22 +48,29 @@ export default function checkOverlap(d, chart) { .map(d => d.values.raw[0][chart.config.id_col]); console.log(chart.overlap_ids); - + // If there are overlapping points, add a note in the details section. if (chart.overlap_ids.length) { - const overlap_div = chart.multiples.container - .insert('div', '*') - .attr('class', 'overlapNote'); + const overlap_div = chart.wrap + .insert('div', 'div.multiples') + .attr('class', 'overlapNote') + .style('background-color', '#999') + .style('border', '1px solid #555') + .style('padding', '0.5em') + .style('border-radius', '0.2em') + .style('margin', '0 0.1em'); + overlap_div .append('span') .html( - 'Note:' + + 'Note: ' + chart.overlap_ids.length + - ' points overlap with the clicked point. Click an ID for details:' + ' points overlap with the clicked point. Click an ID for details: ' ); const overlap_ul = overlap_div .append('ul') .style('list-style', 'none') .style('display', 'inline-block'); + overlap_ul .selectAll('li') .data(chart.overlap_ids) @@ -67,7 +83,28 @@ export default function checkOverlap(d, chart) { .style('cursor', 'pointer') .text(d => d) .on('click', function(d) { + //click an overlapping ID to see details for that participant console.log('changing to participant:', d); + 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); }); } } diff --git a/src/callbacks/onResize/addEventListeners/functions/smallMultiples/updateParticipantDropdown.js b/src/callbacks/onResize/addEventListeners/functions/smallMultiples/updateParticipantDropdown.js index b3a5c90..76357aa 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); + chart.wrap.select('div.overlapNote').remove(); //Trigger participantsSelected event context.participantsSelected = [context.selected_id]; From e95a6c1addc1bb11357ec22926a9674a579792d7 Mon Sep 17 00:00:00 2001 From: jwildfire Date: Tue, 13 Aug 2019 22:56:07 -0700 Subject: [PATCH 03/12] update version and rebuild --- package-lock.json | 2 +- package.json | 2 +- safetyOutlierExplorer.js | 13 +++++++++++++ src/util/polyfills.js | 25 ++++++++++++------------- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9fd1fe5..d270964 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": { 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..d483281 100644 --- a/safetyOutlierExplorer.js +++ b/safetyOutlierExplorer.js @@ -133,6 +133,19 @@ 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() { return this.each(function() { 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; +})(); From b4f2626e9c6da31422e089542d410ac66a16b32e Mon Sep 17 00:00:00 2001 From: jwildfire Date: Wed, 14 Aug 2019 14:52:52 -0700 Subject: [PATCH 04/12] clean up overlap logic. fix #132 --- safetyOutlierExplorer.js | 117 ++++++++---------- .../onLayout/addSmallMultiplesContainer.js | 1 - src/callbacks/onResize.js | 4 - .../addLineEventListeners.js | 3 - .../addOverlayEventListener.js | 2 + .../addPointEventListeners.js | 3 - .../functions/checkOverlap.js | 74 ++++++----- .../functions/highlightHovered.js | 2 +- .../functions/highlightSelected.js | 2 +- .../functions/reorderMarks.js | 17 --- .../updateParticipantDropdown.js | 2 +- src/callbacks/onResize/orderPoints.js | 9 -- src/configuration/controlInputs.js | 62 ---------- src/configuration/index.js | 14 --- src/configuration/rendererSettings.js | 79 ------------ src/configuration/syncControlInputs.js | 35 ------ src/configuration/syncSettings.js | 82 ------------ src/configuration/webchartsSettings.js | 43 ------- 18 files changed, 102 insertions(+), 449 deletions(-) delete mode 100644 src/callbacks/onResize/addEventListeners/functions/reorderMarks.js delete mode 100644 src/callbacks/onResize/orderPoints.js delete mode 100644 src/configuration/controlInputs.js delete mode 100644 src/configuration/index.js delete mode 100644 src/configuration/rendererSettings.js delete mode 100644 src/configuration/syncControlInputs.js delete mode 100644 src/configuration/syncSettings.js delete mode 100644 src/configuration/webchartsSettings.js diff --git a/safetyOutlierExplorer.js b/safetyOutlierExplorer.js index bf955d4..f79e07b 100644 --- a/safetyOutlierExplorer.js +++ b/safetyOutlierExplorer.js @@ -1151,7 +1151,6 @@ .append('div') .classed('multiples', true) .style({ - 'border-top': '1px solid #ccc', 'padding-top': '10px' }), id: null @@ -1528,7 +1527,7 @@ .select('circle') .attr({ r: function r(d) { - return d.radius * 1.5; + return d.radius; }, stroke: 'black', 'stroke-width': function strokeWidth(d) { @@ -1622,6 +1621,7 @@ function addOverlayEventListener() { var _this = this; + var context = this; this.overlay .on('mouseover', function() { clearHovered.call(_this); @@ -1629,6 +1629,7 @@ .on('click', function() { clearHovered.call(_this); clearSelected.call(_this); + context.wrap.select('div.overlapNote').remove(); }); } @@ -1666,7 +1667,7 @@ .select('circle') .attr({ r: function r(d) { - return d.radius * 1.25; + return d.radius; }, stroke: 'black', 'stroke-width': function strokeWidth(d) { @@ -1675,30 +1676,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) { @@ -2039,7 +2016,7 @@ context.selected_id = context.multiples.id; highlightSelected.call(context); smallMultiples.call(context); - chart.wrap.select('div.overlapNote').remove(); + context.wrap.select('div.overlapNote').remove(); //Trigger participantsSelected event context.participantsSelected = [context.selected_id]; @@ -2090,11 +2067,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 @@ -2105,7 +2078,31 @@ } function checkOverlap(d, chart) { - console.log(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(); @@ -2150,14 +2147,13 @@ return d.values.raw[0][chart.config.id_col]; }); - console.log(chart.overlap_ids); // If there are overlapping points, add a note in the details section. if (chart.overlap_ids.length) { var overlap_div = chart.wrap .insert('div', 'div.multiples') .attr('class', 'overlapNote') - .style('background-color', '#999') - .style('border', '1px solid #555') + .style('background-color', '#eee') + .style('border', '1px solid #999') .style('padding', '0.5em') .style('border-radius', '0.2em') .style('margin', '0 0.1em'); @@ -2167,8 +2163,17 @@ .html( 'Note: ' + chart.overlap_ids.length + - ' points overlap with the clicked point. Click an ID for details: ' + ' points overlap 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); var overlap_ul = overlap_div .append('ul') .style('list-style', 'none') @@ -2181,37 +2186,21 @@ .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', function(d) { - //click an overlapping ID to see details for that participant - console.log('changing to participant:', d); - 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); + .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); }); } } @@ -2233,11 +2222,7 @@ clearHovered.call(chart); clearSelected.call(chart); chart.selected_id = d.values.raw[0][chart.config.id_col]; - chart.selected_id_order = chart.IDOrder.find(function(di) { - return di.ID === chart.selected_id; - }).order; highlightSelected.call(chart); - reorderMarks.call(chart); smallMultiples.call(chart); //Trigger participantsSelected event 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 2ffe09e..d06abed 100644 --- a/src/callbacks/onResize/addEventListeners/addPointEventListeners.js +++ b/src/callbacks/onResize/addEventListeners/addPointEventListeners.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'; import checkOverlap from './functions/checkOverlap'; export default function addPointEventListeners() { @@ -20,9 +19,7 @@ export default function addPointEventListeners() { clearHovered.call(chart); clearSelected.call(chart); chart.selected_id = d.values.raw[0][chart.config.id_col]; - chart.selected_id_order = chart.IDOrder.find(di => di.ID === chart.selected_id).order; highlightSelected.call(chart); - reorderMarks.call(chart); smallMultiples.call(chart); //Trigger participantsSelected event diff --git a/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js b/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js index ea58e41..af87b1b 100644 --- a/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js +++ b/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js @@ -2,9 +2,33 @@ import { select } from 'd3'; import clearSelected from './clearSelected'; import highlightSelected from './highlightSelected'; import smallMultiples from './smallMultiples'; +import clearHovered from './clearHovered'; +import highlightHovered from './highlightHovered'; export default function checkOverlap(d, chart) { - console.log(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(); @@ -47,14 +71,13 @@ export default function checkOverlap(d, chart) { .data() .map(d => d.values.raw[0][chart.config.id_col]); - console.log(chart.overlap_ids); // If there are overlapping points, add a note in the details section. if (chart.overlap_ids.length) { const overlap_div = chart.wrap .insert('div', 'div.multiples') .attr('class', 'overlapNote') - .style('background-color', '#999') - .style('border', '1px solid #555') + .style('background-color', '#eee') + .style('border', '1px solid #999') .style('padding', '0.5em') .style('border-radius', '0.2em') .style('margin', '0 0.1em'); @@ -64,8 +87,17 @@ export default function checkOverlap(d, chart) { .html( 'Note: ' + chart.overlap_ids.length + - ' points overlap with the clicked point. Click an ID for details: ' + ' points overlap 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); const overlap_ul = overlap_div .append('ul') .style('list-style', 'none') @@ -78,33 +110,19 @@ export default function checkOverlap(d, chart) { .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', function(d) { - //click an overlapping ID to see details for that participant - console.log('changing to participant:', d); - 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); + .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/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 76357aa..ccaaeb8 100644 --- a/src/callbacks/onResize/addEventListeners/functions/smallMultiples/updateParticipantDropdown.js +++ b/src/callbacks/onResize/addEventListeners/functions/smallMultiples/updateParticipantDropdown.js @@ -27,7 +27,7 @@ export default function updateParticipantDropdown() { context.selected_id = context.multiples.id; highlightSelected.call(context); smallMultiples.call(context); - chart.wrap.select('div.overlapNote').remove(); + 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 deleted file mode 100644 index 714563c..0000000 --- a/src/configuration/controlInputs.js +++ /dev/null @@ -1,62 +0,0 @@ -export default function controlInputs() { - return [ - { - type: 'subsetter', - value_col: 'soe_measure', // set in syncControlInputs() - label: 'Measure', - start: null - }, - { - type: 'dropdown', - option: 'x.column', - label: 'X-axis', - require: true - }, - { - type: 'number', - option: 'y.domain[0]', - label: 'Lower', - require: true - }, - { - type: 'number', - option: 'y.domain[1]', - label: 'Upper', - require: true - }, - { - type: 'dropdown', - option: 'normal_range_method', - label: 'Method', - values: ['None', 'LLN-ULN', 'Standard Deviation', 'Quantiles'], - require: true - }, - { - type: 'number', - option: 'normal_range_sd', - label: '# Std. Dev.' - }, - { - type: 'number', - label: 'Lower', - option: 'normal_range_quantile_low' - }, - { - type: 'number', - label: 'Upper', - option: 'normal_range_quantile_high' - }, - { - type: 'checkbox', - inline: true, - option: 'visits_without_data', - label: 'Without Data' - }, - { - type: 'checkbox', - inline: true, - option: 'unscheduled_visits', - label: 'Unscheduled' - } - ]; -} diff --git a/src/configuration/index.js b/src/configuration/index.js deleted file mode 100644 index bdc9f95..0000000 --- a/src/configuration/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import rendererSettings from './rendererSettings'; -import webchartsSettings from './webchartsSettings'; -import syncSettings from './syncSettings'; -import controlInputs from './controlInputs'; -import syncControlInputs from './syncControlInputs'; - -export default { - rendererSettings, - webchartsSettings, - settings: Object.assign({}, rendererSettings(), webchartsSettings()), - syncSettings, - controlInputs, - syncControlInputs -}; diff --git a/src/configuration/rendererSettings.js b/src/configuration/rendererSettings.js deleted file mode 100644 index 456ae8f..0000000 --- a/src/configuration/rendererSettings.js +++ /dev/null @@ -1,79 +0,0 @@ -export default function rendererSettings() { - return { - //participant - id_col: 'USUBJID', - details: [ - { value_col: 'AGE', label: 'Age' }, - { value_col: 'SEX', label: 'Sex' }, - { value_col: 'RACE', label: 'Race' } - ], - - //timing - time_cols: [ - { - type: 'ordinal', - value_col: 'VISIT', - label: 'Visit', - order_col: 'VISITNUM', - order: null, - rotate_tick_labels: true, - vertical_space: 100 - }, - { - type: 'linear', - value_col: 'DY', - label: 'Study Day', - order_col: 'DY', - order: null, - rotate_tick_labels: false, - vertical_space: 0 - } - ], - visits_without_data: false, - unscheduled_visits: false, - unscheduled_visit_pattern: '/unscheduled|early termination/i', - unscheduled_visit_values: null, // takes precedence over unscheduled_visit_pattern - - //measure - measure_col: 'TEST', - start_value: null, - unit_col: 'STRESU', - - //result - value_col: 'STRESN', - - //normal range - normal_col_low: 'STNRLO', - normal_col_high: 'STNRHI', - normal_range_method: 'LLN-ULN', - normal_range_sd: 1.96, - normal_range_quantile_low: 0.05, - normal_range_quantile_high: 0.95, - - //filters - filters: null, - - //marks - line_attributes: { - stroke: 'black', - 'stroke-width': 0.5, - 'stroke-opacity': 0.75 - }, - point_attributes: { - stroke: '#1f78b4', - 'stroke-width': 0.5, - 'stroke-opacity': 1, - radius: 3, - fill: '#1f78b4', - 'fill-opacity': 0.2 - }, - tooltip_cols: null, - custom_marks: null, - - //multiples - multiples_sizing: { - width: 300, - height: 100 - } - }; -} diff --git a/src/configuration/syncControlInputs.js b/src/configuration/syncControlInputs.js deleted file mode 100644 index 93e2126..0000000 --- a/src/configuration/syncControlInputs.js +++ /dev/null @@ -1,35 +0,0 @@ -export default function syncControlInputs(controlInputs, settings) { - const xAxisControl = controlInputs.find(d => d.label === 'X-axis'); - xAxisControl.values = settings.time_cols.map(d => d.value_col); - - if (settings.filters) { - settings.filters.forEach(function(d, i) { - const thisFilter = { - type: 'subsetter', - value_col: d.value_col ? d.value_col : d, - label: d.label ? d.label : d.value_col ? d.value_col : d - }; - //add the filter to the control inputs (as long as it isn't already there) - var current_value_cols = controlInputs - .filter(f => f.type == 'subsetter') - .map(m => m.value_col); - if (current_value_cols.indexOf(thisFilter.value_col) == -1) - controlInputs.splice(4 + i, 0, thisFilter); - }); - } - - //Remove unscheduled visit control if unscheduled visit pattern is unscpecified. - if ( - !settings.unscheduled_visit_regex && - !( - Array.isArray(settings.unscheduled_visit_values) && - settings.unscheduled_visit_values.length - ) - ) - controlInputs.splice( - controlInputs.map(controlInput => controlInput.label).indexOf('Unscheduled Visits'), - 1 - ); - - return controlInputs; -} diff --git a/src/configuration/syncSettings.js b/src/configuration/syncSettings.js deleted file mode 100644 index 0e91b36..0000000 --- a/src/configuration/syncSettings.js +++ /dev/null @@ -1,82 +0,0 @@ -export default function syncSettings(settings) { - const time_col = settings.time_cols[0]; - - //handle a string arguments to array settings - const array_settings = ['filters', 'details', 'tooltip_cols']; - array_settings.forEach(function(s) { - if (!(settings[s] instanceof Array)) - settings[s] = typeof settings[s] === 'string' ? [settings[s]] : []; - }); - - //x-axis - settings.x.column = time_col.value_col; - settings.x.type = time_col.type; - settings.x.label = time_col.label; - settings.x.order = time_col.order; - - //y-axis - settings.y.column = settings.value_col; - - //lines - const lines = settings.marks.find(mark => mark.type === 'line'); - lines.per = [settings.id_col, settings.measure_col]; - lines.tooltip = `[${settings.id_col}]`; - Object.assign(lines.attributes, settings.line_attributes); - lines.attributes['stroke-width'] = settings.line_attributes['stroke-width'] || 0.5; - - //points - const points = settings.marks.find(mark => mark.type === 'circle'); - points.per = [settings.id_col, settings.measure_col, time_col.value_col, settings.value_col]; - points.tooltip = `Participant = [${settings.id_col}]\n[${settings.measure_col}] = [${settings.value_col}] [${settings.unit_col}]\n${settings.x.label} = [${settings.x.column}]`; - - //add custom tooltip values - if (settings.tooltip_cols) { - settings.tooltip_cols.forEach(function(tooltip) { - var obj = typeof tooltip == 'string' ? { label: tooltip, value_col: tooltip } : tooltip; - points.tooltip = points.tooltip + `\n${obj.label} = [${obj.value_col}]`; - }); - } - - Object.assign(points.attributes, settings.point_attributes); - points.radius = settings.point_attributes.radius || 3; - - //Add custom marks to settings.marks. - if (Array.isArray(settings.custom_marks) && settings.custom_marks.length) - settings.custom_marks.forEach(mark => { - if (mark instanceof Object) { - mark.default = false; // distinguish custom marks from default marks - if (mark.type === 'line') - mark.attributes = Object.assign({}, lines.attributes, mark.attributes); - else if (mark.type === 'circle') { - mark.attributes = Object.assign({}, points.attributes, mark.attributes); - mark.radius = mark.radius || points.radius; - } - settings.marks.push(mark); - } - }); - - //Define margins for box plot and rotated x-axis tick labels. - if (settings.margin) settings.margin.bottom = time_col.vertical_space; - else - settings.margin = { - right: 20, - bottom: time_col.vertical_space - }; - - settings.rotate_x_tick_labels = time_col.rotate_tick_labels; - - //Convert unscheduled_visit_pattern from string to regular expression. - if ( - typeof settings.unscheduled_visit_pattern === 'string' && - settings.unscheduled_visit_pattern !== '' - ) { - const flags = settings.unscheduled_visit_pattern.replace(/.*?\/([gimy]*)$/, '$1'), - pattern = settings.unscheduled_visit_pattern.replace( - new RegExp('^/(.*?)/' + flags + '$'), - '$1' - ); - settings.unscheduled_visit_regex = new RegExp(pattern, flags); - } - - return settings; -} diff --git a/src/configuration/webchartsSettings.js b/src/configuration/webchartsSettings.js deleted file mode 100644 index b0882d9..0000000 --- a/src/configuration/webchartsSettings.js +++ /dev/null @@ -1,43 +0,0 @@ -export default function webchartsSettings() { - return { - x: { - column: null, // set in ./syncSettings - type: null, // set in ./syncSettings - behavior: 'raw' - }, - y: { - column: null, // set in ./syncSettings - stat: 'mean', - type: 'linear', - label: 'Value', - behavior: 'raw' - }, - marks: [ - { - per: null, // set in ./syncSettings - type: 'line', - attributes: { - 'clip-path': null // set in ./syncSettings - }, - tooltip: null, // set in ./syncSettings - default: true - }, - { - per: null, // set in ./syncSettings - type: 'circle', - attributes: { - 'clip-path': null // set in ./syncSettings - }, - tooltip: null, // set in ./syncSettings - default: true - } - ], - resizable: true, - margin: { - right: 30, // create space for box plot - left: 60 - }, - gridlines: 'y', - aspect: 3 - }; -} From 4d039f648d6650ec5da2b664aa6faa2e864097e8 Mon Sep 17 00:00:00 2001 From: jwildfire Date: Wed, 14 Aug 2019 14:57:17 -0700 Subject: [PATCH 05/12] undo configuration folder delete --- safetyOutlierExplorer.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/safetyOutlierExplorer.js b/safetyOutlierExplorer.js index f79e07b..c228494 100644 --- a/safetyOutlierExplorer.js +++ b/safetyOutlierExplorer.js @@ -1564,22 +1564,6 @@ } } - 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() { @@ -2393,9 +2377,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); From d7fd4013a6461371ab9ce0e21df6b8d73b5a741b Mon Sep 17 00:00:00 2001 From: jwildfire Date: Wed, 14 Aug 2019 14:57:36 -0700 Subject: [PATCH 06/12] undo configuration folder delete --- src/configuration/controlInputs.js | 62 +++++++++++++++++++ src/configuration/index.js | 14 +++++ src/configuration/rendererSettings.js | 79 +++++++++++++++++++++++++ src/configuration/syncControlInputs.js | 35 +++++++++++ src/configuration/syncSettings.js | 82 ++++++++++++++++++++++++++ src/configuration/webchartsSettings.js | 43 ++++++++++++++ 6 files changed, 315 insertions(+) create mode 100755 src/configuration/controlInputs.js create mode 100755 src/configuration/index.js create mode 100755 src/configuration/rendererSettings.js create mode 100755 src/configuration/syncControlInputs.js create mode 100755 src/configuration/syncSettings.js create mode 100755 src/configuration/webchartsSettings.js diff --git a/src/configuration/controlInputs.js b/src/configuration/controlInputs.js new file mode 100755 index 0000000..714563c --- /dev/null +++ b/src/configuration/controlInputs.js @@ -0,0 +1,62 @@ +export default function controlInputs() { + return [ + { + type: 'subsetter', + value_col: 'soe_measure', // set in syncControlInputs() + label: 'Measure', + start: null + }, + { + type: 'dropdown', + option: 'x.column', + label: 'X-axis', + require: true + }, + { + type: 'number', + option: 'y.domain[0]', + label: 'Lower', + require: true + }, + { + type: 'number', + option: 'y.domain[1]', + label: 'Upper', + require: true + }, + { + type: 'dropdown', + option: 'normal_range_method', + label: 'Method', + values: ['None', 'LLN-ULN', 'Standard Deviation', 'Quantiles'], + require: true + }, + { + type: 'number', + option: 'normal_range_sd', + label: '# Std. Dev.' + }, + { + type: 'number', + label: 'Lower', + option: 'normal_range_quantile_low' + }, + { + type: 'number', + label: 'Upper', + option: 'normal_range_quantile_high' + }, + { + type: 'checkbox', + inline: true, + option: 'visits_without_data', + label: 'Without Data' + }, + { + type: 'checkbox', + inline: true, + option: 'unscheduled_visits', + label: 'Unscheduled' + } + ]; +} diff --git a/src/configuration/index.js b/src/configuration/index.js new file mode 100755 index 0000000..bdc9f95 --- /dev/null +++ b/src/configuration/index.js @@ -0,0 +1,14 @@ +import rendererSettings from './rendererSettings'; +import webchartsSettings from './webchartsSettings'; +import syncSettings from './syncSettings'; +import controlInputs from './controlInputs'; +import syncControlInputs from './syncControlInputs'; + +export default { + rendererSettings, + webchartsSettings, + settings: Object.assign({}, rendererSettings(), webchartsSettings()), + syncSettings, + controlInputs, + syncControlInputs +}; diff --git a/src/configuration/rendererSettings.js b/src/configuration/rendererSettings.js new file mode 100755 index 0000000..456ae8f --- /dev/null +++ b/src/configuration/rendererSettings.js @@ -0,0 +1,79 @@ +export default function rendererSettings() { + return { + //participant + id_col: 'USUBJID', + details: [ + { value_col: 'AGE', label: 'Age' }, + { value_col: 'SEX', label: 'Sex' }, + { value_col: 'RACE', label: 'Race' } + ], + + //timing + time_cols: [ + { + type: 'ordinal', + value_col: 'VISIT', + label: 'Visit', + order_col: 'VISITNUM', + order: null, + rotate_tick_labels: true, + vertical_space: 100 + }, + { + type: 'linear', + value_col: 'DY', + label: 'Study Day', + order_col: 'DY', + order: null, + rotate_tick_labels: false, + vertical_space: 0 + } + ], + visits_without_data: false, + unscheduled_visits: false, + unscheduled_visit_pattern: '/unscheduled|early termination/i', + unscheduled_visit_values: null, // takes precedence over unscheduled_visit_pattern + + //measure + measure_col: 'TEST', + start_value: null, + unit_col: 'STRESU', + + //result + value_col: 'STRESN', + + //normal range + normal_col_low: 'STNRLO', + normal_col_high: 'STNRHI', + normal_range_method: 'LLN-ULN', + normal_range_sd: 1.96, + normal_range_quantile_low: 0.05, + normal_range_quantile_high: 0.95, + + //filters + filters: null, + + //marks + line_attributes: { + stroke: 'black', + 'stroke-width': 0.5, + 'stroke-opacity': 0.75 + }, + point_attributes: { + stroke: '#1f78b4', + 'stroke-width': 0.5, + 'stroke-opacity': 1, + radius: 3, + fill: '#1f78b4', + 'fill-opacity': 0.2 + }, + tooltip_cols: null, + custom_marks: null, + + //multiples + multiples_sizing: { + width: 300, + height: 100 + } + }; +} diff --git a/src/configuration/syncControlInputs.js b/src/configuration/syncControlInputs.js new file mode 100755 index 0000000..93e2126 --- /dev/null +++ b/src/configuration/syncControlInputs.js @@ -0,0 +1,35 @@ +export default function syncControlInputs(controlInputs, settings) { + const xAxisControl = controlInputs.find(d => d.label === 'X-axis'); + xAxisControl.values = settings.time_cols.map(d => d.value_col); + + if (settings.filters) { + settings.filters.forEach(function(d, i) { + const thisFilter = { + type: 'subsetter', + value_col: d.value_col ? d.value_col : d, + label: d.label ? d.label : d.value_col ? d.value_col : d + }; + //add the filter to the control inputs (as long as it isn't already there) + var current_value_cols = controlInputs + .filter(f => f.type == 'subsetter') + .map(m => m.value_col); + if (current_value_cols.indexOf(thisFilter.value_col) == -1) + controlInputs.splice(4 + i, 0, thisFilter); + }); + } + + //Remove unscheduled visit control if unscheduled visit pattern is unscpecified. + if ( + !settings.unscheduled_visit_regex && + !( + Array.isArray(settings.unscheduled_visit_values) && + settings.unscheduled_visit_values.length + ) + ) + controlInputs.splice( + controlInputs.map(controlInput => controlInput.label).indexOf('Unscheduled Visits'), + 1 + ); + + return controlInputs; +} diff --git a/src/configuration/syncSettings.js b/src/configuration/syncSettings.js new file mode 100755 index 0000000..0e91b36 --- /dev/null +++ b/src/configuration/syncSettings.js @@ -0,0 +1,82 @@ +export default function syncSettings(settings) { + const time_col = settings.time_cols[0]; + + //handle a string arguments to array settings + const array_settings = ['filters', 'details', 'tooltip_cols']; + array_settings.forEach(function(s) { + if (!(settings[s] instanceof Array)) + settings[s] = typeof settings[s] === 'string' ? [settings[s]] : []; + }); + + //x-axis + settings.x.column = time_col.value_col; + settings.x.type = time_col.type; + settings.x.label = time_col.label; + settings.x.order = time_col.order; + + //y-axis + settings.y.column = settings.value_col; + + //lines + const lines = settings.marks.find(mark => mark.type === 'line'); + lines.per = [settings.id_col, settings.measure_col]; + lines.tooltip = `[${settings.id_col}]`; + Object.assign(lines.attributes, settings.line_attributes); + lines.attributes['stroke-width'] = settings.line_attributes['stroke-width'] || 0.5; + + //points + const points = settings.marks.find(mark => mark.type === 'circle'); + points.per = [settings.id_col, settings.measure_col, time_col.value_col, settings.value_col]; + points.tooltip = `Participant = [${settings.id_col}]\n[${settings.measure_col}] = [${settings.value_col}] [${settings.unit_col}]\n${settings.x.label} = [${settings.x.column}]`; + + //add custom tooltip values + if (settings.tooltip_cols) { + settings.tooltip_cols.forEach(function(tooltip) { + var obj = typeof tooltip == 'string' ? { label: tooltip, value_col: tooltip } : tooltip; + points.tooltip = points.tooltip + `\n${obj.label} = [${obj.value_col}]`; + }); + } + + Object.assign(points.attributes, settings.point_attributes); + points.radius = settings.point_attributes.radius || 3; + + //Add custom marks to settings.marks. + if (Array.isArray(settings.custom_marks) && settings.custom_marks.length) + settings.custom_marks.forEach(mark => { + if (mark instanceof Object) { + mark.default = false; // distinguish custom marks from default marks + if (mark.type === 'line') + mark.attributes = Object.assign({}, lines.attributes, mark.attributes); + else if (mark.type === 'circle') { + mark.attributes = Object.assign({}, points.attributes, mark.attributes); + mark.radius = mark.radius || points.radius; + } + settings.marks.push(mark); + } + }); + + //Define margins for box plot and rotated x-axis tick labels. + if (settings.margin) settings.margin.bottom = time_col.vertical_space; + else + settings.margin = { + right: 20, + bottom: time_col.vertical_space + }; + + settings.rotate_x_tick_labels = time_col.rotate_tick_labels; + + //Convert unscheduled_visit_pattern from string to regular expression. + if ( + typeof settings.unscheduled_visit_pattern === 'string' && + settings.unscheduled_visit_pattern !== '' + ) { + const flags = settings.unscheduled_visit_pattern.replace(/.*?\/([gimy]*)$/, '$1'), + pattern = settings.unscheduled_visit_pattern.replace( + new RegExp('^/(.*?)/' + flags + '$'), + '$1' + ); + settings.unscheduled_visit_regex = new RegExp(pattern, flags); + } + + return settings; +} diff --git a/src/configuration/webchartsSettings.js b/src/configuration/webchartsSettings.js new file mode 100755 index 0000000..b0882d9 --- /dev/null +++ b/src/configuration/webchartsSettings.js @@ -0,0 +1,43 @@ +export default function webchartsSettings() { + return { + x: { + column: null, // set in ./syncSettings + type: null, // set in ./syncSettings + behavior: 'raw' + }, + y: { + column: null, // set in ./syncSettings + stat: 'mean', + type: 'linear', + label: 'Value', + behavior: 'raw' + }, + marks: [ + { + per: null, // set in ./syncSettings + type: 'line', + attributes: { + 'clip-path': null // set in ./syncSettings + }, + tooltip: null, // set in ./syncSettings + default: true + }, + { + per: null, // set in ./syncSettings + type: 'circle', + attributes: { + 'clip-path': null // set in ./syncSettings + }, + tooltip: null, // set in ./syncSettings + default: true + } + ], + resizable: true, + margin: { + right: 30, // create space for box plot + left: 60 + }, + gridlines: 'y', + aspect: 3 + }; +} From 5209e824963413e247a8da648d1c315e91263739 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 19 Aug 2019 11:10:07 -0400 Subject: [PATCH 07/12] tweak checkOverlap.js --- package-lock.json | 10 ++++++++-- safetyOutlierExplorer.js | 11 ++++++++++- src/callbacks/onDraw/resetChart.js | 1 + .../addEventListeners/functions/checkOverlap.js | 10 +++++++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9fd1fe5..0da0fc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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/safetyOutlierExplorer.js b/safetyOutlierExplorer.js index c228494..5ea6d74 100644 --- a/safetyOutlierExplorer.js +++ b/safetyOutlierExplorer.js @@ -1450,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) @@ -2157,7 +2158,15 @@ .style('color', 'blue') .style('text-decoration', 'underline') .style('cursor', 'pointer') - .on('click', showID); + .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') 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/onResize/addEventListeners/functions/checkOverlap.js b/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js index af87b1b..dbdeed9 100644 --- a/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js +++ b/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js @@ -97,7 +97,15 @@ export default function checkOverlap(d, chart) { .style('color', 'blue') .style('text-decoration', 'underline') .style('cursor', 'pointer') - .on('click', showID); + .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') From 1b79d357a6c9e3eaac8b0058b28146a1817eac41 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 19 Aug 2019 11:52:53 -0400 Subject: [PATCH 08/12] tweak verbiage --- safetyOutlierExplorer.js | 6 +++++- .../onResize/addEventListeners/functions/checkOverlap.js | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/safetyOutlierExplorer.js b/safetyOutlierExplorer.js index 5ea6d74..b21fba8 100644 --- a/safetyOutlierExplorer.js +++ b/safetyOutlierExplorer.js @@ -2148,7 +2148,11 @@ .html( 'Note: ' + chart.overlap_ids.length + - ' points overlap the clicked point for ' + + (' 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: ' ); diff --git a/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js b/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js index dbdeed9..c559b45 100644 --- a/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js +++ b/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js @@ -87,7 +87,9 @@ export default function checkOverlap(d, chart) { .html( 'Note: ' + chart.overlap_ids.length + - ' points overlap the clicked point for ' + + ` 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: ' ); From d5610d7a403a0986583604a8023e8ea252ce3d1c Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 19 Aug 2019 13:06:35 -0400 Subject: [PATCH 09/12] removing padding from overlapping points ul --- safetyOutlierExplorer.js | 1 + .../onResize/addEventListeners/functions/checkOverlap.js | 1 + 2 files changed, 2 insertions(+) diff --git a/safetyOutlierExplorer.js b/safetyOutlierExplorer.js index b21fba8..183267a 100644 --- a/safetyOutlierExplorer.js +++ b/safetyOutlierExplorer.js @@ -2174,6 +2174,7 @@ var overlap_ul = overlap_div .append('ul') .style('list-style', 'none') + .style('padding', '0') .style('display', 'inline-block'); overlap_ul diff --git a/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js b/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js index c559b45..245c0e2 100644 --- a/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js +++ b/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js @@ -111,6 +111,7 @@ export default function checkOverlap(d, chart) { const overlap_ul = overlap_div .append('ul') .style('list-style', 'none') + .style('padding', '0') .style('display', 'inline-block'); overlap_ul From 058596f64f3e6c89274f06392564d7da109ebbfb Mon Sep 17 00:00:00 2001 From: jwildfire Date: Mon, 19 Aug 2019 14:59:38 -0700 Subject: [PATCH 10/12] update title with overlap info. fix #141 --- package-lock.json | 10 +- safetyOutlierExplorer.js | 95 +++-- .../addPointEventListeners.js | 15 +- .../{checkOverlap.js => addOverlapNote.js} | 45 +-- .../functions/addOverlapTitle.js | 18 + .../functions/checkPointOverlap.js | 42 +++ .../addEventListeners/functions/index.html | 41 +++ .../addEventListeners/functions/index.js | 342 ++++++++++++++++++ 8 files changed, 521 insertions(+), 87 deletions(-) rename src/callbacks/onResize/addEventListeners/functions/{checkOverlap.js => addOverlapNote.js} (73%) create mode 100644 src/callbacks/onResize/addEventListeners/functions/addOverlapTitle.js create mode 100644 src/callbacks/onResize/addEventListeners/functions/checkPointOverlap.js create mode 100644 src/callbacks/onResize/addEventListeners/functions/index.html create mode 100644 src/callbacks/onResize/addEventListeners/functions/index.js diff --git a/package-lock.json b/package-lock.json index 8937794..d270964 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1002,13 +1002,12 @@ }, "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": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "dev": true }, @@ -1104,7 +1103,7 @@ }, "printj": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", + "resolved": "http://registry.npmjs.org/printj/-/printj-1.1.2.tgz", "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==" }, "private": { @@ -1149,7 +1148,6 @@ }, "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": { @@ -1160,13 +1158,11 @@ }, "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": { @@ -1279,7 +1275,6 @@ }, "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 }, @@ -1291,7 +1286,6 @@ }, "util-deprecate": { "version": "1.0.2", - "resolved": false, "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "optional": true }, diff --git a/safetyOutlierExplorer.js b/safetyOutlierExplorer.js index 183267a..cd9641d 100644 --- a/safetyOutlierExplorer.js +++ b/safetyOutlierExplorer.js @@ -2062,35 +2062,7 @@ }); } - function checkOverlap(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(); - + function checkPointOverlap(d, chart) { // Get the position of the clicked point var click_x = d3 .select(this) @@ -2107,7 +2079,7 @@ var click_id = d.values.raw[0][chart.config.id_col]; // See if any other points overlap - chart.overlap_ids = chart.points + var overlap_ids = chart.points .filter(function(f) { var point_id = f.values.raw[0][chart.config.id_col]; var point_x = d3 @@ -2132,8 +2104,45 @@ 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') @@ -2203,15 +2212,33 @@ } } + 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); @@ -2229,7 +2256,7 @@ chart.wrap.node().dispatchEvent(chart.events.participantsSelected); //check for overlapping points - checkOverlap.call(this, d, chart); + addOverlapNote.call(this, d, chart); }); } diff --git a/src/callbacks/onResize/addEventListeners/addPointEventListeners.js b/src/callbacks/onResize/addEventListeners/addPointEventListeners.js index d06abed..307e300 100644 --- a/src/callbacks/onResize/addEventListeners/addPointEventListeners.js +++ b/src/callbacks/onResize/addEventListeners/addPointEventListeners.js @@ -3,14 +3,17 @@ import highlightHovered from './functions/highlightHovered'; import clearSelected from './functions/clearSelected'; import highlightSelected from './functions/highlightSelected'; import smallMultiples from './functions/smallMultiples'; -import checkOverlap from './functions/checkOverlap'; +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); @@ -28,6 +31,6 @@ export default function addPointEventListeners() { chart.wrap.node().dispatchEvent(chart.events.participantsSelected); //check for overlapping points - checkOverlap.call(this, d, chart); + addOverlapNote.call(this, d, chart); }); } diff --git a/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js b/src/callbacks/onResize/addEventListeners/functions/addOverlapNote.js similarity index 73% rename from src/callbacks/onResize/addEventListeners/functions/checkOverlap.js rename to src/callbacks/onResize/addEventListeners/functions/addOverlapNote.js index 245c0e2..f74ec57 100644 --- a/src/callbacks/onResize/addEventListeners/functions/checkOverlap.js +++ b/src/callbacks/onResize/addEventListeners/functions/addOverlapNote.js @@ -4,8 +4,9 @@ import highlightSelected from './highlightSelected'; import smallMultiples from './smallMultiples'; import clearHovered from './clearHovered'; import highlightHovered from './highlightHovered'; +import checkPointOverlap from './checkPointOverlap'; -export default function checkOverlap(d, chart) { +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 @@ -32,47 +33,13 @@ export default function checkOverlap(d, chart) { chart.wrap.select('div.overlapNote').remove(); - // 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 - chart.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]); + // 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') diff --git a/src/callbacks/onResize/addEventListeners/functions/addOverlapTitle.js b/src/callbacks/onResize/addEventListeners/functions/addOverlapTitle.js new file mode 100644 index 0000000..9efb7ef --- /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/index.html b/src/callbacks/onResize/addEventListeners/functions/index.html new file mode 100644 index 0000000..6f3df12 --- /dev/null +++ b/src/callbacks/onResize/addEventListeners/functions/index.html @@ -0,0 +1,41 @@ + + + + Safety Explorer Suite: Test Page + + + + + + + + + + + + + + + + + + + + + + + +
Safety Explorer Suite
+
+ Test Page + +
+
+ + + + diff --git a/src/callbacks/onResize/addEventListeners/functions/index.js b/src/callbacks/onResize/addEventListeners/functions/index.js new file mode 100644 index 0000000..c7f9a39 --- /dev/null +++ b/src/callbacks/onResize/addEventListeners/functions/index.js @@ -0,0 +1,342 @@ +//Generate safety explorer given an array of data files. +function initSafetyExplorerSuite(settings, dataArray) { + d3.selectAll('#container *').remove(); + safetyExplorerSuite + .createExplorer( + '#container', // element + settings // settings + ) + .init( + clone(dataArray), // array of data files + true, // load .csv files? + true // SDTM-structured data files? + ); +} + +//Array of SDTM data files. +const dataArray = [ + { + type: 'DM', // demographics + path: + 'https://raw.githubusercontent.com/RhoInc/data-library/master/data/clinical-trials/sdtm/cdisc-pilot-01/dm.csv' + }, + { + type: 'AE', // adverse events + path: + 'https://raw.githubusercontent.com/RhoInc/data-library/master/data/clinical-trials/sdtm/cdisc-pilot-01/ae.csv' + }, + { + type: 'BDS', // basic data structure: labs + path: + 'https://raw.githubusercontent.com/RhoInc/data-library/master/data/clinical-trials/sdtm/cdisc-pilot-01/lb.csv' + }, + { + type: 'BDS', // basic data structure: vital signs + path: + 'https://raw.githubusercontent.com/RhoInc/data-library/master/data/clinical-trials/sdtm/cdisc-pilot-01/vs.csv' + } +]; + +//Get safety-explorer-suite version from window hash. +const version = window.location.hash ? window.location.hash.substring(1) : 'master'; +if (version !== 'master') loadLibrary(version); + +//Wait for settings (defined below) to be available. +const waitForSettings = setInterval(function() { + if (settings !== undefined) { + clearInterval(waitForSettings); + initSafetyExplorerSuite(settings, dataArray); + } +}, 1); + +/*------------------------------------------------------------------------------------------------\ + * Settings +\------------------------------------------------------------------------------------------------*/ + +const filters = [ + { value_col: 'SEX', label: 'Sex' }, + { value_col: 'ARM', label: 'Treatment Group' }, + { value_col: 'RACE', label: 'Race' }, + { value_col: 'SITEID', label: 'Site' } +]; +const settings = { + initial_renderer: + window && window.location && window.location.hash + ? window.location.hash.substring(1) + : null, // allow linking to each renderer + custom_settings: [ + { + renderer_name: 'aeexplorer', + variables: { + filters: [ + { + value_col: 'AESER', + label: 'Serious?', + type: 'event', + start: null + }, + { + value_col: 'AESEV', + label: 'Severity', + type: 'event', + start: null + }, + { + value_col: 'AEREL', + label: 'Relationship', + type: 'event', + start: null + }, + { + value_col: 'AEOUT', + label: 'Outcome', + type: 'event', + start: null + } + ].concat(clone(filters)) + }, + defaults: { + placeholderFlag: { + value_col: 'AETERM', + values: [''] + } + } + }, + { + renderer_name: 'ae-timelines', + filters: [ + { + value_col: 'AESER', + label: 'Serious?', + type: 'event', + start: null + }, + { + value_col: 'AESEV', + label: 'Severity', + type: 'event', + start: null + }, + { + value_col: 'AEREL', + label: 'Relationship', + type: 'event', + start: null + }, + { + value_col: 'AEOUT', + label: 'Outcome', + type: 'event', + start: null + } + ].concat(clone(filters)) + }, + { + renderer_name: 'safety-histogram', + filters: clone(filters), + displayNormalRange: true + }, + { + renderer_name: 'safety-outlier-explorer', + filters: clone(filters) + }, + { + renderer_name: 'paneled-outlier-explorer', + filters: clone(filters) + }, + { + renderer_name: 'safety-results-over-time', + groups: clone(filters), + filters: clone(filters) + }, + { + renderer_name: 'safety-shift-plot', + filters: clone(filters) + }, + { + renderer_name: 'hep-explorer', + group_cols: clone(filters), + filters: clone(filters), + measure_values: { + ALT: 'Alanine Aminotransferase', + AST: 'Aspartate Aminotransferase', + TB: 'Bilirubin', + ALP: 'Alkaline Phosphatase' + } + } + ] +}; + +/*------------------------------------------------------------------------------------------------\ + This code creates a dropdown of previous versions and open branches of safety-explorer-suite. +\------------------------------------------------------------------------------------------------*/ + +//Load branches. +const branchRequest = new XMLHttpRequest(); +branchRequest.onload = function() { + const branches = JSON.parse(this.responseText) + .map(function(branch) { + return branch.name; + }) + .sort(function(a, b) { + return a === 'master' + ? -1 + : b === 'master' + ? 1 + : /^dev-/.test(a) && /^dev-/.test(b) + ? a > b + ? -1 + : 1 + : /^dev-/.test(a) + ? -1 + : /^dev-/.test(b) + ? 1 + : a < b + ? -a + : 1; + }); + d3.select('.version-select__select') + .selectAll('option.branch') + .data(branches) + .enter() + .append('option') + .classed('branch', true) + .text(function(d) { + return d; + }) + .property('selected', function(d) { + return d === version; + }); +}; +branchRequest.open('get', 'https://api.github.com/repos/RhoInc/safety-explorer-suite/branches'); +branchRequest.send(); + +//Load releases. +const releaseRequest = new XMLHttpRequest(); +releaseRequest.onload = function() { + const releases = JSON.parse(this.responseText) + .map(function(release) { + return release.name; + }) + .sort(d3.descending); + d3.select('.version-select__select') + .selectAll('option.release') + .data(releases) + .enter() + .append('option') + .classed('release', true) + .text(function(d) { + return d; + }) + .property('selected', function(d) { + return d === version; + }); +}; +releaseRequest.open('get', 'https://api.github.com/repos/RhoInc/safety-explorer-suite/releases'); +releaseRequest.send(); + +//Add version select functionality. +d3.select('.version-select__submit').on('click', function() { + const version = d3 + .select('.version-select__select') + .select('option:checked') + .text(); + loadLibrary(version); +}); + +/*------------------------------------------------------------------------------------------------\ + * Other functions +\------------------------------------------------------------------------------------------------*/ + +function clone(obj) { + let copy; + + //boolean, number, string, null, undefined + if ('object' != typeof obj || null == obj) return obj; + + //date + if (obj instanceof Date) { + copy = new Date(); + copy.setTime(obj.getTime()); + return copy; + } + + //array + if (obj instanceof Array) { + copy = []; + for (var i = 0, len = obj.length; i < len; i++) { + copy[i] = clone(obj[i]); + } + return copy; + } + + //object + if (obj instanceof Object) { + copy = {}; + for (var attr in obj) { + if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]); + } + return copy; + } + + throw new Error('Unable to copy [obj]! Its type is not supported.'); +} + +function loadLibrary(version) { + console.log('Selected version: ' + version); + const baseURL = 'https://cdn.jsdelivr.net/gh/RhoInc/safety-explorer-suite'; + const versionURL = version !== 'master' ? baseURL + '@' + version : baseURL; + + //Load branches. + const pkgRequest = new XMLHttpRequest(); + pkgRequest.onload = function() { + const pkg = JSON.parse(this.responseText); + + const main = pkg.main.replace(/^\.?\/?/, ''); + + //Load .js file. + const jsURL = versionURL + '/' + main; + const js = document.createElement('script'); + js.addEventListener( + 'load', + function() { + console.log('Successfully loaded ' + jsURL + '.'); + initSafetyExplorerSuite(settings, dataArray); + }, + false + ); + js.addEventListener( + 'error', + function() { + console.log('Failed to load ' + jsURL + '.'); + }, + false + ); + js.src = jsURL; + document.head.appendChild(js); + + //Load .css file. + const cssURL = jsURL.replace('build', 'css').replace(/\.js$/, '.css'); + const css = document.createElement('link'); + css.addEventListener( + 'load', + function() { + console.log('Successfully loaded ' + cssURL + '.'); + }, + false + ); + css.addEventListener( + 'error', + function() { + console.log('Failed to load ' + cssURL + '.'); + }, + false + ); + css.type = 'text/css'; + css.rel = 'stylesheet'; + css.href = cssURL; + document.head.appendChild(css); + }; + pkgRequest.open('get', versionURL + '/package.json'); + pkgRequest.send(); +} From 356e8d8dcba20c0a64e71b7cd7d9ff3eb656fe0a Mon Sep 17 00:00:00 2001 From: jwildfire Date: Mon, 19 Aug 2019 15:09:15 -0700 Subject: [PATCH 11/12] minor title tweak. --- safetyOutlierExplorer.js | 3 ++- .../onResize/addEventListeners/functions/addOverlapTitle.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/safetyOutlierExplorer.js b/safetyOutlierExplorer.js index cd9641d..0d17de1 100644 --- a/safetyOutlierExplorer.js +++ b/safetyOutlierExplorer.js @@ -2223,7 +2223,8 @@ var currentTitle = titleEl.text(); var hasOverlapNote = currentTitle.search('overlapping'); //minor hack ... if (hasOverlapNote == -1) { - var newTitle = currentTitle + '\nNumber of overlapping point(s) ' + overlap.length; + var newTitle = + currentTitle + '\nNumber of overlapping point(s) = ' + overlap.length; titleEl.text(newTitle); } } diff --git a/src/callbacks/onResize/addEventListeners/functions/addOverlapTitle.js b/src/callbacks/onResize/addEventListeners/functions/addOverlapTitle.js index 9efb7ef..ae14cdf 100644 --- a/src/callbacks/onResize/addEventListeners/functions/addOverlapTitle.js +++ b/src/callbacks/onResize/addEventListeners/functions/addOverlapTitle.js @@ -11,7 +11,7 @@ export default function addOverlapTitle(d, chart) { var currentTitle = titleEl.text(); var hasOverlapNote = currentTitle.search('overlapping'); //minor hack ... if (hasOverlapNote == -1) { - var newTitle = currentTitle + '\nNumber of overlapping point(s) ' + overlap.length; + var newTitle = currentTitle + '\nNumber of overlapping point(s) = ' + overlap.length; titleEl.text(newTitle); } } From 07bc523dc2987e48b53ddf9513820123d0fe2986 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 20 Aug 2019 10:10:34 -0400 Subject: [PATCH 12/12] remove index.html and index.js files --- package-lock.json | 10 +- .../addEventListeners/functions/index.html | 41 --- .../addEventListeners/functions/index.js | 342 ------------------ 3 files changed, 8 insertions(+), 385 deletions(-) delete mode 100644 src/callbacks/onResize/addEventListeners/functions/index.html delete mode 100644 src/callbacks/onResize/addEventListeners/functions/index.js diff --git a/package-lock.json b/package-lock.json index d270964..8937794 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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/src/callbacks/onResize/addEventListeners/functions/index.html b/src/callbacks/onResize/addEventListeners/functions/index.html deleted file mode 100644 index 6f3df12..0000000 --- a/src/callbacks/onResize/addEventListeners/functions/index.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - Safety Explorer Suite: Test Page - - - - - - - - - - - - - - - - - - - - - - - -
Safety Explorer Suite
-
- Test Page - -
-
- - - - diff --git a/src/callbacks/onResize/addEventListeners/functions/index.js b/src/callbacks/onResize/addEventListeners/functions/index.js deleted file mode 100644 index c7f9a39..0000000 --- a/src/callbacks/onResize/addEventListeners/functions/index.js +++ /dev/null @@ -1,342 +0,0 @@ -//Generate safety explorer given an array of data files. -function initSafetyExplorerSuite(settings, dataArray) { - d3.selectAll('#container *').remove(); - safetyExplorerSuite - .createExplorer( - '#container', // element - settings // settings - ) - .init( - clone(dataArray), // array of data files - true, // load .csv files? - true // SDTM-structured data files? - ); -} - -//Array of SDTM data files. -const dataArray = [ - { - type: 'DM', // demographics - path: - 'https://raw.githubusercontent.com/RhoInc/data-library/master/data/clinical-trials/sdtm/cdisc-pilot-01/dm.csv' - }, - { - type: 'AE', // adverse events - path: - 'https://raw.githubusercontent.com/RhoInc/data-library/master/data/clinical-trials/sdtm/cdisc-pilot-01/ae.csv' - }, - { - type: 'BDS', // basic data structure: labs - path: - 'https://raw.githubusercontent.com/RhoInc/data-library/master/data/clinical-trials/sdtm/cdisc-pilot-01/lb.csv' - }, - { - type: 'BDS', // basic data structure: vital signs - path: - 'https://raw.githubusercontent.com/RhoInc/data-library/master/data/clinical-trials/sdtm/cdisc-pilot-01/vs.csv' - } -]; - -//Get safety-explorer-suite version from window hash. -const version = window.location.hash ? window.location.hash.substring(1) : 'master'; -if (version !== 'master') loadLibrary(version); - -//Wait for settings (defined below) to be available. -const waitForSettings = setInterval(function() { - if (settings !== undefined) { - clearInterval(waitForSettings); - initSafetyExplorerSuite(settings, dataArray); - } -}, 1); - -/*------------------------------------------------------------------------------------------------\ - * Settings -\------------------------------------------------------------------------------------------------*/ - -const filters = [ - { value_col: 'SEX', label: 'Sex' }, - { value_col: 'ARM', label: 'Treatment Group' }, - { value_col: 'RACE', label: 'Race' }, - { value_col: 'SITEID', label: 'Site' } -]; -const settings = { - initial_renderer: - window && window.location && window.location.hash - ? window.location.hash.substring(1) - : null, // allow linking to each renderer - custom_settings: [ - { - renderer_name: 'aeexplorer', - variables: { - filters: [ - { - value_col: 'AESER', - label: 'Serious?', - type: 'event', - start: null - }, - { - value_col: 'AESEV', - label: 'Severity', - type: 'event', - start: null - }, - { - value_col: 'AEREL', - label: 'Relationship', - type: 'event', - start: null - }, - { - value_col: 'AEOUT', - label: 'Outcome', - type: 'event', - start: null - } - ].concat(clone(filters)) - }, - defaults: { - placeholderFlag: { - value_col: 'AETERM', - values: [''] - } - } - }, - { - renderer_name: 'ae-timelines', - filters: [ - { - value_col: 'AESER', - label: 'Serious?', - type: 'event', - start: null - }, - { - value_col: 'AESEV', - label: 'Severity', - type: 'event', - start: null - }, - { - value_col: 'AEREL', - label: 'Relationship', - type: 'event', - start: null - }, - { - value_col: 'AEOUT', - label: 'Outcome', - type: 'event', - start: null - } - ].concat(clone(filters)) - }, - { - renderer_name: 'safety-histogram', - filters: clone(filters), - displayNormalRange: true - }, - { - renderer_name: 'safety-outlier-explorer', - filters: clone(filters) - }, - { - renderer_name: 'paneled-outlier-explorer', - filters: clone(filters) - }, - { - renderer_name: 'safety-results-over-time', - groups: clone(filters), - filters: clone(filters) - }, - { - renderer_name: 'safety-shift-plot', - filters: clone(filters) - }, - { - renderer_name: 'hep-explorer', - group_cols: clone(filters), - filters: clone(filters), - measure_values: { - ALT: 'Alanine Aminotransferase', - AST: 'Aspartate Aminotransferase', - TB: 'Bilirubin', - ALP: 'Alkaline Phosphatase' - } - } - ] -}; - -/*------------------------------------------------------------------------------------------------\ - This code creates a dropdown of previous versions and open branches of safety-explorer-suite. -\------------------------------------------------------------------------------------------------*/ - -//Load branches. -const branchRequest = new XMLHttpRequest(); -branchRequest.onload = function() { - const branches = JSON.parse(this.responseText) - .map(function(branch) { - return branch.name; - }) - .sort(function(a, b) { - return a === 'master' - ? -1 - : b === 'master' - ? 1 - : /^dev-/.test(a) && /^dev-/.test(b) - ? a > b - ? -1 - : 1 - : /^dev-/.test(a) - ? -1 - : /^dev-/.test(b) - ? 1 - : a < b - ? -a - : 1; - }); - d3.select('.version-select__select') - .selectAll('option.branch') - .data(branches) - .enter() - .append('option') - .classed('branch', true) - .text(function(d) { - return d; - }) - .property('selected', function(d) { - return d === version; - }); -}; -branchRequest.open('get', 'https://api.github.com/repos/RhoInc/safety-explorer-suite/branches'); -branchRequest.send(); - -//Load releases. -const releaseRequest = new XMLHttpRequest(); -releaseRequest.onload = function() { - const releases = JSON.parse(this.responseText) - .map(function(release) { - return release.name; - }) - .sort(d3.descending); - d3.select('.version-select__select') - .selectAll('option.release') - .data(releases) - .enter() - .append('option') - .classed('release', true) - .text(function(d) { - return d; - }) - .property('selected', function(d) { - return d === version; - }); -}; -releaseRequest.open('get', 'https://api.github.com/repos/RhoInc/safety-explorer-suite/releases'); -releaseRequest.send(); - -//Add version select functionality. -d3.select('.version-select__submit').on('click', function() { - const version = d3 - .select('.version-select__select') - .select('option:checked') - .text(); - loadLibrary(version); -}); - -/*------------------------------------------------------------------------------------------------\ - * Other functions -\------------------------------------------------------------------------------------------------*/ - -function clone(obj) { - let copy; - - //boolean, number, string, null, undefined - if ('object' != typeof obj || null == obj) return obj; - - //date - if (obj instanceof Date) { - copy = new Date(); - copy.setTime(obj.getTime()); - return copy; - } - - //array - if (obj instanceof Array) { - copy = []; - for (var i = 0, len = obj.length; i < len; i++) { - copy[i] = clone(obj[i]); - } - return copy; - } - - //object - if (obj instanceof Object) { - copy = {}; - for (var attr in obj) { - if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]); - } - return copy; - } - - throw new Error('Unable to copy [obj]! Its type is not supported.'); -} - -function loadLibrary(version) { - console.log('Selected version: ' + version); - const baseURL = 'https://cdn.jsdelivr.net/gh/RhoInc/safety-explorer-suite'; - const versionURL = version !== 'master' ? baseURL + '@' + version : baseURL; - - //Load branches. - const pkgRequest = new XMLHttpRequest(); - pkgRequest.onload = function() { - const pkg = JSON.parse(this.responseText); - - const main = pkg.main.replace(/^\.?\/?/, ''); - - //Load .js file. - const jsURL = versionURL + '/' + main; - const js = document.createElement('script'); - js.addEventListener( - 'load', - function() { - console.log('Successfully loaded ' + jsURL + '.'); - initSafetyExplorerSuite(settings, dataArray); - }, - false - ); - js.addEventListener( - 'error', - function() { - console.log('Failed to load ' + jsURL + '.'); - }, - false - ); - js.src = jsURL; - document.head.appendChild(js); - - //Load .css file. - const cssURL = jsURL.replace('build', 'css').replace(/\.js$/, '.css'); - const css = document.createElement('link'); - css.addEventListener( - 'load', - function() { - console.log('Successfully loaded ' + cssURL + '.'); - }, - false - ); - css.addEventListener( - 'error', - function() { - console.log('Failed to load ' + cssURL + '.'); - }, - false - ); - css.type = 'text/css'; - css.rel = 'stylesheet'; - css.href = cssURL; - document.head.appendChild(css); - }; - pkgRequest.open('get', versionURL + '/package.json'); - pkgRequest.send(); -}