From 903643e2fc0e23945cb7f254494dda8135831166 Mon Sep 17 00:00:00 2001 From: Spencer Childress Date: Tue, 23 Mar 2021 15:38:59 -0400 Subject: [PATCH 1/2] fix #500; fix #504 --- R/makeChartConfig.R | 8 +- R/mod_chartsTab.R | 3 +- R/mod_filterTab.R | 4 +- .../lib/ae-timelines-2.1.6/aeTimelines.js | 1158 ---- .../lib/aeexplorer-3.3.5/aeTable.css | 369 -- .../lib/aeexplorer-3.3.5/aeTable.js | 2329 ------- .../lib/hep-explorer-1.3.1/hepexplorer.js | 5691 ----------------- .../paneledOutlierExplorer.js | 2101 ------ .../safetyDeltaDelta.js | 1851 ------ .../safety-histogram-2.4.1/safetyHistogram.js | 2205 ------- .../safetyOutlierExplorer.js | 2478 ------- .../safetyResultsOverTime.js | 1866 ------ .../safetyShiftPlot.js | 1399 ---- 13 files changed, 10 insertions(+), 21452 deletions(-) delete mode 100644 inst/htmlwidgets/lib/ae-timelines-2.1.6/aeTimelines.js delete mode 100644 inst/htmlwidgets/lib/aeexplorer-3.3.5/aeTable.css delete mode 100644 inst/htmlwidgets/lib/aeexplorer-3.3.5/aeTable.js delete mode 100644 inst/htmlwidgets/lib/hep-explorer-1.3.1/hepexplorer.js delete mode 100644 inst/htmlwidgets/lib/paneled-outlier-explorer-1.1.4/paneledOutlierExplorer.js delete mode 100644 inst/htmlwidgets/lib/safety-delta-delta-1.0.0/safetyDeltaDelta.js delete mode 100644 inst/htmlwidgets/lib/safety-histogram-2.4.1/safetyHistogram.js delete mode 100644 inst/htmlwidgets/lib/safety-outlier-explorer-2.6.0/safetyOutlierExplorer.js delete mode 100644 inst/htmlwidgets/lib/safety-results-over-time-2.3.3/safetyResultsOverTime.js delete mode 100644 inst/htmlwidgets/lib/safety-shift-plot-2.1.3/safetyShiftPlot.js diff --git a/R/makeChartConfig.R b/R/makeChartConfig.R index 19a380e7..aae70c18 100644 --- a/R/makeChartConfig.R +++ b/R/makeChartConfig.R @@ -56,10 +56,16 @@ makeChartConfig <- function(dirs, sourceFiles=TRUE){ chart <- read_yaml(path) chart$path <- path chart$name <- path %>% file_path_sans_ext %>% basename + chart$order <- ifelse( + is.null(chart$order), + length(yaml_files) + 1, + chart$order + ) %>% as.numeric + return(chart) }) - + charts <- charts[order(purrr::map_dbl(charts, function(chart) chart$order))] names(charts) <- yaml_files %>% file_path_sans_ext %>% basename diff --git a/R/mod_chartsTab.R b/R/mod_chartsTab.R index 873ba17d..10c33ba0 100644 --- a/R/mod_chartsTab.R +++ b/R/mod_chartsTab.R @@ -57,7 +57,6 @@ chartsTab <- function(input, output, session, chart, data, mapping){ message(chart$name, " has an init.") print(chart$functions[chart$workflow$init]) params <- do.call(chart$functions[[chart$workflow$init]], params) - print(params) } } return(params) @@ -94,4 +93,4 @@ chartsTab <- function(input, output, session, chart, data, mapping){ type=chart$type ) } -} \ No newline at end of file +} diff --git a/R/mod_filterTab.R b/R/mod_filterTab.R index 83040daf..f169abb3 100644 --- a/R/mod_filterTab.R +++ b/R/mod_filterTab.R @@ -26,7 +26,7 @@ filterTabUI <- function(id, filterDomain = "dm"){ id = ns("pbar"), value = 100, total = 100, display_pct = TRUE ), - shiny::dataTableOutput(outputId = ns("table")), + DT::dataTableOutput(outputId = ns("table")), tags$p("Code dplyr:"), verbatimTextOutput(outputId = ns("code_dplyr")), tags$p("Expression:"), @@ -80,7 +80,7 @@ filterTab <- function(input, output, session, domainData, filterDomain, id_col){ ) }) - output$table <- shiny::renderDataTable({ + output$table <- DT::renderDataTable({ res_filter$data_filtered() }, options = list(pageLength = 5)) diff --git a/inst/htmlwidgets/lib/ae-timelines-2.1.6/aeTimelines.js b/inst/htmlwidgets/lib/ae-timelines-2.1.6/aeTimelines.js deleted file mode 100644 index cfc093f2..00000000 --- a/inst/htmlwidgets/lib/ae-timelines-2.1.6/aeTimelines.js +++ /dev/null @@ -1,1158 +0,0 @@ -(function(global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' - ? (module.exports = factory(require('d3'), require('webcharts'))) - : typeof define === 'function' && define.amd - ? define(['d3', 'webcharts'], factory) - : (global.aeTimelines = factory(global.d3, global.webCharts)); -})(this, function(d3, webcharts) { - 'use strict'; - - if (typeof Object.assign != 'function') { - Object.defineProperty(Object, 'assign', { - value: function assign(target, varArgs) { - if (target == null) { - // TypeError if undefined or null - throw new TypeError('Cannot convert undefined or null to object'); - } - - var to = Object(target); - - for (var index = 1; index < arguments.length; index++) { - var nextSource = arguments[index]; - - if (nextSource != null) { - // Skip over if undefined or null - for (var nextKey in nextSource) { - // Avoid bugs when hasOwnProperty is shadowed - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - - return to; - }, - writable: true, - configurable: true - }); - } - - if (!Array.prototype.find) { - Object.defineProperty(Array.prototype, 'find', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, 'length')). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return kValue. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return kValue; - } - // e. Increase k by 1. - k++; - } - - // 7. Return undefined. - return undefined; - } - }); - } - - if (!Array.prototype.findIndex) { - Object.defineProperty(Array.prototype, 'findIndex', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, "length")). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return k. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return k; - } - // e. Increase k by 1. - k++; - } - - // 7. Return -1. - return -1; - } - }); - } - - var _typeof = - typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' - ? function(obj) { - return typeof obj; - } - : function(obj) { - return obj && - typeof Symbol === 'function' && - obj.constructor === Symbol && - obj !== Symbol.prototype - ? 'symbol' - : typeof obj; - }; - - /*------------------------------------------------------------------------------------------------\ - Clone a variable (http://stackoverflow.com/a/728694). - \------------------------------------------------------------------------------------------------*/ - - function clone(obj) { - var copy; - - //Handle the 3 simple types, and null or undefined - if (null == obj || 'object' != (typeof obj === 'undefined' ? 'undefined' : _typeof(obj))) - return obj; - - //Handle Date - if (obj instanceof Date) { - copy = new Date(); - copy.setTime(obj.getTime()); - return copy; - } - - //Handle Array - if (obj instanceof Array) { - copy = []; - for (var i = 0, len = obj.length; i < len; i++) { - copy[i] = clone(obj[i]); - } - return copy; - } - - //Handle 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 isn't supported."); - } - - var isMergeableObject = function isMergeableObject(value) { - return isNonNullObject(value) && !isSpecial(value); - }; - - function isNonNullObject(value) { - return ( - !!value && (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' - ); - } - - function isSpecial(value) { - var stringValue = Object.prototype.toString.call(value); - - return ( - stringValue === '[object RegExp]' || - stringValue === '[object Date]' || - isReactElement(value) - ); - } - - // see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25 - var canUseSymbol = typeof Symbol === 'function' && Symbol.for; - var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7; - - function isReactElement(value) { - return value.$$typeof === REACT_ELEMENT_TYPE; - } - - function emptyTarget(val) { - return Array.isArray(val) ? [] : {}; - } - - function cloneUnlessOtherwiseSpecified(value, options) { - return options.clone !== false && options.isMergeableObject(value) - ? deepmerge(emptyTarget(value), value, options) - : value; - } - - function defaultArrayMerge(target, source, options) { - return target.concat(source).map(function(element) { - return cloneUnlessOtherwiseSpecified(element, options); - }); - } - - function mergeObject(target, source, options) { - var destination = {}; - - if (options.isMergeableObject(target)) { - Object.keys(target).forEach(function(key) { - destination[key] = cloneUnlessOtherwiseSpecified(target[key], options); - }); - } - - Object.keys(source).forEach(function(key) { - if (!options.isMergeableObject(source[key]) || !target[key]) { - destination[key] = cloneUnlessOtherwiseSpecified(source[key], options); - } else { - destination[key] = deepmerge(target[key], source[key], options); - } - }); - - return destination; - } - - function deepmerge(target, source, options) { - options = options || {}; - - options.arrayMerge = options.arrayMerge || defaultArrayMerge; - - options.isMergeableObject = options.isMergeableObject || isMergeableObject; - - var sourceIsArray = Array.isArray(source); - - var targetIsArray = Array.isArray(target); - - var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray; - - if (!sourceAndTargetTypesMatch) { - return cloneUnlessOtherwiseSpecified(source, options); - } else if (sourceIsArray) { - return options.arrayMerge(target, source, options); - } else { - return mergeObject(target, source, options); - } - } - - deepmerge.all = function deepmergeAll(array, options) { - if (!Array.isArray(array)) { - throw new Error('first argument should be an array'); - } - - return array.reduce(function(prev, next) { - return deepmerge(prev, next, options); - }, {}); - }; - - var deepmerge_1 = deepmerge; - - var rendererSpecificSettings = { - id_col: 'USUBJID', - seq_col: 'AESEQ', - stdy_col: 'ASTDY', - endy_col: 'AENDY', - term_col: 'AETERM', - - color: { - value_col: 'AESEV', - label: 'Severity/Intensity', - values: ['MILD', 'MODERATE', 'SEVERE'], - colors: [ - '#66bd63', // mild - '#fdae61', // moderate - '#d73027', // severe - '#377eb8', - '#984ea3', - '#ff7f00', - '#a65628', - '#f781bf' - ] - }, - - highlight: { - value_col: 'AESER', - label: 'Serious Event', - value: 'Y', - detail_col: null, - attributes: { - stroke: 'black', - 'stroke-width': '2', - fill: 'none' - } - }, - - filters: null, - details: null, - custom_marks: null - }; - - var webchartsSettings = { - x: { - column: 'wc_value', - type: 'linear', - label: null - }, - y: { - column: null, // set in syncSettings() - type: 'ordinal', - label: '', - sort: 'earliest', - behavior: 'flex' - }, - marks: [ - { - type: 'line', - per: null, // set in syncSettings() - tooltip: null, // set in syncSettings() - attributes: { - 'stroke-width': 5, - 'stroke-opacity': 0.5 - } - }, - { - type: 'circle', - per: null, // set in syncSettings() - tooltip: null, // set in syncSettings() - attributes: { - 'fill-opacity': 0.5, - 'stroke-opacity': 0.5 - } - } - ], - legend: { location: 'top', mark: 'circle' }, - gridlines: 'y', - range_band: 15, - margin: { top: 50 }, // for second x-axis - resizable: true - }; - - var defaultSettings = Object.assign({}, rendererSpecificSettings, webchartsSettings); - - function syncSettings(preSettings) { - var nextSettings = clone(preSettings); - - nextSettings.y.column = nextSettings.id_col; - - //Lines (AE duration) - nextSettings.marks[0].per = [nextSettings.id_col, nextSettings.seq_col]; - nextSettings.marks[0].tooltip = - 'Reported Term: [' + - nextSettings.term_col + - ']' + - ('\nStart Day: [' + nextSettings.stdy_col + ']') + - ('\nStop Day: [' + nextSettings.endy_col + ']'); - - //Circles (AE start day) - nextSettings.marks[1].per = [nextSettings.id_col, nextSettings.seq_col, 'wc_value']; - nextSettings.marks[1].tooltip = - 'Reported Term: [' + - nextSettings.term_col + - ']' + - ('\nStart Day: [' + nextSettings.stdy_col + ']') + - ('\nStop Day: [' + nextSettings.endy_col + ']'); - nextSettings.marks[1].values = { wc_category: [nextSettings.stdy_col] }; - - //Define highlight marks. - if (nextSettings.highlight) { - //Lines (highlighted event duration) - var highlightLine = { - type: 'line', - per: [nextSettings.id_col, nextSettings.seq_col], - tooltip: - 'Reported Term: [' + - nextSettings.term_col + - ']' + - ('\nStart Day: [' + nextSettings.stdy_col + ']') + - ('\nStop Day: [' + nextSettings.endy_col + ']') + - ('\n' + - nextSettings.highlight.label + - ': [' + - (nextSettings.highlight.detail_col - ? nextSettings.highlight.detail_col - : nextSettings.highlight.value_col) + - ']'), - values: {}, - attributes: nextSettings.highlight.attributes || {} - }; - highlightLine.values[nextSettings.highlight.value_col] = nextSettings.highlight.value; - highlightLine.attributes.class = 'highlight'; - nextSettings.marks.push(highlightLine); - - //Circles (highlighted event start day) - var highlightCircle = { - type: 'circle', - per: [nextSettings.id_col, nextSettings.seq_col, 'wc_value'], - tooltip: - 'Reported Term: [' + - nextSettings.term_col + - ']' + - ('\nStart Day: [' + nextSettings.stdy_col + ']') + - ('\nStop Day: [' + nextSettings.endy_col + ']') + - ('\n' + - nextSettings.highlight.label + - ': [' + - (nextSettings.highlight.detail_col - ? nextSettings.highlight.detail_col - : nextSettings.highlight.value_col) + - ']'), - values: { wc_category: nextSettings.stdy_col }, - attributes: nextSettings.highlight.attributes || {} - }; - highlightCircle.values[nextSettings.highlight.value_col] = nextSettings.highlight.value; - highlightCircle.attributes.class = 'highlight'; - nextSettings.marks.push(highlightCircle); - } - - //Define mark coloring and legend. - nextSettings.color_by = nextSettings.color.value_col; - nextSettings.colors = nextSettings.color.colors; - nextSettings.legend = nextSettings.legend || { location: 'top' }; - nextSettings.legend.label = nextSettings.color.label; - nextSettings.legend.order = nextSettings.color.values; - nextSettings.color_dom = nextSettings.color.values; - - //Default filters - if (!nextSettings.filters || nextSettings.filters.length === 0) { - nextSettings.filters = [ - { value_col: nextSettings.color.value_col, label: nextSettings.color.label }, - { value_col: nextSettings.id_col, label: 'Participant Identifier' } - ]; - if (nextSettings.highlight) - nextSettings.filters.unshift({ - value_col: nextSettings.highlight.value_col, - label: nextSettings.highlight.label - }); - } - - //Default detail listing columns - var defaultDetails = [ - { value_col: nextSettings.seq_col, label: 'Sequence Number' }, - { value_col: nextSettings.stdy_col, label: 'Start Day' }, - { value_col: nextSettings.endy_col, label: 'Stop Day' }, - { value_col: nextSettings.term_col, label: 'Reported Term' } - ]; - - //Add settings.color.value_col to default details. - defaultDetails.push({ - value_col: nextSettings.color.value_col, - label: nextSettings.color.label - }); - - //Add settings.highlight.value_col and settings.highlight.detail_col to default details. - if (nextSettings.highlight) { - defaultDetails.push({ - value_col: nextSettings.highlight.value_col, - label: nextSettings.highlight.label - }); - - if (nextSettings.highlight.detail_col) - defaultDetails.push({ - value_col: nextSettings.highlight.detail_col, - label: nextSettings.highlight.label + ' Details' - }); - } - - //Add settings.filters columns to default details. - nextSettings.filters.forEach(function(filter) { - if (filter !== nextSettings.id_col && filter.value_col !== nextSettings.id_col) - defaultDetails.push({ - value_col: filter.value_col, - label: filter.label - }); - }); - - //Redefine settings.details with defaults. - if (!nextSettings.details) nextSettings.details = defaultDetails; - else { - //Allow user to specify an array of columns or an array of objects with a column property - //and optionally a column label. - nextSettings.details = nextSettings.details.map(function(d) { - return { - value_col: d.value_col ? d.value_col : d, - label: d.label ? d.label : d.value_col ? d.value_col : d - }; - }); - - //Add default details to settings.details. - defaultDetails.reverse().forEach(function(defaultDetail) { - return nextSettings.details.unshift(defaultDetail); - }); - } - - //Add custom marks to marks array. - if (nextSettings.custom_marks) - nextSettings.custom_marks.forEach(function(custom_mark) { - custom_mark.attributes = custom_mark.attributes || {}; - custom_mark.attributes.class = 'custom'; - nextSettings.marks.push(custom_mark); - }); - - return nextSettings; - } - - var controlInputs = [ - { - type: 'dropdown', - option: 'y.sort', - label: 'Sort Participant IDs', - values: ['earliest', 'alphabetical-descending'], - require: true - } - ]; - - function syncControlInputs(preControlInputs, preSettings) { - preSettings.filters.forEach(function(d, i) { - var 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 = preControlInputs - .filter(function(f) { - return f.type == 'subsetter'; - }) - .map(function(m) { - return m.value_col; - }); - if (current_value_cols.indexOf(thisFilter.value_col) == -1) - preControlInputs.unshift(thisFilter); - }); - - return preControlInputs; - } - - function syncSecondSettings(preSettings) { - var nextSettings = clone(preSettings); - - nextSettings.y.column = nextSettings.seq_col; - nextSettings.y.sort = 'alphabetical-descending'; - - nextSettings.marks[0].per = [nextSettings.seq_col]; - nextSettings.marks[1].per = [nextSettings.seq_col, 'wc_value']; - - if (nextSettings.highlight) { - nextSettings.marks[2].per = [nextSettings.seq_col]; - nextSettings.marks[3].per = [nextSettings.seq_col, 'wc_value']; - } - - nextSettings.range_band = preSettings.range_band * 2; - nextSettings.margin = null; - nextSettings.transitions = false; - - return nextSettings; - } - - function calculatePopulationSize() { - var _this = this; - - this.populationCount = d3 - .set( - this.raw_data.map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values().length; - } - - function cleanData() { - var _this = this; - - this.superRaw = this.raw_data; - var N = this.superRaw.length; - - //Remove records with empty verbatim terms. - this.superRaw = this.superRaw.filter(function(d) { - return /[^\s*$]/.test(d[_this.config.term_col]); - }); - var n1 = this.superRaw.length; - var diff1 = N - n1; - if (diff1) - console.warn(diff1 + ' records without [ ' + this.config.term_col + ' ] removed.'); - - //Remove records with non-integer start days. - this.superRaw = this.superRaw.filter(function(d) { - return /^-?\d+$/.test(d[_this.config.stdy_col]); - }); - var n2 = this.superRaw.length; - var diff2 = n1 - n2; - if (diff2) - console.warn(diff2 + ' records without [ ' + this.config.stdy_col + ' ] removed.'); - } - - function checkFilters() { - var _this = this; - - this.controls.config.inputs = this.controls.config.inputs.filter(function(input) { - if (input.type !== 'subsetter') return true; - else { - var levels = d3 - .set( - _this.superRaw.map(function(d) { - return d[input.value_col]; - }) - ) - .values(); - if (levels.length < 2) { - console.warn( - 'The [ ' + - input.value_col + - ' ] filter was removed because the variable has only one level.' - ); - return false; - } - - return true; - } - }); - } - - function checkColorBy() { - var _this = this; - - this.superRaw.forEach(function(d) { - return (d[_this.config.color_by] = /[^\s*$]/.test(d[_this.config.color_by]) - ? d[_this.config.color_by] - : 'N/A'); - }); - - //Flag NAs - if ( - this.superRaw.some(function(d) { - return d[_this.config.color_by] === 'N/A'; - }) - ) - this.na = true; - } - - function defineColorDomain() { - var _this = this; - - var color_by_values = d3 - .set( - this.superRaw.map(function(d) { - return d[_this.config.color_by]; - }) - ) - .values() - .sort(function(a, b) { - var aIndex = _this.config.color.values.indexOf(a); - var bIndex = _this.config.color.values.indexOf(b); - var diff = aIndex > -1 && bIndex > -1 ? aIndex - bIndex : 0; - - return diff - ? diff - : aIndex > -1 - ? -1 - : bIndex > -1 - ? 1 - : a === 'N/A' - ? 1 - : b === 'N/A' ? -1 : a.toLowerCase() < b.toLowerCase() ? -1 : 1; - }); - color_by_values.forEach(function(color_by_value, i) { - if (_this.config.color.values.indexOf(color_by_value) < 0) { - _this.config.color_dom.push(color_by_value); - _this.config.legend.order.push(color_by_value); - _this.chart2.config.color_dom.push(color_by_value); - _this.chart2.config.legend.order.push(color_by_value); - } - }); - } - - /*------------------------------------------------------------------------------------------------\ - Expand a data array to one item per original item per specified column. - \------------------------------------------------------------------------------------------------*/ - - function lengthenRaw() { - var data = this.superRaw; - var columns = [this.config.stdy_col, this.config.endy_col]; - var my_data = []; - - data.forEach(function(d) { - columns.forEach(function(column) { - var obj = Object.assign({}, d); - obj.wc_category = column; - obj.wc_value = parseFloat(d[column]); - my_data.push(obj); - }); - }); - - this.raw_data = my_data; - } - - function initCustomEvents() { - var chart = this; - chart.participantsSelected = []; - chart.events.participantsSelected = new CustomEvent('participantsSelected'); - } - - function onInit() { - calculatePopulationSize.call(this); - cleanData.call(this); - checkFilters.call(this); - checkColorBy.call(this); - defineColorDomain.call(this); - lengthenRaw.call(this); - initCustomEvents.call(this); - } - - function sortLegendFilter() { - var _this = this; - - this.controls.wrap - .selectAll('.control-group') - .filter(function(d) { - return d.value_col === _this.config.color.value_col; - }) - .selectAll('option') - .sort(function(a, b) { - return _this.config.legend.order.indexOf(a) - _this.config.legend.order.indexOf(b); - }); - } - - function addParticipantCountContainer() { - this.wrap - .select('.legend') - .append('span') - .classed('annote', true) - .style('float', 'right') - .style('font-style', 'italic'); - } - - function addTopXaxis() { - this.svg - .append('g') - .attr('class', 'x2 axis linear') - .append('text') - .attr({ - class: 'axis-title top', - dy: '2em', - 'text-anchor': 'middle' - }) - .text(this.config.x_label); - } - - function addBackButton() { - var _this = this; - - this.chart2.wrap - .insert('div', ':first-child') - .attr('id', 'backButton') - .insert('button', '.legend') - .html('← Back') - .style('cursor', 'pointer') - .on('click', function() { - //Trigger participantsSelected event - _this.participantsSelected = []; - _this.events.participantsSelected.data = _this.participantsSelected; - _this.wrap.node().dispatchEvent(_this.events.participantsSelected); - - //remove the details chart - _this.chart2.wrap.select('.id-title').remove(); - _this.chart2.wrap.style('display', 'none'); - _this.table.wrap.style('display', 'none'); - _this.controls.wrap.style('display', 'block'); - _this.wrap.style('display', 'block'); - _this.draw(); - }); - } - - function onLayout() { - sortLegendFilter.call(this); - addParticipantCountContainer.call(this); - addTopXaxis.call(this); - addBackButton.call(this); - } - - function onPreprocess() {} - - function onDatatransform() {} - - function addNAToColorScale() { - if (this.na) - // defined in ../onInit/checkColorBy - this.colorScale.range().splice(this.colorScale.domain().indexOf('N/A'), 1, '#999999'); - } - - /*------------------------------------------------------------------------------------------------\ - Annotate number of participants based on current filters, number of participants in all, and - the corresponding percentage. - - Inputs: - - chart - a webcharts chart object - id_unit - a text string to label the units in the annotation (default = 'participants') - selector - css selector for the annotation - \------------------------------------------------------------------------------------------------*/ - - function updateParticipantCount(chart, selector, id_unit) { - //count the number of unique ids in the current chart and calculate the percentage - var filtered_data = chart.raw_data.filter(function(d) { - var filtered = d[chart.config.seq_col] === ''; - chart.filters.forEach(function(di) { - if (filtered === false && di.val !== 'All') - filtered = - Object.prototype.toString.call(di.val) === '[object Array]' - ? di.val.indexOf(d[di.col]) === -1 - : di.val !== d[di.col]; - }); - return !filtered; - }); - var currentObs = d3 - .set( - filtered_data.map(function(d) { - return d[chart.config.id_col]; - }) - ) - .values().length; - - var percentage = d3.format('0.1%')(currentObs / chart.populationCount); - - //clear the annotation - var annotation = d3.select(selector); - annotation.selectAll('*').remove(); - - //update the annotation - var units = id_unit ? ' ' + id_unit : ' participant(s)'; - annotation.text( - currentObs + ' of ' + chart.populationCount + units + ' shown (' + percentage + ')' - ); - } - - function sortYdomain() { - var _this = this; - - var yAxisSort = this.controls.wrap - .selectAll('.control-group') - .filter(function(d) { - return d.option && d.option === 'y.sort'; - }) - .select('option:checked') - .text(); - - if (yAxisSort === 'earliest') { - //Redefine filtered data as it defaults to the final mark drawn, which might be filtered in - //addition to the current filter selections. - var filtered_data = this.raw_data.filter(function(d) { - var filtered = d[_this.config.seq_col] === ''; - _this.filters.forEach(function(di) { - if (filtered === false && di.val !== 'All') - filtered = - Object.prototype.toString.call(di.val) === '[object Array]' - ? di.val.indexOf(d[di.col]) === -1 - : di.val !== d[di.col]; - }); - return !filtered; - }); - - //Capture all participant IDs with adverse events with a start day. - var withStartDay = d3 - .nest() - .key(function(d) { - return d[_this.config.id_col]; - }) - .rollup(function(d) { - return d3.min(d, function(di) { - return +di[_this.config.stdy_col]; - }); - }) - .entries( - filtered_data.filter(function(d) { - return ( - !isNaN(parseFloat(d[_this.config.stdy_col])) && - isFinite(d[_this.config.stdy_col]) - ); - }) - ) - .sort(function(a, b) { - return a.values > b.values - ? -2 - : a.values < b.values ? 2 : a.key > b.key ? -1 : 1; - }) - .map(function(d) { - return d.key; - }); - - //Capture all participant IDs with adverse events without a start day. - var withoutStartDay = d3 - .set( - filtered_data - .filter(function(d) { - return ( - +d[_this.config.seq_col] > 0 && - (isNaN(parseFloat(d[_this.config.stdy_col])) || - !isFinite(d[_this.config.stdy_col])) && - withStartDay.indexOf(d[_this.config.id_col]) === -1 - ); - }) - .map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values(); - this.y_dom = withStartDay.concat(withoutStartDay); - } else this.y_dom = this.y_dom.sort(d3.descending); - } - - function onDraw() { - addNAToColorScale.call(this); - updateParticipantCount(this, '.annote', 'participant ID(s)'); - sortYdomain.call(this); - } - - /*------------------------------------------------------------------------------------------------\ - Add highlighted adverse event legend item. - \------------------------------------------------------------------------------------------------*/ - - function addHighlightLegendItem(chart) { - chart.wrap.select('.legend li.highlight').remove(); - var highlightLegendItem = chart.wrap - .select('.legend') - .append('li') - .attr('class', 'highlight') - .style({ - 'list-style-type': 'none', - 'margin-right': '1em', - display: 'inline-block' - }); - var highlightLegendColorBlock = highlightLegendItem - .append('svg') - .attr({ - width: '1.75em', - height: '1.5em' - }) - .style({ - position: 'relative', - top: '0.35em' - }); - highlightLegendColorBlock - .append('circle') - .attr({ - cx: 10, - cy: 10, - r: 4 - }) - .style(chart.config.highlight.attributes); - highlightLegendColorBlock - .append('line') - .attr({ - x1: 2 * 3.14 * 4 - 10, - y1: 10, - x2: 2 * 3.14 * 4 - 5, - y2: 10 - }) - .style(chart.config.highlight.attributes) - .style('shape-rendering', 'crispEdges'); - highlightLegendItem - .append('text') - .style('margin-left', '.35em') - .text(chart.config.highlight.label); - } - - function drawTopXaxis() { - var x2Axis = d3.svg - .axis() - .scale(this.x) - .orient('top') - .tickFormat(this.xAxis.tickFormat()) - .innerTickSize(this.xAxis.innerTickSize()) - .outerTickSize(this.xAxis.outerTickSize()) - .ticks(this.xAxis.ticks()[0]); - var g_x2_axis = this.svg.select('g.x2.axis').attr('class', 'x2 axis linear'); - g_x2_axis.call(x2Axis); - g_x2_axis - .select('text.axis-title.top') - .attr( - 'transform', - 'translate(' + this.raw_width / 2 + ',-' + this.config.margin.top + ')' - ); - g_x2_axis.select('.domain').attr({ - fill: 'none', - stroke: '#ccc', - 'shape-rendering': 'crispEdges' - }); - g_x2_axis.selectAll('.tick line').attr('stroke', '#eee'); - } - - function addTickClick() { - var _this = this; - - var context = this; - this.svg - .select('.y.axis') - .selectAll('.tick') - .style('cursor', 'pointer') - .on('click', function(d) { - var csv2 = _this.raw_data.filter(function(di) { - return di[_this.config.id_col] === d; - }); - _this.chart2.wrap.style('display', 'block'); - _this.chart2.draw(csv2); - _this.chart2.wrap - .select('#backButton') - .append('strong') - .attr('class', 'id-title') - .style('margin-left', '1%') - .text('Participant: ' + d); - - //Trigger participantsSelected event - context.participantsSelected = [d]; - context.events.participantsSelected.data = context.participantsSelected; - context.wrap.node().dispatchEvent(context.events.participantsSelected); - - //Sort listing by sequence. - var seq_col = context.config.seq_col; - var tableData = _this.superRaw - .filter(function(di) { - return di[_this.config.id_col] === d; - }) - .sort(function(a, b) { - return +a[seq_col] < b[seq_col] ? -1 : 1; - }); - - //Define listing columns. - _this.table.config.cols = d3 - .set( - _this.config.details.map(function(detail) { - return detail.value_col; - }) - ) - .values(); - _this.table.config.headers = d3 - .set( - _this.config.details.map(function(detail) { - return detail.label; - }) - ) - .values(); - _this.table.wrap.style('display', 'block'); - _this.table.draw(tableData); - _this.table.wrap.selectAll('th,td').style({ - 'text-align': 'left', - 'padding-right': '10px' - }); - - //Hide timelines. - _this.wrap.style('display', 'none'); - _this.controls.wrap.style('display', 'none'); - }); - } - - function onResize() { - var context = this; - - //Add highlight adverse event legend item. - if (this.config.highlight) addHighlightLegendItem(this); - - //Draw second x-axis at top of chart. - drawTopXaxis.call(this); - - //Draw second chart when y-axis tick label is clicked. - addTickClick.call(this); - - /**-------------------------------------------------------------------------------------------\ - Second chart callbacks. - \-------------------------------------------------------------------------------------------**/ - - this.chart2.on('preprocess', function() { - //Define color scale. - this.config.color_dom = context.colorScale.domain(); - }); - - this.chart2.on('draw', function() { - //Sync x-axis domain of second chart with that of the original chart. - this.x_dom = context.x_dom; - }); - - this.chart2.on('resize', function() { - //Add highlight adverse event legend item. - if (this.config.highlight) addHighlightLegendItem(this); - }); - } - - // utilities - - function aeTimelines() { - var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'body'; - var settings = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - - //Merge default settings with custom settings. - var mergedSettings = deepmerge_1(defaultSettings, settings, { - arrayMerge: function arrayMerge(destination, source) { - return source; - } - }); - - //Sync properties within settings object. - var syncedSettings = syncSettings(mergedSettings); - - //Sync control inputs with settings object. - var syncedControlInputs = syncControlInputs(controlInputs, syncedSettings); - - //Sync properties within secondary settings object. - var syncedSecondSettings = syncSecondSettings(syncedSettings); - - //Create controls. - var controls = webcharts.createControls(element, { - location: 'top', - inputs: syncedControlInputs - }); - - //Create chart. - var chart = webcharts.createChart(element, syncedSettings, controls); - chart.on('init', onInit); - chart.on('layout', onLayout); - chart.on('preprocess', onPreprocess); - chart.on('datatransform', onDatatransform); - chart.on('draw', onDraw); - chart.on('resize', onResize); - - //Create participant-level chart. - var chart2 = webcharts.createChart(element, syncedSecondSettings).init([]); - chart2.wrap.style('display', 'none'); - chart.chart2 = chart2; - - //Create participant-level listing. - var table = webcharts.createTable(element, {}).init([]); - table.wrap.style('display', 'none'); - table.table.style('display', 'table'); - table.table.attr('width', '100%'); - chart.table = table; - - return chart; - } - - return aeTimelines; -}); diff --git a/inst/htmlwidgets/lib/aeexplorer-3.3.5/aeTable.css b/inst/htmlwidgets/lib/aeexplorer-3.3.5/aeTable.css deleted file mode 100644 index 56916b7a..00000000 --- a/inst/htmlwidgets/lib/aeexplorer-3.3.5/aeTable.css +++ /dev/null @@ -1,369 +0,0 @@ -/*------------------------------------------------------------------------------------------------\ - AE Table style sheet -\------------------------------------------------------------------------------------------------*/ - - /***--------------------------------------------------------------------------------------\ - Table layout - \--------------------------------------------------------------------------------------***/ - - div.aeTable div.wc-navbar { - margin-top:5px; - } - - div.aeTable span.headingtext { - color:#377EB8; - } - - div.aeTable span.labeltext { - color:#BBB; - font-size:14px; - } - - div.aeTable div.wc-navbar .wc-dropdown-menu li { - cursor: pointer; - } - - div.aeTable div.wc-navbar ul.wc-dropdown-menu li a.disabled { - color: #BBB; - } - - div.aeExplorer .wc-alert{ - padding: 20px; - background-color: #f44336; /* Red */ - color: white; - margin-bottom: 15px; - border-radius:5px; - } - /***--------------------------------------------------------------------------------------\ - Controls layout - \--------------------------------------------------------------------------------------***/ - div.aeTable .controls .filterDiv{ - display:inline-block; - vertical-align:top; - padding-right:10px; - - } - - div.aeTable .controls .filterDiv.custom-filters{ - display:block; - } - - div.aeTable .controls .filterDiv.custom-filters select{ - min-width: 3em; - } - - - /*Rate filter*/ - div.aeTable .controls .rate-filter { - margin-bottom: 10px; - } - - div.aeTable .controls .rate-filter > * { - display: table-cell; - vertical-align: middle; - float: left; - } - - /*Summary Radio Buttons*/ - div.aeTable .controls .summary-control { - float: left; - margin-bottom: 10px; - } - - div.aeTable .controls .summary-control > * { - display: table-cell; - float: left; - } - - div.aeTable .controls .summary-control div { - display: table-cell; - vertical-align: middle; - float: left; - } - - div.aeTable .controls .summary-control div label:hover { - color: blue; - } - - div.aeTable .controls .summary-control div label input { - float: left; - } - - /*Rate Filter*/ - div.aeTable span.sectionHead { - font-weight: bold; - padding-right: 10px; - } - - div.aeTable .controls .rate-filter .rateFilterDiv > * { - font-size: small; - float: left; - padding: 3px; - display: table-cell; - line-height: 1.5em; - } - - div.aeTable .controls .rate-filter span.add-on { - background: #eee; - text-align: center; - text-shadow: 0 1px 0 #fff; - border: 1px solid #ccc; - } - - div.aeTable .controls .rate-filter span.add-on.before { - margin-right: -1px; - -webkit-border-radius: 4px 0 0 4px; - -moz-border-radius: 4px 0 0 4px; - border-radius: 4px 0 0 4px; - } - - div.aeTable .controls .rate-filter span.add-on.after { - margin-left: -1px; - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; - } - - div.aeTable .controls .rate-filter input { - text-align: right; - width: 40px; - border: 1px solid #ccc; - } - - /* Custom Filters */ - div.aeTable .controls .custom-filters { - clear: both; - } - - div.aeTable .controls .custom-filters ul li { - margin: 0 3px 5px 0; - } - - div.aeTable .controls .custom-filters ul li.wc-active > a > label, - div.aeTable .controls .custom-filters ul li:hover > a > label { - color: white; - } - - div.aeTable .controls .custom-filters ul li .filterType{ - font-size: 0.8em - } - - div.aeTable .controls .custom-filters ul li.filterCustom { - display:inline-block ; - padding-right:5px; - } - - div.aeTable .controls .custom-filters ul li.filterCustom * { - display:block ; - } - - div.aeTable .controls .custom-filters ul li.filterCustom span.filterLabel * { - display:inline-block ; - } - - div.aeTable .controls .custom-filters ul li.filterCustom span.filterLabel .filterType { - font-style:italic; - padding-left:3px; - cursor: pointer; - } - - /*Search*/ - div.aeTable span.search-label { - padding-left:5px; - margin-right:5px; - } - - div.aeTable span.search-label span.clear-search { - color:red; - padding-left:5px; - cursor:pointer; - } - - div.aeTable .wc-navbar-search .search-query { - line-height: 1.3em; - } - - /***--------------------------------------------------------------------------------------\ - Summary table formatting - \--------------------------------------------------------------------------------------***/ - - div.aeTable .SummaryTable table { - border-collapse:collapse; - } - - div.aeTable .SummaryTable tr td { - text-align:left; - padding: 0.5em 5px; - } - - td.rowLabel.highlight { - text-decoration:underline; - cursor:pointer; - } - - div.aeTable tr.major td.controls:hover { - cursor:pointer; - } - - /*Handles indentation for nested rows*/ - div.aeTable tr.minor td.rowLabel { - padding-left:15px; - } - - div.aeTable .SummaryTable tr td.values, - div.aeTable .SummaryTable tr td.prevplot, - div.aeTable .SummaryTable tr td.diffplot, - div.aeTable .SummaryTable th { - text-align:center; - } - - div.aeTable .SummaryTable th span.group-num { - font-size: 9pt; - } - - div.aeTable .SummaryTable thead tr:last-of-type th, - div.aeTable .SummaryTable thead tr th[rowspan="2"] { - border-bottom: 1px solid #555; - } - - div.aeTable .SummaryTable th, - div.aeTable .SummaryTable tfoot td { - padding: 0 5px; - background-color:#eee; - font-weight: bold; - border: none; - } - - /* alignment for column header/axes*/ - div.aeTable .SummaryTable th.prevHeader svg, - div.aeTable .SummaryTable th.diffplot.axis svg { - vertical-align: bottom; - } - - div.aeTable .SummaryTable th.prevHeader, - div.aeTable .SummaryTable th.diffplot.axis { - padding-bottom: 0px; - vertical-align:bottom; - } - - div.aeTable .SummaryTable th.prevHeader svg text, - div.aeTable .SummaryTable th.diffplot.axis svg text { - font-size: 10px; - } - - div.aeTable .SummaryTable tfoot, - div.aeTable .SummaryTable thead { - border-top:1px #555 solid; - border-bottom:1px #555 solid; - } - - div.aeTable .SummaryTable i { - cursor: pointer; - font-size: .8em; - top: 0px; - } - - /* Handle visibility in various scenarios */ - div.aeTable .SummaryTable tr.filter, - div.aeTable .SummaryTable table tbody.minorHidden tr.minor, - div.aeTable .SummaryTable tr.inactive, - div.aeTable .SummaryTable table.summary { - display:none; - } - - div.aeTable .wc-hidden, - div.aeTable .controls .wc-hidden, - div.aeTable .controls .filterDiv.custom-filters.wc-hidden { - display: none; - } - - /*using set widths to avoid funky behavior on mouseover - could maybe make the width interactive based on the data? */ - div.aeTable .SummaryTable tr td.values { - min-width:100px; - white-space: nowrap; - } - - div.aeTable .SummaryTable table tr:hover { - background-color:#eee; - } - - div.aeTable .SummaryTable button.closeDetailTable { - margin-right:auto; - margin-left:auto; - } - - /* Search Visibility */ - div.aeTable .SummaryTable.search tbody:not(.search) tr, .SummaryTable.search tbody.search tr:not(.search) { - display:none; - } - - div.aeTable .SummaryTable.search tbody.search tr.search, .SummaryTable.search tbody.search tr.major { - display:table-row; - } - - div.aeTable .SummaryTable.search tbody tr.major td.controls span.toggle.icon { - display:none; - } - - div.aeTable .SummaryTable.search tbody td.prevplot > svg > g.points > text { - font-size: 10px; - } - - div.aeTable span.search { - background:#feb24c; - color:black; - } - - div.aeTable span.icon.toggle { - color: inherit; - background-position: inherit; - } - - div.aeTable span.icon.toggle.transparent > i, - div.aeTable span.icon.details.transparent > i { - color: transparent; - background-position: 1000px; - } - - /***--------------------------------------------------------------------------------------\ - Summary plot formatting - \--------------------------------------------------------------------------------------***/ - - div.aeTable .axis path { - fill:none; - stroke:none; - } - - div.aeTable .axis line { - fill: none; - stroke: #999; - shape-rendering: crispEdges; - } - - div.aeTable .axis { - font-size:10px; - } - - /***--------------------------------------------------------------------------------------\ - Details table formatting - \--------------------------------------------------------------------------------------***/ - - div.aeTable .DetailTable h4 { - text-decoration: none; - font-weight: normal; - margin-top: 10px; - } - - div.aeTable .DetailTable h4 b { - text-decoration: none; - font-weight: bold; - } - - div.aeTable .DetailTable table { - font-size: 9pt; - } - - div.aeTable .DetailTable th { - padding: 3px 5px; - text-align: left; - } diff --git a/inst/htmlwidgets/lib/aeexplorer-3.3.5/aeTable.js b/inst/htmlwidgets/lib/aeexplorer-3.3.5/aeTable.js deleted file mode 100644 index a2dfc076..00000000 --- a/inst/htmlwidgets/lib/aeexplorer-3.3.5/aeTable.js +++ /dev/null @@ -1,2329 +0,0 @@ -(function(global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' - ? (module.exports = factory()) - : typeof define === 'function' && define.amd - ? define(factory) - : (global.aeTable = factory()); -})(this, function() { - 'use strict'; - /*------------------------------------------------------------------------------------------------\ - Initialize adverse event explorer. - \------------------------------------------------------------------------------------------------*/ - - function init(data) { - var settings = this.config; - - //create chart wrapper in specified div - this.wrap = d3.select(this.element).append('div'); - this.wrap.attr('class', 'aeExplorer'); - - //save raw data - this.raw_data = data; - - //settings and defaults - this.util.setDefaults(this); - this.layout(); - - //Flag placeholder rows in raw data save a separate event-only data set - var placeholderCol = this.config.defaults.placeholderFlag.value_col; - var placeholderValues = this.config.defaults.placeholderFlag.values; - this.raw_data.forEach(function(d) { - return (d.placeholderFlag = placeholderValues.indexOf(d[placeholderCol]) > -1); - }); - this.raw_event_data = data.filter(function(d) { - return !d.placeholderFlag; - }); - //draw controls and initial chart - this.controls.init(this); - this.AETable.redraw(this); - } - - /*------------------------------------------------------------------------------------------------\ - Set colors. - \------------------------------------------------------------------------------------------------*/ - - var colorScale = d3.scale - .ordinal() - .range(['#377EB8', '#4DAF4A', '#984EA3', '#FF7F00', '#A65628', '#F781BF', '#E41A1C']); - - /*------------------------------------------------------------------------------------------------\ - Generate HTML containers. - \------------------------------------------------------------------------------------------------*/ - - function layout() { - var wrapper = this.wrap - .append('div') - .attr('class', 'aeTable') - .append('div') - .attr('class', 'table-wrapper'); - wrapper.append('div').attr('class', 'controls'); - wrapper.append('div').attr('class', 'SummaryTable'); - if (this.config.validation) - this.wrap - .append('a') - .attr({ - id: 'downloadCSV' - }) - .text('Download Summarized Data'); - } - - /*------------------------------------------------------------------------------------------------\ - Initialize controls. - \------------------------------------------------------------------------------------------------*/ - - function init$1(chart) { - chart.controls.wrap = chart.wrap.select('div.controls'); - chart.controls.wrap.attr('onsubmit', 'return false;'); - chart.controls.wrap.selectAll('*').remove(); //Clear controls. - - //Draw variable controls if options are specified - if (chart.config.defaults.useVariableControls) { - var optionList = ['id', 'major', 'minor', 'group']; - optionList.forEach(function(option) { - if (chart.config.variableOptions[option].length > 1) { - chart.controls.variableSelect.init(chart, option); - } - }); - } - - //Draw standard UI components - chart.controls.filters.rate.init(chart); - chart.controls.summaryControl.init(chart); - chart.controls.search.init(chart); - chart.controls.filters.custom.init(chart); - - //Initialize the filter rate. - chart.controls.filters.rate.set(chart); - - //assign filterDiv class to all filter wrappers - chart.controls.wrap.selectAll('div').classed('filterDiv', true); - } - - /*------------------------------------------------------------------------------------------------\ - Initialize rate filter. - \------------------------------------------------------------------------------------------------*/ - - function init$2(chart) { - //create the wrapper - var selector = chart.controls.wrap.append('div').attr('class', 'rate-filter'); - - //Clear rate filter. - selector.selectAll('span.filterLabel, div.rateFilterDiv').remove(); - - //Generate rate filter. - selector.append('span').attr('class', 'sectionHead').text('Filter by prevalence:'); - - var rateFilter = selector - .append('div') - .attr('class', 'input-prepend input-append input-medium rateFilterDiv'); - rateFilter.append('span').attr('class', 'add-on before').html('≥'); - rateFilter.append('input').attr({ - class: 'appendedPrependedInput rateFilter', - type: 'number' - }); - rateFilter.append('span').attr('class', 'add-on after').text('%'); - - //event listener - rateFilter.on('input', function(d) { - //Clear filter flags. - chart.wrap.selectAll('.SummaryTable table tbody tr').classed('filter', false); - - //Add filter flags. - chart.AETable.toggleRows(chart); - }); - } - - /*------------------------------------------------------------------------------------------------\ - Set rate filter default. - \------------------------------------------------------------------------------------------------*/ - - function set(chart) { - chart.controls.wrap - .select('input.rateFilter') - .property( - 'value', - chart.config.defaults.maxPrevalence ? chart.config.defaults.maxPrevalence : 0 - ); - } - - /*------------------------------------------------------------------------------------------------\ - Define rate filter object. - \------------------------------------------------------------------------------------------------*/ - - var rate = { - init: init$2, - set: set - }; - - /*------------------------------------------------------------------------------------------------\ - Initialize custom controls. - \------------------------------------------------------------------------------------------------*/ - - //export function init(selector, data, vars, settings) { - function init$3(chart) { - //initialize the wrapper - var selector = chart.controls.wrap.append('div').attr('class', 'custom-filters'); - - //add a list of values to each filter object - chart.config.variables.filters.forEach(function(e) { - var currentData = e.type == 'participant' ? chart.raw_data : chart.raw_event_data; - e.values = d3 - .nest() - .key(function(d) { - return d[e.value_col]; - }) - .entries(currentData) - .map(function(d) { - return d.key; - }); - }); - - //drop filters with 0 or 1 levels and throw a warning - chart.config.variables.filters = chart.config.variables.filters.filter(function(d) { - if (d.values.length <= 1) { - console.warn( - d.value_col + ' filter not shown since the variable has less than 2 levels' - ); - } - return d.values.length > 1; - }); - - //Clear custom controls. - selector.selectAll('ul.nav').remove(); - - //Add filter controls. - var filterList = selector.append('ul').attr('class', 'nav'); - var filterItem = filterList - .selectAll('li') - .data(chart.config.variables.filters) - .enter() - .append('li') - .attr('class', function(d) { - return 'custom-' + d.key + ' filterCustom'; - }); - var filterLabel = filterItem.append('span').attr('class', 'filterLabel'); - - filterLabel.append('span').html(function(d) { - return d.label || d.value_col; - }); - - filterLabel - .append('sup') - .attr('class', 'filterType') - .text(function(d) { - return d.type == 'event' ? 'E' : 'P'; - }) - .property('title', function(d) { - return d.type == 'event' - ? 'Event filter: Changes rate counts only. Does not change population.' - : 'Participant filter: Changes rate counts and populations.'; - }); - - var filterCustom = filterItem.append('select').attr('multiple', true); - - //Add data-driven filter options. - var filterItems = filterCustom - .selectAll('option') - .data(function(d) { - return d.values.map(function(di) { - return { - value: di, - selected: Array.isArray(d.start) && d.start.length - ? d.start.indexOf(di) > -1 - : true - }; - }); - }) - .enter() - .append('option') - .html(function(d) { - return d.value; - }) - .attr('value', function(d) { - return d.value; - }) - .attr('selected', function(d) { - return d.selected ? 'selected' : null; - }); - - //Initialize event listeners - filterCustom.on('change', function() { - chart.AETable.redraw(chart); - }); - } - - /*------------------------------------------------------------------------------------------------\ - Define custom filters object. - \------------------------------------------------------------------------------------------------*/ - - var custom = { init: init$3 }; - - /*------------------------------------------------------------------------------------------------\ - Define filter controls object. - \------------------------------------------------------------------------------------------------*/ - - var filters = { - rate: rate, - custom: custom - }; - - /*------------------------------------------------------------------------------------------------\ - - Initialize summary control. - - \------------------------------------------------------------------------------------------------*/ - - function init$4(chart) { - //set the initial summary status - chart.config.summary = chart.config.defaults.summarizeBy; - - //create element - var selector = chart.controls.wrap.append('div').attr('class', 'summary-control'); - - //Clear summary control. - selector.selectAll('div.summaryDiv').remove(); - - //Generate summary control. - selector.append('span').attr('class', 'sectionHead').text('Summarize by:'); - - var summaryControl = selector - .append('div') - .attr('class', 'input-prepend input-append input-medium summaryDiv'); - summaryControl - .selectAll('div') - .data(['participant', 'event']) - .enter() - .append('div') - .append('label') - .style('font-weight', function(d) { - return d === chart.config.summary ? 'bold' : null; - }) - .text(function(d) { - return d; - }) - .append('input') - .attr({ - class: 'appendedPrependedInput summaryRadio', - type: 'radio' - }) - .property('checked', function(d) { - return d === chart.config.summary; - }); - - //initialize event listener - var radios = chart.wrap.selectAll('div.summaryDiv .summaryRadio'); - radios.on('change', function(d) { - radios.each(function(di) { - d3.select(this.parentNode).style('font-weight', 'normal'); - d3.select(this)[0][0].checked = false; - }); - d3.select(this)[0][0].checked = true; - d3.select(this.parentNode).style('font-weight', 'bold'); - chart.config.summary = d3.select(this.parentNode)[0][0].textContent; - chart.AETable.redraw(chart); - }); - } - - /*------------------------------------------------------------------------------------------------\ - Define search control object. - \------------------------------------------------------------------------------------------------*/ - - var summaryControl = { init: init$4 }; - - function init$5(chart, variable) { - var selector = chart.controls.wrap.append('div').attr('class', 'variable-control variable'); - - //Clear summary control. - selector.selectAll('div.summaryDiv').remove(); - - //Generate summary control. - var labels = { - major: 'Major Category Variable:', - minor: 'Minor Category Variable:', - group: 'Group Variable:', - id: 'ID Variable:' - }; - selector.append('span').attr('class', 'sectionHead').text(labels[variable]); - - var variableControl = selector.append('select'); - - variableControl - .selectAll('option') - .data(chart.config.variableOptions[variable]) - .enter() - .append('option') - .text(function(d) { - return d; - }) - .property('selected', function(d) { - if ((variable == 'group') & !chart.config.defaults.groupCols) { - return d == 'None'; - } else { - return d === chart.config.variables[variable]; - } - }); - - //initialize event listener - variableControl.on('change', function(d) { - var current = this.value; - if (current != 'None') chart.config.variables[variable] = current; - - //update config.groups if needed - console.log(chart); - if (variable == 'group') { - if (current == 'None') { - chart.config.defaults.diffCol = false; - chart.config.defaults.groupCols = false; - chart.config.defaults.totalCol = true; - } else { - chart.config.defaults.groupCols = true; - chart.config.defaults.diffCol = true; - } - - //update the groups setting - var allGroups = d3 - .set( - chart.raw_data.map(function(d) { - return d[chart.config.variables.group]; - }) - ) - .values(); - var groupsObject = allGroups.map(function(d) { - return { key: d }; - }); - chart.config.groups = groupsObject.sort(function(a, b) { - return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; - }); - - //update the color scale - var levels = chart.config.groups.map(function(e) { - return e.key; - }); - var colors = [ - '#377EB8', - '#4DAF4A', - '#984EA3', - '#FF7F00', - '#A65628', - '#F781BF', - '#E41A1C' - ]; - if (chart.config.defaults.totalCol) - //Set 'Total' column color to #777. - colors[chart.config.groups.length] = '#777'; - - chart.colorScale.range(colors).domain(levels); - } - - //Check to see if there are too many levels in the new group variable - if ( - (chart.config.groups.length > chart.config.defaults.maxGroups) & - (current != 'None') - ) { - chart.wrap - .select('.aeTable') - .select('.table-wrapper') - .select('.SummaryTable') - .style('display', 'none'); - var errorText = - 'Too Many Group Variables specified. You specified ' + - chart.config.groups.length + - ', but the maximum supported is ' + - chart.config.defaults.maxGroups + - '.'; - chart.wrap.selectAll('div.wc-alert').remove(); - chart.wrap - .append('div') - .attr('class', 'wc-alert') - .text('Fatal Error: ' + errorText); - throw new Error(errorText); - } else { - chart.wrap - .select('.aeTable') - .select('.table-wrapper') - .select('.SummaryTable') - .style('display', null); - chart.wrap.selectAll('div.wc-alert').remove(); - chart.AETable.redraw(chart); - } - }); - } - - /*------------------------------------------------------------------------------------------------\ - Define search control object. - \------------------------------------------------------------------------------------------------*/ - - var variableSelect = { init: init$5 }; - - /*------------------------------------------------------------------------------------------------\ - Initialize search control. - \------------------------------------------------------------------------------------------------*/ - - function init$6(chart) { - //draw the search control - var selector = chart.controls.wrap - .append('div') - .attr('class', 'searchForm wc-navbar-search pull-right') - .attr('onsubmit', 'return false;'); - - //Clear search control. - selector.selectAll('span.seach-label, input.searchBar').remove(); - - //Generate search control. - var searchLabel = selector.append('span').attr('class', 'search-label label wc-hidden'); - searchLabel.append('span').attr('class', 'search-count'); - searchLabel.append('span').attr('class', 'clear-search').html('☓'); - selector - .append('input') - .attr('type', 'text') - .attr('class', 'searchBar search-query input-medium') - .attr('placeholder', 'Search'); - - //event listeners for search - chart.wrap.select('input.searchBar').on('input', function(d) { - var searchTerm = d3.select(this).property('value').toLowerCase(); - - if (searchTerm.length > 0) { - //Clear the previous search but preserve search text. - chart.controls.search.clear(chart); - d3.select(this).property('value', searchTerm); - - //Clear flags. - chart.wrap.selectAll('div.SummaryTable table tbody').classed('minorHidden', false); - chart.wrap.selectAll('div.SummaryTable table tbody tr').classed('filter', false); - chart.wrap.select('div.SummaryTable').classed('search', false); - chart.wrap.selectAll('div.SummaryTable table tbody').classed('search', false); - chart.wrap.selectAll('div.SummaryTable table tbody tr').classed('search', false); - - //Hide expand/collapse cells. - chart.wrap - .selectAll('div.SummaryTable table tbody tr td.controls span') - .classed('wc-hidden', true); - - //Display 'clear search' icon. - chart.wrap.select('span.search-label').classed('wc-hidden', false); - - //Flag summary table. - var tab = chart.wrap.select('div.SummaryTable').classed('search', true); - - //Capture rows which contain the search term. - var tbodyMatch = tab.select('table').selectAll('tbody').each(function(bodyElement) { - var bodyCurrent = d3.select(this); - var bodyData = bodyCurrent.data()[0]; - - bodyCurrent.selectAll('tr').each(function(rowElement) { - var rowCurrent = d3.select(this); - var rowData = rowCurrent.data()[0]; - var rowText = rowCurrent.classed('major') - ? bodyData.key.toLowerCase() - : rowData.key.toLowerCase(); - - if (rowText.search(searchTerm) >= 0) { - bodyCurrent.classed('search', true); - rowCurrent.classed('search', true); - - //Highlight search text in selected table cell. - var currentText = rowCurrent.select('td.rowLabel').html(); - var searchStart = currentText.toLowerCase().search(searchTerm); - var searchStop = searchStart + searchTerm.length; - var newText = - currentText.slice(0, searchStart) + - '' + - currentText.slice(searchStart, searchStop) + - '' + - currentText.slice(searchStop, currentText.length); - rowCurrent.select('td.rowLabel').html(newText); - } - }); - }); - - //Disable the rate filter. - d3.select('input.rateFilter').property('disabled', true); - - //Update the search label. - var matchCount = chart.wrap.selectAll('tr.search')[0].length; - chart.wrap.select('span.search-count').text(matchCount + ' matches'); - chart.wrap - .select('span.search-label') - .attr( - 'class', - matchCount === 0 - ? 'search-label label label-warning' - : 'search-label label label-success' - ); - - //Check whether search term returned zero matches. - if (matchCount === 0) { - //Restore the table. - chart.wrap.selectAll('div.SummaryTable').classed('search', false); - chart.wrap.selectAll('div.SummaryTable table tbody').classed('search', false); - chart.wrap - .selectAll('div.SummaryTable table tbody tr') - .classed('search', false); - - //Reset the filters and row toggle. - chart.AETable.toggleRows(chart); - } - } else chart.controls.search.clear(chart); - }); - - chart.wrap.select('span.clear-search').on('click', function() { - chart.controls.search.clear(chart); - }); - } - - /*------------------------------------------------------------------------------------------------\ - Clear search term results. - \------------------------------------------------------------------------------------------------*/ - - function clear(chart) { - //Re-enable rate filter. - chart.wrap.select('input.rateFilter').property('disabled', false); - - //Clear search box. - chart.wrap.select('input.searchBar').property('value', ''); - - //Remove search highlighting. - chart.wrap - .selectAll('div.SummaryTable table tbody tr.search td.rowLabel') - .html(function(d) { - return d.values[0].values['label']; - }); - - //Remove 'clear search' icon and label. - chart.wrap.select('span.search-label').classed('wc-hidden', true); - - //Clear search flags. - chart.wrap.selectAll('div.SummaryTable').classed('search', false); - chart.wrap.selectAll('div.SummaryTable table tbody').classed('search', false); - chart.wrap.selectAll('div.SummaryTable table tbody tr').classed('search', false); - - //Reset filters and row toggle. - chart.AETable.toggleRows(chart); - } - - /*------------------------------------------------------------------------------------------------\ - Define search control object. - \------------------------------------------------------------------------------------------------*/ - - var search = { - init: init$6, - clear: clear - }; - - /*------------------------------------------------------------------------------------------------\ - Define controls object. - \------------------------------------------------------------------------------------------------*/ - - var controls = { - init: init$1, - filters: filters, - summaryControl: summaryControl, - variableSelect: variableSelect, - search: search - }; - - /*------------------------------------------------------------------------------------------------\ - Clear the current chart and draw a new one. - \------------------------------------------------------------------------------------------------*/ - - function redraw(chart) { - chart.controls.search.clear(chart); - chart.AETable.wipe(chart.wrap); - chart.util.prepareData(chart); - chart.AETable.init(chart); - chart.AETable.toggleRows(chart); - } - - /*------------------------------------------------------------------------------------------------\ - Clears the summary or detail table and all associated buttons. - \------------------------------------------------------------------------------------------------*/ - - function wipe(canvas) { - canvas.select('.table-wrapper .SummaryTable .wc-alert').remove(); - canvas.select('.table-wrapper .SummaryTable table').remove(); - canvas.select('.table-wrapper .SummaryTable button').remove(); - canvas.select('.table-wrapper .DetailTable').remove(); - canvas.select('.table-wrapper .DetailTable').remove(); - } - - /*------------------------------------------------------------------------------------------------\ - - annoteDetails(table, canvas, row, group) - - Convenience function that shows the raw #s and annotates point values for a single group - - + table - - AE table object - + rows - - highlighted row(s) (selection containing 'tr' objects) - + group - - group to highlight - - \------------------------------------------------------------------------------------------------*/ - - function showCellCounts(chart, rows, group) { - //Add raw counts for the specified row/groups . - rows - .selectAll('td.values') - .filter(function(e) { - return e.key === group; - }) - .append('span.annote') - .classed('annote', true) - .text(function(d) { - return ' (' + d['values'].n + '/' + d['values'].tot + ')'; - }); - } - - /*------------------------------------------------------------------------------------------------\ - Calculate differences between groups. - \------------------------------------------------------------------------------------------------*/ - - function calculateDifference(major, minor, group1, group2, n1, tot1, n2, tot2) { - var p1 = n1 / tot1; - var p2 = n2 / tot2; - var diff = p1 - p2; - var se = Math.sqrt(p1 * (1 - p1) / tot1 + p2 * (1 - p2) / tot2); - var lower = diff - 1.96 * se; - var upper = diff + 1.96 * se; - var sig = (lower > 0) | (upper < 0) ? 1 : 0; - var summary = { - major: major, - minor: minor, - - group1: group1, - n1: n1, - tot1: tot1, - p1: p1, - - group2: group2, - n2: n2, - tot2: tot2, - p2: p2, - - diff: diff * 100, - lower: lower * 100, - upper: upper * 100, - sig: sig - }; - - return summary; - } - - /*------------------------------------------------------------------------------------------------\ - Add differences to data object. - \------------------------------------------------------------------------------------------------*/ - - function addDifferences(data, groups) { - var nGroups = groups.length; - - if (nGroups > 1) { - data.forEach(function(major) { - major.values.forEach(function(minor) { - minor.differences = []; - - var groups = minor.values; - var otherGroups = [].concat(minor.values); - - groups.forEach(function(group) { - delete otherGroups[ - otherGroups - .map(function(m) { - return m.key; - }) - .indexOf(group.key) - ]; - otherGroups.forEach(function(otherGroup) { - var diff = calculateDifference( - major.key, - minor.key, - group.key, - otherGroup.key, - group.values.n, - group.values.tot, - otherGroup.values.n, - otherGroup.values.tot - ); - minor.differences.push(diff); - }); - }); - }); - }); - } - - return data; - } - - /*------------------------------------------------------------------------------------------------\ - Calculate number of events, number of subjects, and adverse event rate by major, minor, and - group. - \------------------------------------------------------------------------------------------------*/ - - function cross(data, groups, id, major, minor, group) { - var groupNames = groups.map(function(d) { - return d.key; - }); - var summary = d3.selectAll('.summaryDiv label').filter(function(d) { - return d3.select(this).selectAll('.summaryRadio').property('checked'); - })[0][0].textContent; - - //Calculate [id] and event frequencies and rates by [major], [minor], and [group]. - var nestedData = d3 - .nest() - .key(function(d) { - return major == 'All' ? 'All' : d[major]; - }) - .key(function(d) { - return minor == 'All' ? 'All' : d[minor]; - }) - .key(function(d) { - return d[group]; - }) - .rollup(function(d) { - var selection = {}; - - //Category - selection.major = major === 'All' ? 'All' : d[0][major]; - selection.minor = minor === 'All' ? 'All' : d[0][minor]; - selection.label = selection.minor === 'All' ? selection.major : selection.minor; - selection.group = d[0][group]; - - var currentGroup = groups.filter(function(di) { - return di.key === d[0][group]; - }); - - //Numerator/denominator - if (summary === 'participant') { - var ids = d3 - .nest() - .key(function(di) { - return di[id]; - }) - .entries(d); - selection.n = ids.length; - selection.tot = currentGroup[0].n; - } else { - selection.n = d.length; - selection.tot = currentGroup[0].nEvents; - } - - //Rate - selection.per = Math.round(selection.n / selection.tot * 1000) / 10; - - return selection; - }) - .entries(data); - - //Generate data objects for major*minor*group combinations absent in data. - nestedData.forEach(function(dMajor) { - dMajor.values.forEach(function(dMinor) { - var currentGroupList = dMinor.values.map(function(d) { - return d.key; - }); - - groupNames.forEach(function(dGroup, groupIndex) { - if (currentGroupList.indexOf(dGroup) === -1) { - var currentGroup = groups.filter(function(d) { - return d.key === dGroup; - }); - var tot = summary === 'participant' - ? currentGroup[0].n - : currentGroup[0].nEvents; - - var shellMajorMinorGroup = { - key: dGroup, - values: { - major: dMajor.key, - minor: dMinor.key, - label: dMinor.key === 'All' ? dMajor.key : dMinor.key, - group: dGroup, - - n: 0, - tot: tot, - per: 0 - } - }; - - dMinor.values.push(shellMajorMinorGroup); - } - }); - - dMinor.values.sort(function(a, b) { - return ( - groups - .map(function(group) { - return group.key; - }) - .indexOf(a.key) - - groups - .map(function(group) { - return group.key; - }) - .indexOf(b.key) - ); - }); - }); - }); - - return nestedData; - } - - /*------------------------------------------------------------------------------------------------\ - Define sorting algorithms. - \------------------------------------------------------------------------------------------------*/ - - var sort = { - //Sort by descending frequency. - maxPer: function maxPer(a, b) { - var max_a = a.values.map(function(minor) { - var n = d3.sum( - minor.values.map(function(group) { - return group.values.n; - }) - ); - var tot = d3.sum( - minor.values.map(function(group) { - return group.values.tot; - }) - ); - return n / tot; - })[0]; - var max_b = b.values.map(function(minor) { - var n = d3.sum( - minor.values.map(function(group) { - return group.values.n; - }) - ); - var tot = d3.sum( - minor.values.map(function(group) { - return group.values.tot; - }) - ); - return n / tot; - })[0]; - var diff = max_b - max_a; - - return diff ? diff : a.key < b.key ? -1 : 1; - } - }; - - /**-------------------------------------------------------------------------------------------\ - - fillrow(currentRow, chart, d) - - inputs (all required): - currentRow = d3.selector for a 'tr' element - chart = the chart object - d = the raw data for the row - - - Convienence function which fills each table row and draws the plots. - - + Note1: We'll call this 3x. Once for the major rows, once for - the minor rows and once for overall row. - - + Note2: Would be good to split out separate plotting functions if - this gets too much more complex. - - \-------------------------------------------------------------------------------------------**/ - - function fillRow(currentRow, chart, d) { - var table = chart; - //Append major row expand/collapse control. - var controlCell = currentRow.append('td').attr('class', 'controls'); - - if (d.key === 'All') { - controlCell.append('span').attr('title', 'Expand').text('+'); - } - - //Append row label. - var category = currentRow.append('td').attr({ - class: 'rowLabel', - title: 'Show listing' - }); - category.append('a').text(function(rowValues) { - return rowValues.values[0].values['label']; - }); - - //Calculate total frequency, number of records, population denominator, and rate. - if (chart.config.defaults.totalCol) { - var total = {}; - total.major = d.values[0].values.major; - total.minor = d.values[0].values.minor; - total.label = d.values[0].values.label; - total.group = 'Total'; - - total.n = d3.sum(d.values, function(di) { - return di.values.n; - }); - total.tot = d3.sum(d.values, function(di) { - return di.values.tot; - }); - - total.per = total.n / total.tot * 100; - - d.values[d.values.length] = { - key: 'Total', - values: total - }; - } - - //Append textual rates. - var values = currentRow - .selectAll('td.values') - .data(d.values, function(d) { - return d.key; - }) - .enter() - .append('td') - .attr('class', 'values') - .classed('total', function(d) { - return d.key == 'Total'; - }) - .classed('wc-hidden', function(d) { - if (d.key == 'Total') { - return !chart.config.defaults.totalCol; - } else { - return !chart.config.defaults.groupCols; - } - }) - .attr('title', function(d) { - return d.values.n + '/' + d.values.tot; - }) - .text(function(d) { - return d3.format('0.1f')(d['values'].per) + '%'; - }) - .style('color', function(d) { - return table.colorScale(d.key); - }); - - //Append graphical rates. - var prevalencePlot = currentRow - .append('td') - .classed('prevplot', true) - .append('svg') - .attr('height', chart.config.plotSettings.h) - .attr('width', chart.config.plotSettings.w + 10) - .append('svg:g') - .attr('transform', 'translate(5,0)'); - - var points = prevalencePlot - .selectAll('g.points') - .data(d.values) - .enter() - .append('g') - .attr('class', 'points'); - - points - .append('svg:circle') - .attr('cx', function(d) { - return chart.percentScale(d.values['per']); - }) - .attr('cy', chart.config.plotSettings.h / 2) - .attr('r', chart.config.plotSettings.r - 2) - .attr('fill', function(d) { - return table.colorScale(d.values['group']); - }) - .classed('wc-hidden', function(d) { - if (d.key == 'Total') { - return !chart.config.defaults.totalCol; - } else { - return !chart.config.defaults.groupCols; - } - }) - .append('title') - .text(function(d) { - return d.key + ': ' + d3.format(',.1%')(d.values.per / 100); - }); - - //Handle rate differences between groups if settings reference more then one group. - if (chart.config.groups.length > 1 && chart.config.defaults.diffCol) { - //Append container for group rate differences. - var differencePlot = currentRow - .append('td') - .classed('diffplot', true) - .append('svg') - .attr('height', chart.config.plotSettings.h) - .attr('width', chart.config.plotSettings.w + 10) - .append('svg:g') - .attr('transform', 'translate(5,0)'); - - var diffPoints = differencePlot - .selectAll('g') - .data(d.differences) - .enter() - .append('svg:g'); - diffPoints.append('title').text(function(d) { - return ( - d.group1 + - ' (' + - d3.format(',.1%')(d.p1) + - ') vs. ' + - d.group2 + - ' (' + - d3.format(',.1%')(d.p2) + - '): ' + - d3.format(',.1%')(d.diff / 100) - ); - }); - - //Append graphical rate difference confidence intervals. - diffPoints - .append('svg:line') - .attr('x1', function(d) { - return chart.diffScale(d.upper); - }) - .attr('x2', function(d) { - return chart.diffScale(d.lower); - }) - .attr('y1', chart.config.plotSettings.h / 2) - .attr('y2', chart.config.plotSettings.h / 2) - .attr('class', 'ci') - .classed('wc-hidden', chart.config.groups.length > 2) - .attr('stroke', '#bbb'); - - //Append graphical rate differences. - var triangle = d3.svg - .line() - .x(function(d) { - return d.x; - }) - .y(function(d) { - return d.y; - }) - .interpolate('linear-closed'); - - diffPoints - .append('svg:path') - .attr('d', function(d) { - var h = chart.config.plotSettings.h, - r = chart.config.plotSettings.r; - - var leftpoints = [ - { x: chart.diffScale(d.diff), y: h / 2 + r }, //bottom - { x: chart.diffScale(d.diff) - r, y: h / 2 }, //middle-left - { - x: chart.diffScale(d.diff), - y: h / 2 - r //top - } - ]; - return triangle(leftpoints); - }) - .attr('class', 'diamond') - .attr('fill-opacity', function(d) { - return d.sig === 1 ? 1 : 0.1; - }) - .attr('fill', function(d) { - return d.diff < 0 ? chart.colorScale(d.group1) : chart.colorScale(d.group2); - }) - .attr('stroke', function(d) { - return d.diff < 0 ? chart.colorScale(d.group1) : chart.colorScale(d.group2); - }) - .attr('stroke-opacity', 0.3); - - diffPoints - .append('svg:path') - .attr('d', function(d) { - var h = chart.config.plotSettings.h, - r = chart.config.plotSettings.r; - - var rightpoints = [ - { x: chart.diffScale(d.diff), y: h / 2 + r }, //bottom - { x: chart.diffScale(d.diff) + r, y: h / 2 }, //middle-right - { - x: chart.diffScale(d.diff), - y: h / 2 - r //top - } - ]; - return triangle(rightpoints); - }) - .attr('class', 'diamond') - .attr('fill-opacity', function(d) { - return d.sig === 1 ? 1 : 0.1; - }) - .attr('fill', function(d) { - return d.diff < 0 ? chart.colorScale(d.group2) : chart.colorScale(d.group1); - }) - .attr('stroke', function(d) { - return d.diff < 0 ? chart.colorScale(d.group2) : chart.colorScale(d.group1); - }) - .attr('stroke-opacity', 0.3); - } - } - - /*------------------------------------------------------------------------------------------------\ - Collapse data for export to .csv. - \------------------------------------------------------------------------------------------------*/ - - function collapse(nested) { - //Collapse nested object. - var collapsed = nested.map(function(soc) { - var allRows = soc.values.map(function(e) { - var eCollapsed = {}; - eCollapsed.majorCategory = e.values[0].values.major; - eCollapsed.minorCategory = e.values[0].values.minor; - - e.values.forEach(function(val, i) { - var n = i + 1; - eCollapsed['val' + n + '_label'] = val.key; - eCollapsed['val' + n + '_numerator'] = val.values.n; - eCollapsed['val' + n + '_denominator'] = val.values.tot; - eCollapsed['val' + n + '_percent'] = val.values.per; - }); - - if (e.differences) { - e.differences.forEach(function(diff, i) { - var n = i + 1; - eCollapsed['diff' + n + '_label'] = diff.group1 + '-' + diff.group2; - eCollapsed['diff' + n + '_val'] = diff['diff']; - eCollapsed['diff' + n + '_sig'] = diff['sig']; - }); - } - return eCollapsed; - }); - return allRows; - }); - return d3.merge(collapsed); - } - - function json2csv(chart) { - var majorValidation = collapse(chart.data.major), - // flatten major data array - minorValidation = collapse(chart.data.minor), - // flatten minor data array - fullValidation = d3 - .merge([majorValidation, minorValidation]) // combine flattened major and minor data arrays - .sort(function(a, b) { - return a.majorCategory < b.majorCategory - ? -1 - : a.majorCategory > b.majorCategory - ? 1 - : a.minorCategory < b.minorCategory ? -1 : 1; - }), - CSVarray = []; - - fullValidation.forEach(function(d, i) { - //add headers to CSV array - if (i === 0) { - var headers = Object.keys(d).map(function(key) { - return '"' + key.replace(/"/g, '""') + '"'; - }); - CSVarray.push(headers); - } - - //add rows to CSV array - var row = Object.keys(d).map(function(key) { - if (typeof d[key] === 'string') d[key] = d[key].replace(/"/g, '""'); - - return '"' + d[key] + '"'; - }); - - CSVarray.push(row); - }); - - //transform CSV array into CSV string - var CSV = new Blob([CSVarray.join('\n')], { type: 'text/csv;charset=utf-8;' }), - fileName = - chart.config.variables.major + - '-' + - chart.config.variables.minor + - '-' + - chart.config.summary + - '.csv', - link = chart.wrap.select('#downloadCSV'); - - if (navigator.msSaveBlob) { - // IE 10+ - link.style({ - cursor: 'pointer', - 'text-decoration': 'underline', - color: 'blue' - }); - link.on('click', function() { - navigator.msSaveBlob(CSV, fileName); - }); - } else { - // Browsers that support HTML5 download attribute - if (link.node().download !== undefined) { - // feature detection - var url = URL.createObjectURL(CSV); - link.node().setAttribute('href', url); - link.node().setAttribute('download', fileName); - } - } - - return CSVarray; - } - - /*------------------------------------------------------------------------------------------------\ - Filter the raw data per the current filter and group selections. - After this function is executed there should be 4 data objects bound to the chart: - (1) raw_data: an exact copy of the raw data, with an added "placeholderFlag" variable for participants with no events - (2) raw_event_data: an exact copy of the raw data with placeholder rows removed - (3) population_data: a copy of the raw data filtered by: - (a) chart.config.groups - rows from groups not included in the settings object are removed - (b) charts.config.variables.filters[type=="participant"] - according to current user selections - (4) population_event_data a copy of the population_data with: - (a) placeholder rows removed - (b) filtered by charts.config.variables.filters[type=="participant"] - according to current user selections - \------------------------------------------------------------------------------------------------*/ - function prepareData(chart) { - var vars = chart.config.variables; //convenience mapping - - //get filter information - chart.config.variables.filters.forEach(function(filter_d) { - //get a list of values that are currently selected - filter_d.currentValues = []; - var thisFilter = chart.wrap - .select('.custom-filters') - .selectAll('select') - .filter(function(select_d) { - return select_d.value_col == filter_d.value_col; - }); - thisFilter.selectAll('option').each(function(option_d) { - if (d3.select(this).property('selected')) { - filter_d.currentValues.push(option_d.value); - } - }); - }); - ///////////////////////////////// - //Create chart.population_data - ///////////////////////////////// - - //Subset data on groups specified in chart.config.groups. - var groupNames = chart.config.groups.map(function(d) { - return d.key; - }); - chart.population_data = chart.raw_data.filter(function(d) { - return groupNames.indexOf(d[vars['group']]) >= 0; - }); - - //Filter data to reflect the current population (based on filters where type = `participant`) - chart.config.variables.filters - .filter(function(d) { - return d.type == 'participant'; - }) - .forEach(function(filter_d) { - //remove the filtered values from the population data - chart.population_data = chart.population_data.filter(function(rowData) { - return filter_d.currentValues.indexOf('' + rowData[filter_d.value_col]) > -1; - }); - }); - - /////////////////////////////////////// - // create chart.population_event_data - //////////////////////////////////////// - - // Filter event level data - chart.population_event_data = chart.population_data.filter(function(d) { - return !d.placeholderFlag; - }); - - chart.config.variables.filters - .filter(function(d) { - return d.type == 'event'; - }) - .forEach(function(filter_d) { - //remove the filtered values from the population data - chart.population_event_data = chart.population_event_data.filter(function(rowData) { - return filter_d.currentValues.indexOf('' + rowData[filter_d.value_col]) > -1; - }); - }); - - //////////////////////////////////////////////////////////////////////// - // add group-level participant and event counts to chart.config.groups - // Used in group headers and to calculate rates - //////////////////////////////////////////////////////////////////////// - - //Nest data by [vars.group] and [vars.id]. - var nestedData = d3 - .nest() - .key(function(d) { - return d[vars.group]; - }) - .key(function(d) { - return d[vars.id]; - }) - .entries(chart.population_data); - - //Calculate number of participants and number of events for each group. - - chart.config.groups.forEach(function(groupObj) { - //count unique participants - var groupVar = chart.config.variables.group; - var groupValue = groupObj.key; - var groupEvents = chart.population_data.filter(function(f) { - return f[groupVar] == groupValue; - }); - groupObj.n = d3 - .set( - groupEvents.map(function(m) { - return m[chart.config.variables.id]; - }) - ) - .values().length; - - //count number of events - groupObj.nEvents = chart.population_event_data.filter(function(f) { - return f[groupVar] == groupValue; - }).length; - }); - } - - var defaultSettings = { - variables: { - id: 'USUBJID', - major: 'AEBODSYS', - minor: 'AEDECOD', - group: 'ARM', - details: null, - 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 - } - ] - }, - variableOptions: null, - groups: null, - defaults: { - placeholderFlag: { - value_col: 'AEBODSYS', - values: ['NA'] - }, - maxPrevalence: 0, - maxGroups: 6, - totalCol: true, - groupCols: true, - diffCol: true, - prefTerms: false, - summarizeBy: 'participant', - webchartsDetailsTable: false, - useVariableControls: true - }, - plotSettings: { - h: 15, - w: 200, - margin: { - left: 40, - right: 40 - }, - diffMargin: { - left: 5, - right: 5 - }, - r: 7 - }, - validation: false - }; - - /*------------------------------------------------------------------------------------------------\ - Filter the raw data per the current filter and group selections. - \------------------------------------------------------------------------------------------------*/ - function setDefaults(chart) { - function errorNote(msg) { - chart.wrap.append('div').attr('class', 'wc-alert').text('Fatal Error: ' + msg); - } - - ///////////////////////////// - // Fill defaults as needed // - ///////////////////////////// - //variables - chart.config.variables = chart.config.variables || {}; - - var variables = ['id', 'major', 'minor', 'group']; - variables.forEach(function(varName) { - chart.config.variables[varName] = - chart.config.variables[varName] || defaultSettings.variables[varName]; - }); - - //details, filters and groups - chart.config.variables.details = - chart.config.variables.details || defaultSettings.variables.details || []; - - chart.config.variables.filters = - chart.config.variables.filters || defaultSettings.variables.filters || []; - - chart.config.groups = chart.config.groups || defaultSettings.groups || []; - - //variableOptions - chart.config.variableOptions = - chart.config.variableOptions || defaultSettings.variableOptions || {}; - - variables.forEach(function(varName) { - //initialize options for each mapping variable - chart.config.variableOptions[varName] = chart.config.variableOptions[varName] - ? chart.config.variableOptions[varName] - : []; - - //confirm that specified variables are included as options - var options = chart.config.variableOptions[varName]; - if (options.indexOf(chart.config.variables[varName]) == -1) { - options.push(chart.config.variables[varName]); - } - - //add "None" option for group dropdown - - if ((varName == 'group') & (options.indexOf('None') == -1)) { - options.push('None'); - } - }); - - //defaults - chart.config.defaults = chart.config.defaults || {}; - var defaults = Object.keys(defaultSettings.defaults); - defaults.forEach(function(dflt) { - if ( - dflt !== 'placeholderFlag' // handle primitive types such as maxPrevalence - ) - chart.config.defaults[dflt] = chart.config.defaults[dflt] !== undefined - ? chart.config.defaults[dflt] - : defaultSettings.defaults[dflt]; - else { - // handle objects such as placeholderFlag - var object = {}; - for (var prop in defaultSettings.defaults[dflt]) { - object[prop] = chart.config.defaults[dflt] !== undefined - ? chart.config.defaults[dflt][prop] !== undefined - ? chart.config.defaults[dflt][prop] - : defaultSettings.defaults[dflt][prop] - : defaultSettings.defaults[dflt][prop]; - } - chart.config.defaults[dflt] = object; - } - }); - - //plot settings - chart.config.plotSettings = chart.config.plotSettings || {}; - var plotSettings = ['h', 'w', 'r', 'margin', 'diffMargin']; - plotSettings.forEach(function(varName) { - chart.config.plotSettings[varName] = - chart.config.plotSettings[varName] || defaultSettings.plotSettings[varName]; - }); - - //////////////////////////////////////////////////////////// - // Convert group levels from string to objects (if needed)// - //////////////////////////////////////////////////////////// - var allGroups = d3 - .set( - chart.raw_data.map(function(d) { - return d[chart.config.variables.group]; - }) - ) - .values(); - chart.config.groups = chart.config.groups - .map(function(d) { - return typeof d == 'string' ? { key: d } : d; - }) - .filter(function(d) { - if (allGroups.indexOf(d.key) === -1) - console.log( - 'Warning: You specified a group level ("' + - d.key + - '") that was not found in the data. It is being ignored.' - ); - return allGroups.indexOf(d.key) > -1; - }); - - //////////////////////////////////////////////////// - // Include all group levels if none are specified // - //////////////////////////////////////////////////// - - var groupsObject = allGroups.map(function(d) { - return { key: d }; - }); - - if (!chart.config.groups || chart.config.groups.length === 0) - chart.config.groups = groupsObject.sort(function(a, b) { - return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; - }); - - ////////////////////////////////////////////////////////////// - //Check that variables specified in settings exist in data. // - ////////////////////////////////////////////////////////////// - for (var x in chart.config.variables) { - var varList = d3.keys(chart.raw_data[0]).concat('data_all'); - - if (varList.indexOf(chart.config.variables[x]) === -1) { - if (chart.config.variables[x] instanceof Array) { - chart.config.variables[x].forEach(function(e) { - var value_col = e instanceof Object ? e.value_col : e; - if (varList.indexOf(value_col) === -1) { - errorNote('Error in variables object.'); - throw new Error( - x + ' variable ' + "('" + e + "') not found in dataset." - ); - } - }); - } else { - errorNote('Error in variables object.'); - throw new Error( - x + - ' variable ' + - "('" + - chart.config.variables[x] + - "') not found in dataset." - ); - } - } - } - - ///////////////////////////////////////////////////////////////////////////////// - //Checks on group columns (if they're being renderered) // - ///////////////////////////////////////////////////////////////////////////////// - if (chart.config.defaults.groupCols) { - //Check that group values defined in settings are actually present in dataset. // - if ( - chart.config.defaults.groupCols & - (chart.config.groups.length > chart.config.defaults.maxGroups) - ) { - var errorText = - 'Too Many Group Variables specified. You specified ' + - chart.config.groups.length + - ', but the maximum supported is ' + - chart.config.defaults.maxGroups + - '.'; - errorNote(errorText); - throw new Error(errorText); - } - - //Set the domain for the color scale based on groups. // - chart.colorScale.domain( - chart.config.groups.map(function(e) { - return e.key; - }) - ); - } - - //make sure either group or total columns are being renderered - if (!chart.config.defaults.groupCols & !chart.config.defaults.totalCol) { - var errorText = - 'No data to render. Make sure at least one of chart.config.defaults.groupCols or chart.config.defaults.totalCol is set to true.'; - errorNote(errorText); - throw new Error(errorText); - } - - //don't render differences if you're not renderering group columns - if (!chart.config.defaults.groupCols) { - chart.config.defaults.diffCol = false; - } - - //hide the total column if only one group is selected - if (chart.config.groups.length == 1) { - chart.config.defaults.totalCol = false; - } - - //set color for total column - if (chart.config.defaults.totalCol) - //Set 'Total' column color to #777. - chart.colorScale.range()[chart.config.groups.length] = '#777'; - } - - /*------------------------------------------------------------------------------------------------\ - Define util object. - \------------------------------------------------------------------------------------------------*/ - - var util = { - calculateDifference: calculateDifference, - addDifferences: addDifferences, - cross: cross, - sort: sort, - fillRow: fillRow, - collapse: collapse, - json2csv: json2csv, - prepareData: prepareData, - setDefaults: setDefaults - }; - - /*------------------------------------------------------------------------------------------------\ - Call functions to collapse the raw data using the selected categories and create the summary - table. - \------------------------------------------------------------------------------------------------*/ - - function init$7(chart) { - //convinience mappings - var vars = chart.config.variables; - - ///////////////////////////////////////////////////////////////// - // Prepare the data for charting - ///////////////////////////////////////////////////////////////// - chart.data = {}; - - //Create a dataset nested by [ chart.config.variables.group ] and [ chart.config.variables.id ]. - chart.data.any = util.cross( - chart.population_event_data, - chart.config.groups, - vars['id'], - 'All', - 'All', - vars['group'], - chart.config.groups - ); - - //Create a dataset nested by [ chart.config.variables.major ], [ chart.config.variables.group ], and - //[ chart.config.variables.id ]. - chart.data.major = util.cross( - chart.population_event_data, - chart.config.groups, - vars['id'], - vars['major'], - 'All', - vars['group'], - chart.config.groups - ); - - //Create a dataset nested by [ chart.config.variables.major ], [ chart.config.variables.minor ], - //[ chart.config.variables.group ], and [ chart.config.variables.id ]. - chart.data.minor = util.cross( - chart.population_event_data, - chart.config.groups, - vars['id'], - vars['major'], - vars['minor'], - vars['group'], - chart.config.groups - ); - - //Add a 'differences' object to each row. - chart.data.major = util.addDifferences(chart.data.major, chart.config.groups); - chart.data.minor = util.addDifferences(chart.data.minor, chart.config.groups); - chart.data.any = util.addDifferences(chart.data.any, chart.config.groups); - - //Sort the data based by maximum prevelence. - chart.data.major = chart.data.major.sort(util.sort.maxPer); - chart.data.minor.forEach(function(major) { - major.values.sort(function(a, b) { - var max_a = - d3.sum( - a.values.map(function(group) { - return group.values.n; - }) - ) / - d3.sum( - a.values.map(function(group) { - return group.values.tot; - }) - ); - var max_b = - d3.sum( - b.values.map(function(group) { - return group.values.n; - }) - ) / - d3.sum( - b.values.map(function(group) { - return group.values.tot; - }) - ); - var diff = max_b - max_a; - - return diff ? diff : a.key < b.key ? -1 : 1; - }); - }); - - ///////////////////////////////////////////////////////////////// - // Allow the user to download a csv of the current view - ///////////////////////////////////////////////////////////////// - // - //Output the data if the validation setting is flagged. - if (chart.config.validation) chart.data.CSVarray = util.json2csv(chart); - - ///////////////////////////////////// - // Draw the summary table headers. - ///////////////////////////////////// - //Check to make sure there is some data - if (!chart.data.major.length) { - chart.wrap - .select('.SummaryTable') - .append('div') - .attr('class', 'wc-alert') - .text( - 'Error: No AEs found for the current filters. Update the filters to see results.' - ); - throw new Error('No data found in the column specified for major category. '); - } - - var tab = chart.wrap.select('.SummaryTable').append('table'); - var nGroups = chart.config.groups.length; - var header1 = tab.append('thead').append('tr'); - - //Expand/collapse control column header - header1.append('th').attr('rowspan', 2); - - //Category column header - header1.append('th').attr('rowspan', 2).text('Category'); - - //Group column headers - if (chart.config.defaults.groupCols) - header1.append('th').attr('colspan', nGroups).text('Groups'); - - //Total column header - if (chart.config.defaults.totalCol) header1.append('th').text(''); - - //Graphical AE rates column header - header1.append('th').text('AE Rate by group'); - - //Group differences column header - var groupHeaders = chart.config.defaults.groupCols ? chart.config.groups : []; - if (chart.config.defaults.totalCol) { - groupHeaders = groupHeaders.concat({ - key: 'Total', - n: d3.sum(chart.config.groups, function(d) { - return d.n; - }), - nEvents: d3.sum(chart.config.groups, function(d) { - return d.nEvents; - }) - }); - } - - var header2 = tab.select('thead').append('tr'); - header2 - .selectAll('td.values') - .data(groupHeaders) - .enter() - .append('th') - .html(function(d) { - return ( - '' + - d.key + - '' + - '
(n=' + - (chart.config.summary === 'participant' ? d.n : d.nEvents) + - ')' - ); - }) - .style('color', function(d) { - return chart.colorScale(d.key); - }) - .attr('class', 'values') - .classed('total', function(d) { - return d.key == 'Total'; - }) - .classed('wc-hidden', function(d) { - if (d.key == 'Total') { - return !chart.config.defaults.totalCol; - } else { - return !chart.config.defaults.groupCols; - } - }); - header2.append('th').attr('class', 'prevHeader'); - if (nGroups > 1 && chart.config.defaults.diffCol) { - header1.append('th').text('Difference Between Groups').attr('class', 'diffplot'); - header2.append('th').attr('class', 'diffplot axis'); - } - - //Prevalence scales - var allPercents = d3.merge( - chart.data.major.map(function(major) { - return d3.merge( - major.values.map(function(minor) { - return d3.merge( - minor.values.map(function(group) { - return [group.values.per]; - }) - ); - }) - ); - }) - ); - chart.percentScale = d3.scale - .linear() - .range([0, chart.config.plotSettings.w]) - .range([ - chart.config.plotSettings.margin.left, - chart.config.plotSettings.w - chart.config.plotSettings.margin.right - ]) - .domain([0, d3.max(allPercents)]); - - //Add Prevalence Axis - var percentAxis = d3.svg.axis().scale(chart.percentScale).orient('top').ticks(6); - - var prevAxis = chart.wrap - .select('th.prevHeader') - .append('svg') - .attr('height', '34px') - .attr('width', chart.config.plotSettings.w + 10) - .append('svg:g') - .attr('transform', 'translate(5,34)') - .attr('class', 'axis percent') - .call(percentAxis); - - //Difference Scale - if (chart.config.groups.length > 1) { - //Difference Scale - var allDiffs = d3.merge( - chart.data.major.map(function(major) { - return d3.merge( - major.values.map(function(minor) { - return d3.merge( - minor.differences.map(function(diff) { - return [diff.upper, diff.lower]; - }) - ); - }) - ); - }) - ); - - var minorDiffs = d3.merge( - chart.data.minor.map(function(m) { - return d3.merge( - m.values.map(function(m2) { - return d3.merge( - m2.differences.map(function(m3) { - return d3.merge([[m3.upper], [m3.lower]]); - }) - ); - }) - ); - }) - ); - - chart.diffScale = d3.scale - .linear() - .range([ - chart.config.plotSettings.diffMargin.left, - chart.config.plotSettings.w - chart.config.plotSettings.diffMargin.right - ]) - .domain(d3.extent(d3.merge([minorDiffs, allDiffs]))); - - //Difference Axis - var diffAxis = d3.svg.axis().scale(chart.diffScale).orient('top').ticks(8); - - var prevAxis = chart.wrap - .select('th.diffplot.axis') - .append('svg') - .attr('height', '34px') - .attr('width', chart.config.plotSettings.w + 10) - .append('svg:g') - .attr('transform', 'translate(5,34)') - .attr('class', 'axis') - .attr('class', 'percent') - .call(diffAxis); - } - - //////////////////////////// - // Add Rows to the table // - //////////////////////////// - - //Append a group of rows () for each major category. - var majorGroups = tab - .selectAll('tbody') - .data(chart.data.major, function(d) { - return d.key; - }) - .enter() - .append('tbody') - .attr('class', 'minorHidden') - .attr('class', function(d) { - return 'major-' + d.key.replace(/[^A-Za-z0-9]/g, ''); - }); - - //Append a row summarizing all minor categories for each major category. - var majorRows = majorGroups - .selectAll('tr') - .data( - function(d) { - return d.values; - }, - function(datum) { - return datum.key; - } - ) - .enter() - .append('tr') - .attr('class', 'major') - .each(function(d) { - var thisRow = d3.select(this); - chart.util.fillRow(thisRow, chart, d); - }); - - //Append rows for each minor category. - var majorGroups = tab.selectAll('tbody').data(chart.data.minor, function(d) { - return d.key; - }); - - var minorRows = majorGroups - .selectAll('tr') - .data( - function(d) { - return d.values; - }, - function(datum) { - return datum.key; - } - ) - .enter() - .append('tr') - .attr('class', 'minor') - .each(function(d) { - var thisRow = d3.select(this); - chart.util.fillRow(thisRow, chart, d); - }); - //Add a footer for overall rates. - tab - .append('tfoot') - .selectAll('tr') - .data(chart.data.any.length > 0 ? chart.data.any[0].values : []) - .enter() - .append('tr') - .each(function(d) { - var thisRow = d3.select(this); - chart.util.fillRow(thisRow, chart, d); - }); - - //Remove unwanted elements from the footer. - tab.selectAll('tfoot svg').remove(); - tab.select('tfoot i').remove(); - tab.select('tfoot td.controls span').text(''); - - ////////////////////////////////////////////////// - // Initialize event listeners for summary Table // - ////////////////////////////////////////////////// - - // Show cell counts on Mouseover/Mouseout of difference diamonds - chart.wrap - .selectAll('td.diffplot svg g path.diamond') - .on('mouseover', function(d) { - var currentRow = chart.wrap.selectAll('.SummaryTable tbody tr').filter(function(e) { - return ( - e.values[0].values.major === d.major && e.values[0].values.minor === d.minor - ); - }); - - //Display CI; - d3.select(this.parentNode).select('.ci').classed('wc-hidden', false); - - //show cell counts for selected groups - showCellCounts(chart, currentRow, d.group1); - showCellCounts(chart, currentRow, d.group2); - }) - .on('mouseout', function(d) { - d3.select(this.parentNode).select('.ci').classed('wc-hidden', true); //hide CI - chart.wrap.selectAll('.annote').remove(); //Delete annotations. - }); - - // Highlight rows on mouseover - chart.wrap - .selectAll('.SummaryTable tr') - .on('mouseover', function(d) { - d3.select(this).select('td.rowLabel').classed('highlight', true); - }) - .on('mouseout', function(d) { - d3.select(this).select('td.rowLabel').classed('highlight', false); - }); - - //Expand/collapse a section - chart.wrap.selectAll('tr.major').selectAll('td.controls').on('click', function(d) { - var current = d3.select(this.parentNode.parentNode); - var toggle = !current.classed('minorHidden'); - current.classed('minorHidden', toggle); - - d3 - .select(this) - .select('span') - .attr('title', toggle ? 'Expand' : 'Collapse') - .text(function() { - return toggle ? '+' : '-'; - }); - }); - - // Render the details table - chart.wrap.selectAll('td.rowLabel').on('click', function(d) { - //Update classes (row visibility handeled via css) - var toggle = !chart.wrap.select('.SummaryTable table').classed('summary'); - chart.wrap.select('.SummaryTable table').classed('summary', toggle); - chart.wrap.select('div.controls').selectAll('div').classed('wc-hidden', toggle); - - //Create/remove the participant level table - if (toggle) { - var major = d.values[0].values['major']; - var minor = d.values[0].values['minor']; - var detailTableSettings = { - major: major, - minor: minor - }; - chart.detailTable.init(chart, detailTableSettings); - } else { - chart.wrap.select('.DetailTable').remove(); - chart.wrap.select('div.closeDetailTable').remove(); - } - }); - } - - /*------------------------------------------------------------------------------------------------\ - Apply basic filters and toggles. - \------------------------------------------------------------------------------------------------*/ - - function toggleRows(chart) { - //Toggle minor rows. - var minorToggle = !chart.config.defaults.prefTerms; - chart.wrap.selectAll('.SummaryTable tbody').classed('minorHidden', minorToggle); - chart.wrap - .selectAll('.SummaryTable table tbody') - .select('tr.major td.controls span') - .text(minorToggle ? '+' : '-'); - - //Toggle Difference plots - var differenceToggle = false; - chart.wrap.selectAll('.SummaryTable .diffplot').classed('wc-hidden', differenceToggle); - - //Filter based on prevalence. - var filterVal = chart.wrap.select('div.controls input.rateFilter').property('value'); - chart.wrap.selectAll('div.SummaryTable table tbody').each(function(d) { - var allRows = d3.select(this).selectAll('tr'); - var filterRows = allRows.filter(function(d) { - var percents = d.values - .filter(function(d) { - //only keep the total column if groupColumns are hidden (otherwise keep all columns) - return chart.config.defaults.groupCols ? true : d.key == 'Total'; - }) - .map(function(element) { - return element.values.per; - }); - var maxPercent = d3.max(percents); - - return maxPercent < filterVal; - }); - filterRows.classed('filter', 'true'); - - d3 - .select(this) - .select('tr.major td.controls span') - .classed('wc-hidden', filterRows[0].length + 1 >= allRows[0].length); - }); - } - - /*------------------------------------------------------------------------------------------------\ - Define AETable object (the meat and potatoes). - \------------------------------------------------------------------------------------------------*/ - - var AETable = { - redraw: redraw, - wipe: wipe, - init: init$7, - toggleRows: toggleRows - }; - - function makeDetailData(chart, detailTableSettings) { - //convenience mappings - var major = detailTableSettings.major; - var minor = detailTableSettings.minor; - var vars = chart.config.variables; - - //Filter the raw data given the select major and/or minor category. - var details = chart.population_event_data.filter(function(d) { - var majorMatch = major === 'All' ? true : major === d[vars['major']]; - var minorMatch = minor === 'All' ? true : minor === d[vars['minor']]; - return majorMatch && minorMatch; - }); - - if (vars.details.length === 0) - vars.details = Object.keys(chart.population_data[0]).filter(function(d) { - return ['data_all', 'placeholderFlag'].indexOf(d) === -1; - }); - - //Keep only those columns specified in settings.variables.details append - //If provided with a details object use that to determine chosen - //variables and headers - var detailVars = vars.details; - var details = details.map(function(d) { - var current = {}; - detailVars.forEach(function(currentVar) { - if (currentVar.value_col) { - // only true if a details object is provided - currentVar.label // if label is provided, write over column name with label - ? (current[currentVar.label] = d[currentVar.value_col]) - : (current[currentVar.value_col] = d[currentVar.value_col]); - } else { - current[currentVar] = d[currentVar]; - } - }); - return current; - }); - - return details; - } - - function toggleControls(chart) { - //Details about current population filters - var filtered = chart.raw_event_data.length != chart.population_event_data.length; - if (filtered) { - chart.wrap - .select('div.controls') - .select('div.custom-filters') - .classed('wc-hidden', false) - .selectAll('select') - .property('disabled', 'disabled'); - chart.detailTable.head - .append('span') - .html(filtered ? 'The listing is filtered as shown:' : ''); - } - } - - function makeTitle(chart, detailData, detailTableSettings) { - //Add explanatory listing title. - chart.detailTable.head - .append('h4') - .html( - detailTableSettings.minor === 'All' - ? 'Details for ' + - detailData.length + - ' ' + - detailTableSettings.major + - ' records' - : 'Details for ' + - detailData.length + - ' ' + - detailTableSettings.minor + - ' (' + - detailTableSettings.major + - ') records' - ); - } - - function layout$1(chart) { - chart.detailTable.wrap = chart.wrap - .select('div.table-wrapper') - .append('div') - .attr('class', 'DetailTable'); - - chart.detailTable.head = chart.wrap - .select('div.table-wrapper') - .insert('div', '.controls') - .attr('class', 'DetailHeader'); - - //Add button to return to standard view. - var closeButton = chart.detailTable.head - .append('button') - .attr('class', 'closeDetailTable btn btn-primary'); - - closeButton.html( - ' Return to the Summary View' - ); - - closeButton.on('click', function() { - chart.wrap.select('.SummaryTable table').classed('summary', false); - chart.wrap.select('div.controls').selectAll('div').classed('wc-hidden', false); - chart.wrap - .select('div.controls') - .select('div.custom-filters') - .selectAll('select') - .property('disabled', ''); - chart.wrap.selectAll('.SummaryTable table tbody tr').classed('wc-active', false); - if (chart.config.defaults.webchartsDetailTable) { - chart.detailTable.table.destroy(); - } - chart.detailTable.wrap.remove(); - chart.detailTable.head.remove(); - }); - } - - function draw(chart, data) { - chart.detailTable.table = webCharts.createTable( - //chart.config.container + ' .aeExplorer .aeTable .table-wrapper .DetailTable', - chart.detailTable.wrap.node(), - {} - ); - chart.detailTable.table.init(data); - } - - function draw$1(chart, data) { - //Generate listing container. - var canvas = chart.detailTable.wrap; - var listing = canvas.append('table').attr('class', 'table'); - - //Append header to listing container. - var headerRow = listing.append('thead').append('tr'); - headerRow.selectAll('th').data(d3.keys(data[0])).enter().append('th').html(function(d) { - return d; - }); - - //Add rows to listing container. - var tbody = listing.append('tbody'); - var rows = tbody.selectAll('tr').data(data).enter().append('tr'); - - //Add data cells to rows. - var cols = rows - .selectAll('tr') - .data(function(d) { - return d3.values(d); - }) - .enter() - .append('td') - .html(function(d) { - return d; - }); - } - - /*------------------------------------------------------------------------------------------------\ - Generate data listing. - \------------------------------------------------------------------------------------------------*/ - - function init$8(chart, detailTableSettings) { - var detailData = makeDetailData(chart, detailTableSettings); - layout$1(chart); - makeTitle(chart, detailData, detailTableSettings); - toggleControls(chart); - - //initialize and draw the chart either using webcharts or raw D3 - if (chart.config.defaults.webchartsDetailTable) { - draw(chart, detailData); - } else { - draw$1(chart, detailData); - } - } - - /*------------------------------------------------------------------------------------------------\ - Define detail table object. - \------------------------------------------------------------------------------------------------*/ - - var detailTable = { - init: init$8 - }; - - function createChart() { - var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'body'; - var config = arguments[1]; - - var chart = { - element: element, - config: config, - init: init, - colorScale: colorScale, - layout: layout, - controls: controls, - AETable: AETable, - detailTable: detailTable, - util: util - }; - - return chart; - } - - var index = { - createChart: createChart - }; - - return index; -}); diff --git a/inst/htmlwidgets/lib/hep-explorer-1.3.1/hepexplorer.js b/inst/htmlwidgets/lib/hep-explorer-1.3.1/hepexplorer.js deleted file mode 100644 index fd495d73..00000000 --- a/inst/htmlwidgets/lib/hep-explorer-1.3.1/hepexplorer.js +++ /dev/null @@ -1,5691 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' - ? (module.exports = factory(require('webcharts'))) - : typeof define === 'function' && define.amd - ? define(['webcharts'], factory) - : (global.hepexplorer = factory(global.webCharts)); -})(this, function (webcharts) { - 'use strict'; - - if (typeof Object.assign != 'function') { - Object.defineProperty(Object, 'assign', { - value: function assign(target, varArgs) { - if (target == null) { - // TypeError if undefined or null - throw new TypeError('Cannot convert undefined or null to object'); - } - - var to = Object(target); - - for (var index = 1; index < arguments.length; index++) { - var nextSource = arguments[index]; - - if (nextSource != null) { - // Skip over if undefined or null - for (var nextKey in nextSource) { - // Avoid bugs when hasOwnProperty is shadowed - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - - return to; - }, - writable: true, - configurable: true - }); - } - - if (!Array.prototype.find) { - Object.defineProperty(Array.prototype, 'find', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, 'length')). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return kValue. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return kValue; - } - // e. Increase k by 1. - k++; - } - - // 7. Return undefined. - return undefined; - } - }); - } - - if (!Array.prototype.findIndex) { - Object.defineProperty(Array.prototype, 'findIndex', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, "length")). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return k. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return k; - } - // e. Increase k by 1. - k++; - } - - // 7. Return -1. - return -1; - } - }); - } - - (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 () { - this.parentNode.appendChild(this); - }); - }; - - d3.selection.prototype.moveToBack = function () { - return this.each(function () { - var firstChild = this.parentNode.firstChild; - if (firstChild) { - this.parentNode.insertBefore(this, firstChild); - } - }); - }; - - var _typeof = - typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' - ? function (obj) { - return typeof obj; - } - : function (obj) { - return obj && - typeof Symbol === 'function' && - obj.constructor === Symbol && - obj !== Symbol.prototype - ? 'symbol' - : typeof obj; - }; - - var defineProperty = function (obj, key, value) { - if (key in obj) { - Object.defineProperty(obj, key, { - value: value, - enumerable: true, - configurable: true, - writable: true - }); - } else { - obj[key] = value; - } - - return obj; - }; - - /*------------------------------------------------------------------------------------------------\ - Clone a variable (http://stackoverflow.com/a/728694). - \------------------------------------------------------------------------------------------------*/ - - function clone(obj) { - var copy; - - //Handle the 3 simple types, and null or undefined - if (null == obj || 'object' != (typeof obj === 'undefined' ? 'undefined' : _typeof(obj))) - return obj; - - //Handle Date - if (obj instanceof Date) { - copy = new Date(); - copy.setTime(obj.getTime()); - return copy; - } - - //Handle Array - if (obj instanceof Array) { - copy = []; - for (var i = 0, len = obj.length; i < len; i++) { - copy[i] = clone(obj[i]); - } - return copy; - } - - //Handle 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 isn't supported."); - } - - function settings() { - return { - //LB domain settings - id_col: 'USUBJID', - studyday_col: 'DY', - value_col: 'STRESN', - measure_col: 'TEST', - normal_col_low: null, - normal_col_high: 'STNRHI', - visit_col: null, - visitn_col: null, - - //DM domain settings - group_cols: null, - filters: null, - details: null, - - //EX domain settings - exposure_stdy_col: 'EXSTDY', - exposure_endy_col: 'EXENDY', - exposure_trt_col: 'EXTRT', - exposure_dose_col: 'EXDOSE', - exposure_dosu_col: 'EXDOSU', - - //analysis settings - analysisFlag: { - value_col: null, - values: [] - }, - baseline: { - value_col: null, //synced with studyday_col in syncsettings() - values: [0] - }, - calculate_palt: false, - paltFlag: { - value_col: null, - values: [] - }, - measure_values: { - ALT: 'Aminotransferase, alanine (ALT)', - AST: 'Aminotransferase, aspartate (AST)', - TB: 'Total Bilirubin', - ALP: 'Alkaline phosphatase (ALP)' - }, - add_measures: false, - x_options: 'all', - x_default: 'ALT', - y_options: ['TB'], - y_default: 'TB', - point_size_options: 'all', - point_size_default: 'Uniform', - cuts: { - TB: { - relative_baseline: 4.8, - relative_uln: 2 - }, - ALP: { - relative_baseline: 3.8, - relative_uln: 1 - }, - defaults: { - relative_baseline: 3.8, - relative_uln: 3 - } - }, - imputation_methods: { - ALT: 'data-driven', - AST: 'data-driven', - TB: 'data-driven', - ALP: 'data-driven' - }, - imputation_values: null, - display: 'relative_uln', //or "relative_baseline" - plot_max_values: true, - plot_day: null, //set in onLayout/initStudyDayControl - display_options: [ - { - label: 'Upper limit of normal adjusted (eDish)', - value: 'relative_uln' - }, - { label: 'Baseline adjusted (mDish)', value: 'relative_baseline' } - ], - measureBounds: [0.01, 0.99], - populationProfileURL: null, - participantProfileURL: null, - r_ratio_filter: true, - r_ratio: [0, null], - visit_window: 30, - title: 'Hepatic Safety Explorer', - downloadLink: true, - filters_multiselect: true, - warningText: - "This graphic has been thoroughly tested, but is not validated. Any clinical recommendations based on this tool should be confirmed using your organization's standard operating procedures.", - - //all values set in onLayout/quadrants/*.js - quadrants: [ - { - label: "Possible Hy's Law Range", - position: 'upper-right', - dataValue: 'xHigh:yHigh', - count: null, - total: null, - percent: null - }, - { - label: 'Hyperbilirubinemia', - position: 'upper-left', - dataValue: 'xNormal:yHigh', - count: null, - total: null, - percent: null - }, - { - label: "Temple's Corollary", - position: 'lower-right', - dataValue: 'xHigh:yNormal', - count: null, - total: null, - percent: null - }, - { - label: 'Normal Range', - position: 'lower-left', - dataValue: 'xNormal:yNormal', - count: null, - total: null, - percent: null - } - ], - - //Standard webcharts settings - x: { - column: null, //set in onPreprocess/updateAxisSettings - label: null, // set in onPreprocess/updateAxisSettings, - type: 'linear', - behavior: 'raw', - format: '.2f' - //domain: [0, null] - }, - y: { - column: null, // set in onPreprocess/updateAxisSettings, - label: null, // set in onPreprocess/updateAxisSettings, - type: 'linear', - behavior: 'raw', - format: '.2f' - //domain: [0, null] - }, - marks: [ - { - per: [], // set in syncSettings() - type: 'circle', - summarizeY: 'mean', - summarizeX: 'mean', - attributes: { 'fill-opacity': 0 } - } - ], - gridlines: 'xy', - color_by: null, //set in syncSettings - max_width: 600, - aspect: 1, - legend: { location: 'top' }, - margin: { right: 25, top: 25, bottom: 75 } - }; - } - - //Replicate settings in multiple places in the settings object - function syncSettings(settings$$1) { - var defaults = settings(); - settings$$1.marks[0].per[0] = settings$$1.id_col; - - //set grouping config - if (typeof settings$$1.group_cols == 'string') { - settings$$1.group_cols = [ - { value_col: settings$$1.group_cols, label: settings$$1.group_cols } - ]; - } - - if (!(settings$$1.group_cols instanceof Array && settings$$1.group_cols.length)) { - settings$$1.group_cols = [{ value_col: 'NONE', label: 'None' }]; - } else { - settings$$1.group_cols = settings$$1.group_cols.map(function (group) { - return { - value_col: group.value_col || group, - label: group.label || group.value_col || group - }; - }); - - var hasNone = - settings$$1.group_cols - .map(function (m) { - return m.value_col; - }) - .indexOf('NONE') > -1; - if (!hasNone) { - settings$$1.group_cols.unshift({ value_col: 'NONE', label: 'None' }); - } - } - - if (settings$$1.group_cols.length > 1) { - settings$$1.color_by = settings$$1.group_cols[1].value_col - ? settings$$1.group_cols[1].value_col - : settings$$1.group_cols[1]; - } else { - settings$$1.color_by = 'NONE'; - } - - //make sure filters is an Array - if (!(settings$$1.filters instanceof Array)) { - settings$$1.filters = - typeof settings$$1.filters == 'string' ? [settings$$1.filters] : []; - } - - //Define default details. - var defaultDetails = [{ value_col: settings$$1.id_col, label: 'Subject Identifier' }]; - if (settings$$1.filters) { - settings$$1.filters.forEach(function (filter) { - var obj = { - value_col: filter.value_col ? filter.value_col : filter, - label: filter.label - ? filter.label - : filter.value_col - ? filter.value_col - : filter - }; - - if ( - defaultDetails.find(function (f) { - return f.value_col == obj.value_col; - }) == undefined - ) { - defaultDetails.push(obj); - } - }); - } - - if (settings$$1.group_cols) { - settings$$1.group_cols - .filter(function (f) { - return f.value_col != 'NONE'; - }) - .forEach(function (group) { - var obj = { - value_col: group.value_col ? group.value_col : filter, - label: group.label - ? group.label - : group.value_col - ? group.value_col - : filter - }; - if ( - defaultDetails.find(function (f) { - return f.value_col == obj.value_col; - }) == undefined - ) { - defaultDetails.push(obj); - } - }); - } - - //parse details to array if needed - if (!(settings$$1.details instanceof Array)) { - settings$$1.details = - typeof settings$$1.details == 'string' ? [settings$$1.details] : []; - } - - //If [settings.details] is not specified: - if (!settings$$1.details) settings$$1.details = defaultDetails; - else { - //If [settings.details] is specified: - //Allow user to specify an array of columns or an array of objects with a column property - //and optionally a column label. - settings$$1.details.forEach(function (detail) { - if ( - defaultDetails - .map(function (d) { - return d.value_col; - }) - .indexOf(detail.value_col ? detail.value_col : detail) === -1 - ) - defaultDetails.push({ - value_col: detail.value_col ? detail.value_col : detail, - label: detail.label - ? detail.label - : detail.value_col - ? detail.value_col - : detail - }); - }); - settings$$1.details = defaultDetails; - } - - // If settings.analysisFlag is null - if (!settings$$1.analysisFlag) settings$$1.analysisFlag = { value_col: null, values: [] }; - if (!settings$$1.analysisFlag.value_col) settings$$1.analysisFlag.value_col = null; - if (!(settings$$1.analysisFlag.values instanceof Array)) { - settings$$1.analysisFlag.values = - typeof settings$$1.analysisFlag.values == 'string' - ? [settings$$1.analysisFlag.values] - : []; - } - - // If settings.paltFlag is null - if (!settings$$1.paltFlag) settings$$1.paltFlag = { value_col: null, values: [] }; - if (!settings$$1.paltFlag.value_col) settings$$1.paltFlag.value_col = null; - if (!(settings$$1.paltFlag.values instanceof Array)) { - settings$$1.paltFlag.values = - typeof settings$$1.paltFlag.values == 'string' ? [settings$$1.paltFlag.values] : []; - } - - //if it is null, set settings.baseline.value_col to settings.studyday_col. - if (!settings$$1.baseline) settings$$1.baseline = { value_col: null, values: [] }; - if (!settings$$1.baseline.value_col) - settings$$1.baseline.value_col = settings$$1.studyday_col; - if (!(settings$$1.baseline.values instanceof Array)) { - settings$$1.baseline.values = - typeof settings$$1.baseline.values == 'string' ? [settings$$1.baseline.values] : []; - } - - //merge in default measure_values if user hasn't specified changes - Object.keys(defaults.measure_values).forEach(function (val) { - if (!settings$$1.measure_values.hasOwnProperty(val)) - settings$$1.measure_values[val] = defaults.measure_values[val]; - }); - - //check for 'all' in x_, y_ and point_size_options, but keep track if all options are used for later - var allMeasures = Object.keys(settings$$1.measure_values); - settings$$1.x_options_all = settings$$1.x_options == 'all'; - if (settings$$1.x_options == 'all') settings$$1.x_options = allMeasures; - settings$$1.y_options_all = settings$$1.y_options == 'all'; - if (settings$$1.y_options == 'all') settings$$1.y_options = allMeasures; - settings$$1.point_size_options_all = settings$$1.point_size_options == 'all'; - if (settings$$1.point_size_options == 'all') settings$$1.point_size_options = allMeasures; - - //parse x_ and y_options to array if needed - if (!(settings$$1.x_options instanceof Array)) { - settings$$1.x_options = - typeof settings$$1.x_options == 'string' ? [settings$$1.x_options] : []; - } - - if (!(settings$$1.y_options instanceof Array)) { - settings$$1.y_options = - typeof settings$$1.y_options == 'string' ? [settings$$1.y_options] : []; - } - - //set starting values for axis and point size settings. - settings$$1.point_size = - settings$$1.point_size_options.indexOf(settings$$1.point_size_default) > -1 - ? settings$$1.point_size_default - : settings$$1.point_size_default == 'rRatio' - ? 'rRatio' - : 'Uniform'; - settings$$1.x.column = - settings$$1.x_options.indexOf(settings$$1.x_default) > -1 - ? settings$$1.x_default - : settings$$1.x_options[0]; - settings$$1.y.column = - settings$$1.y_options.indexOf(settings$$1.y_default) > -1 - ? settings$$1.y_default - : settings$$1.y_options[0]; - - // track initial Cutpoint (lets us detect when cutpoint should change) - settings$$1.cuts.x = settings$$1.x.column; - settings$$1.cuts.y = settings$$1.y.column; - settings$$1.cuts.display = settings$$1.display; - - // Confirm detault cuts are set - settings$$1.cuts.defaults = settings$$1.cuts.defaults || defaults.cuts.defaults; - settings$$1.cuts.defaults.relative_uln = - settings$$1.cuts.defaults.relative_uln || defaults.cuts.defaults.relative_uln; - settings$$1.cuts.defaults.relative_baseline = - settings$$1.cuts.defaults.relative_baseline || defaults.cuts.defaults.relative_baseline; - - // keep default cuts if user hasn't provided an alternative - var cutMeasures = Object.keys(settings$$1.cuts); - Object.keys(defaults.cuts).forEach(function (m) { - if (cutMeasures.indexOf(m) == -1) { - settings$$1.cuts[m] = defaults.cuts[m]; - } - }); - - return settings$$1; - } - - function controlInputs() { - return [ - { - type: 'number', - label: 'R Ratio Range', - description: 'Filter points based on R ratio [(ALT/ULN) / (ALP/ULN)]', - option: 'r_ratio[0]' - }, - { - type: 'number', - label: null, //combined with r_ratio[0] control in formatRRatioControl() - description: null, - option: 'r_ratio[1]' - }, - { - type: 'dropdown', - label: 'Group', - description: 'Grouping variable', - options: ['color_by'], - start: null, // set in syncControlInputs() - values: ['NONE'], // set in syncControlInputs() - require: true - }, - { - type: 'dropdown', - label: 'Display Type', - description: 'Relative or absolute axes', - options: ['displayLabel'], - start: null, // set in syncControlInputs() - values: null, // set in syncControlInputs() - require: true - }, - { - type: 'radio', - option: 'plot_max_values', - label: 'Plot Style', - values: [true, false], - relabels: ['Max Values', 'By Study Day'] - }, - { - type: 'number', - label: 'Study Day', - description: null, //set in onDraw/updateStudyDaySlider - option: 'plot_day' - }, - { - type: 'dropdown', - label: 'X-axis Measure', - description: null, // set in syncControlInputs() - option: 'x.column', - start: null, // set in syncControlInputs() - values: null, //set in syncControlInptus() - require: true - }, - { - type: 'number', - label: null, // set in syncControlInputs - description: 'X-axis Reference Line', - option: null // set in syncControlInputs - }, - { - type: 'dropdown', - label: 'Y-axis Measure', - description: null, // set in syncControlInputs() - option: 'y.column', - start: null, // set in syncControlInputs() - values: null, //set in syncControlInptus() - require: true - }, - { - type: 'number', - label: null, // set in syncControlInputs - description: 'Y-axis Reference Line', - option: null // set in syncControlInputs - }, - { - type: 'dropdown', - label: 'Point Size', - description: 'Parameter to set point radius', - options: ['point_size'], - start: null, // set in syncControlInputs() - values: ['Uniform', 'rRatio'], - require: true - }, - { - type: 'dropdown', - label: 'Axis Type', - description: 'Linear or Log Axes', - options: ['x.type', 'y.type'], - start: null, // set in syncControlInputs() - values: ['linear', 'log'], - require: true - }, - { - type: 'number', - label: 'Highlight Points Based on Timing', - description: 'Fill points with max values less than X days apart', - option: 'visit_window' - } - ]; - } - - //Map values from settings to control inputs - function syncControlInputs(controlInputs, settings) { - //////////////////////// - // Group control - /////////////////////// - - var groupControl = controlInputs.find(function (controlInput) { - return controlInput.label === 'Group'; - }); - - //sync start value - groupControl.start = settings.color_by; //sync start value - - //sync values - settings.group_cols - .filter(function (group) { - return group.value_col !== 'NONE'; - }) - .forEach(function (group) { - groupControl.values.push(group.value_col); - }); - - //drop the group control if NONE is the only option - if (settings.group_cols.length == 1) - controlInputs = controlInputs.filter(function (controlInput) { - return controlInput.label != 'Group'; - }); - - ////////////////////////// - // x-axis measure control - ////////////////////////// - - // drop the control if there's only one option - if (settings.x_options.length === 1) - controlInputs = controlInputs.filter(function (controlInput) { - return controlInput.option !== 'x.column'; - }); - else { - //otherwise sync the properties - var xAxisMeasureControl = controlInputs.find(function (controlInput) { - return controlInput.option === 'x.column'; - }); - - //xAxisMeasureControl.description = settings.x_options.join(', '); - xAxisMeasureControl.start = settings.x.column; - xAxisMeasureControl.values = settings.x_options; - } - - ////////////////////////////////// - // x-axis reference line control - ////////////////////////////////// - - var xRefControl = controlInputs.find(function (controlInput) { - return controlInput.description === 'X-axis Reference Line'; - }); - xRefControl.label = settings.x_options[0] + ' Cutpoint'; - xRefControl.option = 'settings.cuts.' + [settings.x.column] + '.' + [settings.display]; - - //////////////////////////// - // y-axis measure control - //////////////////////////// - - // drop the control if there's only one option - if (settings.y_options.length === 1) - controlInputs = controlInputs.filter(function (controlInput) { - return controlInput.option !== 'y.column'; - }); - else { - //otherwise sync the properties - var yAxisMeasureControl = controlInputs.find(function (controlInput) { - return controlInput.option === 'y.column'; - }); - // yAxisMeasureControl.description = settings.y_options.join(', '); - yAxisMeasureControl.start = settings.y.column; - yAxisMeasureControl.values = settings.y_options; - } - - ////////////////////////////////// - // y-axis reference line control - ////////////////////////////////// - - var yRefControl = controlInputs.find(function (controlInput) { - return controlInput.description === 'Y-axis Reference Line'; - }); - yRefControl.label = settings.y_options[0] + ' Cutpoint'; - - yRefControl.option = 'settings.cuts.' + [settings.y.column] + '.' + [settings.display]; - - ////////////////////////////////// - // R ratio filter control - ////////////////////////////////// - - //drop the R Ratio control if r_ratio_filter is false - if (!settings.r_ratio_filter) { - controlInputs = controlInputs.filter(function (controlInput) { - return ['r_ratio[0]', 'r_ratio[1]'].indexOf(controlInput.option) == -1; - }); - } - - ////////////////////////////////// - // Point size control - ////////////////////////////////// - - var pointSizeControl = controlInputs.find(function (ci) { - return ci.label === 'Point Size'; - }); - - pointSizeControl.start = settings.point_size; - - settings.point_size_options.forEach(function (d) { - pointSizeControl.values.push(d); - }); - - //drop the pointSize control if NONE is the only option - if (settings.point_size_options.length == 0) - controlInputs = controlInputs.filter(function (controlInput) { - return controlInput.label != 'Point Size'; - }); - - ////////////////////////////////// - // Display control - ////////////////////////////////// - - controlInputs.find(function (controlInput) { - return controlInput.label === 'Display Type'; - }).values = settings.display_options.map(function (m) { - return m.label; - }); - - ////////////////////////////////// - // Add filters to inputs - ////////////////////////////////// - if (settings.filters && settings.filters.length > 0) { - var otherFilters = settings.filters.map(function (filter) { - filter = { - type: 'subsetter', - value_col: filter.value_col ? filter.value_col : filter, - label: filter.label - ? filter.label - : filter.value_col - ? filter.value_col - : filter, - multiple: settings.filters_multiselect - }; - return filter; - }); - return d3.merge([otherFilters, controlInputs]); - } else return controlInputs; - } - - var configuration = { - settings: settings, - syncSettings: syncSettings, - controlInputs: controlInputs, - syncControlInputs: syncControlInputs - }; - - function checkMeasureDetails() { - var chart = this; - var config = this.config; - var measures = d3 - .set( - this.raw_data.map(function (d) { - return d[config.measure_col]; - }) - ) - .values() - .sort(); - var specifiedMeasures = Object.keys(config.measure_values).map(function (e) { - return config.measure_values[e]; - }); - var missingMeasures = []; - Object.keys(config.measure_values).forEach(function (d) { - if (measures.indexOf(config.measure_values[d]) == -1) { - missingMeasures.push(config.measure_values[d]); - delete config.measure_values[d]; - } - }); - var nMeasuresRemoved = missingMeasures.length; - if (nMeasuresRemoved > 0) - console.warn( - 'The data are missing ' + - (nMeasuresRemoved === 1 ? 'this measure' : 'these measures') + - ': ' + - missingMeasures.join(', ') + - '.' - ); - - //automatically add Measures if requested - if (config.add_measures) { - measures.forEach(function (m, i) { - if (specifiedMeasures.indexOf(m) == -1) { - config.measure_values['m' + i] = m; - } - }); - } - - //check that x_options, y_options and size_options all have value keys/values in measure_values - var valid_options = Object.keys(config.measure_values); - var all_settings = ['x_options', 'y_options', 'point_size_options']; - all_settings.forEach(function (setting) { - // remove invalid options - config[setting].forEach(function (option) { - if (valid_options.indexOf(option) == -1) { - delete config[options][option]; - console.warn( - option + - " wasn't found in the measure_values index and has been removed from config." + - setting + - '. This may cause problems with the chart.' - ); - } - }); - - // update the control input settings - var controlLabel = - setting == 'x_options' - ? 'X-axis Measure' - : setting == 'y_options' - ? 'Y-axis Measure' - : 'Point Size'; - var input = chart.controls.config.inputs.find(function (ci) { - return ci.label == controlLabel; - }); - - if (input) { - //only update this if the input settings exist - axis inputs with only one value are deleted - // add options for controls requesting 'all' measures - if (config[setting + '_all']) { - var point_size_options = d3.merge([['Uniform', 'rRatio'], valid_options]); - config[setting] = - setting == 'point_size_options' ? point_size_options : valid_options; - input.values = config[setting]; - } - } - }); - //check that all measure_values have associated cuts - Object.keys(config.measure_values).forEach(function (m) { - // does a cut point for the measure exist? If not, create a placeholder. - if (!config.cuts.hasOwnProperty(m)) { - config.cuts[m] = {}; - } - - // does the cut have non-null baseline and ULN cuts associated, if not use the default values - config.cuts[m].relative_baseline = - config.cuts[m].relative_baseline || config.cuts.defaults.relative_baseline; - config.cuts[m].relative_uln = - config.cuts[m].relative_uln || config.cuts.defaults.relative_uln; - }); - } - - function iterateOverData() { - var _this = this; - - this.raw_data.forEach(function (d) { - d[_this.config.x.column] = null; // placeholder variable for x-axis - d[_this.config.y.column] = null; // placeholder variable for y-axis - d.NONE = 'All Participants'; // placeholder variable for non-grouped comparisons - - //Remove space characters from result variable. - if (typeof d[_this.config.value_col] == 'string') - d[_this.config.value_col] = d[_this.config.value_col].replace(/\s/g, ''); // remove space characters - }); - } - - function addRRatioFilter() { - if (this.config.r_ratio_filter) { - this.filters.push({ - col: 'rRatioFlag', - val: 'Y', - choices: ['Y', 'N'], - loose: undefined - }); - } - } - - function imputeColumn(data, measure_column, value_column, measure, llod, imputed_value, drop) { - //returns a data set with imputed values (or drops records) for records at or below a lower threshold for a given measure - //data = the data set for imputation - //measure_column = the column with the text measure names - //value_column = the column with the numeric values to be changed via imputation - //measure = the measure to be imputed - //llod = the lower limit of detection - values at or below the llod are imputed - //imputed_value = value for imputed records - //drop = boolean flag indicating whether values at or below the llod should be dropped (default = false) - - if (drop == undefined) drop = false; - if (drop) { - return data.filter(function (f) { - dropFlag = d[measure_column] == measure && +d[value_column] <= 0; - return !dropFlag; - }); - } else { - data.forEach(function (d) { - if ( - d[measure_column] == measure && - +d[value_column] < +llod && - d[value_column] >= 0 - ) { - d.impute_flag = true; - d[value_column + '_original'] = d[value_column]; - d[value_column] = imputed_value; - } - }); - - var impute_count = d3.sum( - data.filter(function (d) { - return d[measure_column] === measure; - }), - function (f) { - return f.impute_flag; - } - ); - - if (impute_count > 0) - console.warn( - '' + - impute_count + - ' value(s) less than ' + - llod + - ' were imputed to ' + - imputed_value + - ' for ' + - measure - ); - return data; - } - } - - function imputeData() { - var chart = this; - var config = this.config; - - Object.keys(config.measure_values).forEach(function (measureKey) { - var values = chart.imputed_data - .filter(function (f) { - return f[config.measure_col] == config.measure_values[measureKey]; - }) - .map(function (m) { - return +m[config.value_col]; - }) - .sort(function (a, b) { - return a - b; - }), - minValue = d3.min( - values.filter(function (f) { - return f > 0; - }) - ), - //minimum value > 0 - llod = null, - imputed_value = null, - drop = null; - - if (config.imputation_methods[measureKey] == 'data-driven') { - llod = minValue; - imputed_value = minValue / 2; - drop = false; - } else if (config.imputation_methods[measureKey] == 'user-defined') { - llod = config.imputation_values[measureKey]; - imputed_value = config.imputation_values[measureKey] / 2; - drop = false; - } else if (config.imputation_methods[measureKey] == 'drop') { - llod = null; - imputed_value = null; - drop = true; - } - chart.imputed_data = imputeColumn( - chart.imputed_data, - config.measure_col, - config.value_col, - config.measure_values[measureKey], - llod, - imputed_value, - drop - ); - - var total_imputed = d3.sum(chart.raw_data, function (f) { - return f.impute_flag ? 1 : 0; - }); - }); - } - - function dropRows() { - var chart = this; - var config = this.config; - this.dropped_rows = []; - - ///////////////////////// - // Remove invalid rows - ///////////////////////// - var numerics = ['value_col', 'studyday_col', 'normal_col_high']; - chart.imputed_data = chart.initial_data.filter(function (f) { - return true; - }); - numerics.forEach(function (setting) { - chart.imputed_data = chart.imputed_data.filter(function (d) { - //Remove non-numeric value_col - var numericCol = /^-?(\d*\.?\d+|\d+\.?\d*)(E-?\d+)?$/.test(d[config[setting]]); - if (!numericCol) { - d.dropReason = setting + ' Column ("' + config[setting] + '") is not numeric.'; - chart.dropped_rows.push(d); - } - return numericCol; - }); - }); - } - - function deriveVariables() { - var config = this.config; - - //filter the lab data to only the required measures - var included_measures = Object.keys(config.measure_values).map(function (e) { - return config.measure_values[e]; - }); - - var sub = this.imputed_data.filter(function (f) { - return included_measures.indexOf(f[config.measure_col]) > -1; - }); - - var missingBaseline = 0; - - //coerce numeric values to number - this.imputed_data = this.imputed_data.map(function (d) { - var numerics = ['value_col', 'studyday_col', 'normal_col_low', 'normal_col_high']; - numerics.forEach(function (col) { - d[config[col]] = parseFloat(d[config[col]]); - }); - return d; - }); - - //create an object mapping baseline values for id/measure pairs - var baseline_records = sub.filter(function (f) { - var current = - typeof f[config.baseline.value_col] == 'string' - ? f[config.baseline.value_col].trim() - : parseFloat(f[config.baseline.value_col]); - return config.baseline.values.indexOf(current) > -1; - }); - - var baseline_values = d3 - .nest() - .key(function (d) { - return d[config.id_col]; - }) - .key(function (d) { - return d[config.measure_col]; - }) - .rollup(function (d) { - return d[0][config.value_col]; - }) - .map(baseline_records); - - this.imputed_data = this.imputed_data.map(function (d) { - //standardize key variables - d.key_measure = false; - if (included_measures.indexOf(d[config.measure_col]) > -1) { - d.key_measure = true; - - //map the raw value to a variable called 'absolute' - d.absolute = d[config.value_col]; - - //get the value relative to the ULN (% of the upper limit of normal) for the measure - d.uln = d[config.normal_col_high]; - d.relative_uln = d[config.value_col] / d[config.normal_col_high]; - - //get value relative to baseline - if (baseline_values[d[config.id_col]]) { - if (baseline_values[d[config.id_col]][d[config.measure_col]]) { - d.baseline_absolute = - baseline_values[d[config.id_col]][d[config.measure_col]]; - d.relative_baseline = d.absolute / d.baseline_absolute; - } else { - missingBaseline = missingBaseline + 1; - d.baseline_absolute = null; - d.relative_baseline = null; - } - } else { - missingBaseline = missingBaseline + 1; - d.baseline_absolute = null; - d.relative_baseline = null; - } - } - return d; - }); - - if (config.debug & (missingBaseline > 0)) - console.warn( - 'No baseline value found for ' + missingBaseline + ' of ' + sub.length + ' records.' - ); - } - - function makeAnalysisFlag() { - var config = this.config; - this.imputed_data = this.imputed_data.map(function (d) { - var hasAnalysisSetting = - config.analysisFlag.value_col != null && config.analysisFlag.values.length > 0; - d.analysisFlag = hasAnalysisSetting - ? config.analysisFlag.values.indexOf(d[config.analysisFlag.value_col]) > -1 - : true; - return d; - }); - } - - function makePaltFlag() { - var config = this.config; - this.imputed_data = this.imputed_data.map(function (d) { - var hasPaltSetting = - config.paltFlag.value_col != null && config.paltFlag.values.length > 0; - d.paltFlag = hasPaltSetting - ? config.paltFlag.values.indexOf(d[config.paltFlag.value_col]) > -1 - : true; - return d; - }); - } - - function cleanData() { - var config = this.config; - - //drop rows with invalid data - this.imputedData = dropRows.call(this); - - this.imputed_data.forEach(function (d) { - d.impute_flag = false; - }); - - imputeData.call(this); - deriveVariables.call(this); - makeAnalysisFlag.call(this); - makePaltFlag.call(this); - } - - function initCustomEvents() { - var chart = this; - chart.participantsSelected = []; - chart.events.participantsSelected = new CustomEvent('participantsSelected'); - } - - function onInit() { - checkMeasureDetails.call(this); - iterateOverData.call(this); - addRRatioFilter.call(this); - cleanData.call(this); //clean visit-level data - imputation and variable derivations - initCustomEvents.call(this); - } - - function formatRRatioControl() { - var chart = this; - var config = this.config; - if (this.config.r_ratio_filter) { - var min_r_ratio = this.controls.wrap.selectAll('.control-group').filter(function (d) { - return d.option === 'r_ratio[0]'; - }); - var min_r_ratio_input = min_r_ratio.select('input'); - - var max_r_ratio = this.controls.wrap.selectAll('.control-group').filter(function (d) { - return d.option === 'r_ratio[1]'; - }); - var max_r_ratio_input = max_r_ratio.select('input'); - - min_r_ratio_input.attr('id', 'r_ratio_min'); - max_r_ratio_input.attr('id', 'r_ratio_max'); - - //move the max r ratio control next to the min control - min_r_ratio.append('span').text(' - '); - min_r_ratio.append(function () { - return max_r_ratio_input.node(); - }); - - max_r_ratio.remove(); - - //add a reset button - min_r_ratio - .append('button') - .style('padding', '0.2em 0.5em 0.2em 0.4em') - .style('margin-left', '0.5em') - .style('border-radius', '0.4em') - .text('Reset') - .on('click', function () { - config.r_ratio[0] = 0; - min_r_ratio.select('input#r_ratio_min').property('value', config.r_ratio[0]); - config.r_ratio[1] = config.max_r_ratio; - min_r_ratio.select('input#r_ratio_max').property('value', config.r_ratio[1]); - chart.draw(); - }); - } - } - - function updateSummaryTable() { - var chart = this; - var config = chart.config; - var quadrants = this.config.quadrants; - var rows = quadrants.table.rows; - var cells = quadrants.table.cells; - - function updateCells(d) { - var cellData = cells.map(function (cell) { - cell.value = d[cell.value_col]; - return cell; - }); - var row_cells = d3 - .select(this) - .selectAll('td') - .data(cellData, function (d) { - return d.value_col; - }); - - row_cells - .enter() - .append('td') - .style('text-align', function (d, i) { - return d.label != 'Quadrant' ? 'center' : null; - }) - .style('font-size', '0.9em') - .style('padding', '0 0.5em 0 0.5em'); - - row_cells.html(function (d) { - return d.value; - }); - } - - //make sure the table is visible - this.config.quadrants.table.wrap.style('display', null); - //update the content of each row - rows.data(quadrants, function (d) { - return d.label; - }); - rows.each(updateCells); - } - - function initSummaryTable() { - var chart = this; - var config = chart.config; - var quadrants = this.config.quadrants; - - quadrants.table = {}; - quadrants.table.wrap = this.wrap - .append('div') - .attr('class', 'quadrantTable') - .style('padding-top', '1em'); - quadrants.table.tab = quadrants.table.wrap - .append('table') - .style('border-collapse', 'collapse'); - - //table header - quadrants.table.cells = [ - { - value_col: 'label', - label: 'Quadrant' - }, - { - value_col: 'count', - label: '#' - }, - { - value_col: 'percent', - label: '%' - } - ]; - - if (config.populationProfileURL) { - quadrants.forEach(function (d) { - d.link = "🔗"; - }); - quadrants.table.cells.push({ - value_col: 'link', - label: 'Population Profile' - }); - } - quadrants.table.thead = quadrants.table.tab - .append('thead') - .style('border-top', '2px solid #999') - .style('border-bottom', '2px solid #999') - .append('tr') - .style('padding', '0.1em'); - - quadrants.table.thead - .selectAll('th') - .data(quadrants.table.cells) - .enter() - .append('th') - .html(function (d) { - return d.label; - }); - - //table contents - quadrants.table.tbody = quadrants.table.tab - .append('tbody') - .style('border-bottom', '2px solid #999'); - quadrants.table.rows = quadrants.table.tbody - .selectAll('tr') - .data(quadrants, function (d) { - return d.label; - }) - .enter() - .append('tr') - .style('padding', '0.1em'); - - //initial table update - updateSummaryTable.call(this); - } - - function init() { - var chart = this; - var config = chart.config; - var quadrants = this.config.quadrants; - - var x_input = chart.controls.wrap - .selectAll('div.control-group') - .filter(function (f) { - return f.description == 'X-axis Reference Line'; - }) - .select('input'); - - var y_input = chart.controls.wrap - .selectAll('div.control-group') - .filter(function (f) { - return f.description == 'Y-axis Reference Line'; - }) - .select('input'); - - /////////////////////////////////////////////////////////// - // set initial values - ////////////////////////////////////////////////////////// - x_input.node().value = config.cuts[config.x.column][config.display]; - y_input.node().value = config.cuts[config.y.column][config.display]; - - /////////////////////////////////////////////////////////// - // set control step to 0.1 - ////////////////////////////////////////////////////////// - x_input.attr('step', 0.1); - y_input.attr('step', 0.1); - - /////////////////////////////////////////////////////////// - // initialize the summary table - ////////////////////////////////////////////////////////// - initSummaryTable.call(chart); - } - - function layoutQuadrantLabels() { - var chart = this; - var config = chart.config; - var quadrants = this.config.quadrants; - - ////////////////////////////////////////////////////////// - //layout the quadrant labels - ///////////////////////////////////////////////////////// - - chart.quadrant_labels = {}; - chart.quadrant_labels.g = this.svg.append('g').attr('class', 'quadrant-labels'); - - chart.quadrant_labels.text = chart.quadrant_labels.g - .selectAll('text.quadrant-label') - .data(quadrants) - .enter() - .append('text') - .attr('class', function (d) { - return 'quadrant-label ' + d.position; - }) - .attr('dy', function (d) { - return d.position.search('lower') > -1 ? '-.2em' : '.5em'; - }) - .attr('dx', function (d) { - return d.position.search('right') > -1 ? '-.5em' : '.5em'; - }) - .attr('text-anchor', function (d) { - return d.position.search('right') > 0 ? 'end' : null; - }) - .attr('fill', '#bbb') - .text(function (d) { - return d.label; - }); - } - - function layoutCutLines() { - var chart = this; - var config = chart.config; - var quadrants = this.config.quadrants; - - ////////////////////////////////////////////////////////// - //layout the cut lines - ///////////////////////////////////////////////////////// - chart.cut_lines = {}; - chart.cut_lines.wrap = this.svg.append('g').attr('class', 'cut-lines'); - var wrap = chart.cut_lines.wrap; - - //slight hack to make life easier on drag - var cutLineData = [{ dimension: 'x' }, { dimension: 'y' }]; - - cutLineData.forEach(function (d) { - d.chart = chart; - }); - - chart.cut_lines.g = wrap - .selectAll('g.cut') - .data(cutLineData) - .enter() - .append('g') - .attr('class', function (d) { - return 'cut ' + d.dimension; - }); - - chart.cut_lines.lines = chart.cut_lines.g - .append('line') - .attr('class', 'cut-line') - .attr('stroke-dasharray', '5,5') - .attr('stroke', '#bbb'); - - chart.cut_lines.backing = chart.cut_lines.g - .append('line') - .attr('class', 'cut-line-backing') - .attr('stroke', 'transparent') - .attr('stroke-width', '10') - .attr('cursor', 'move'); - } - - function initQuadrants() { - init.call(this); - layoutCutLines.call(this); - layoutQuadrantLabels.call(this); - } - - function initRugs() { - //initialize a 'rug' on each axis to show the distribution for a participant on addPointMouseover - this.x_rug = this.svg.append('g').attr('class', 'rug x'); - this.y_rug = this.svg.append('g').attr('class', 'rug y'); - } - - function initVisitPath() { - //initialize a 'rug' on each axis to show the distribution for a participant on addPointMouseover - this.visitPath = this.svg.append('g').attr('class', 'visit-path'); - } - - function initParticipantDetails() { - //layout participant details section - this.participantDetails = {}; - this.participantDetails.wrap = this.wrap.append('div').attr('class', 'participantDetails'); - - this.participantDetails.header = this.participantDetails.wrap - .append('div') - .attr('class', 'participantHeader'); - - //layout spaghettiPlot - var splot = this.participantDetails.wrap.append('div').attr('class', 'spaghettiPlot'); - splot - .append('h3') - .attr('class', 'id') - .html('Standardized Lab Values by Study Day') - .style('border-top', '2px solid black') - .style('border-bottom', '2px solid black') - .style('padding', '.2em'); - - splot.append('div').attr('class', 'chart'); - - //layout rRatio plot - var rrplot = this.participantDetails.wrap.append('div').attr('class', 'rrPlot'); - rrplot - .append('h3') - .attr('class', 'id') - .html('R Ratio by Study Day') - .style('border-top', '2px solid black') - .style('border-bottom', '2px solid black') - .style('padding', '.2em'); - - rrplot.append('div').attr('class', 'chart'); - - //layout measure table - var mtable = this.participantDetails.wrap.append('div').attr('class', 'measureTable'); - mtable - .append('h3') - .attr('class', 'id') - .html('Raw Lab Values Summary Table') - .style('border-top', '2px solid black') - .style('border-bottom', '2px solid black') - .style('padding', '.2em'); - - //initialize the measureTable - var settings = { - cols: ['key', 'n', 'min', 'median', 'max', 'spark'], - headers: ['Measure', 'N', 'Min', 'Median', 'Max', ''], - searchable: false, - sortable: false, - pagination: false, - exportable: false, - applyCSS: true - }; - this.measureTable = webcharts.createTable( - this.element + ' .participantDetails .measureTable', - settings - ); - this.measureTable.init([]); - - //hide the section until needed - this.participantDetails.wrap.selectAll('*').style('display', 'none'); - } - - function initResetButton() { - var chart = this; - - this.controls.reset = {}; - var reset = this.controls.reset; - reset.wrap = this.controls.wrap.append('div').attr('class', 'control-group'); - reset.label = reset.wrap - .append('span') - .attr('class', 'wc-control-label') - .text('Reset Chart'); - reset.button = reset.wrap - .append('button') - .text('Reset') - .on('click', function () { - var initial_container = chart.element; - var initial_settings = chart.initial_settings; - var initial_data = chart.initial_data; - chart.emptyChartWarning.remove(); - - chart.destroy(); - chart = null; - - var newChart = hepexplorer(initial_container, initial_settings); - newChart.init(initial_data); - }); - } - - function initDisplayControl() { - var chart = this; - var config = this.config; - var displayControlWrap = this.controls.wrap.selectAll('div').filter(function (controlInput) { - return controlInput.label === 'Display Type'; - }); - - var displayControl = displayControlWrap.select('select'); - - //set the start value - var start_value = config.display_options.find(function (f) { - return f.value == config.display; - }).label; - displayControl.selectAll('option').attr('selected', function (d) { - return d == start_value ? 'selected' : null; - }); - - //annotation of baseline visit (only visible when mDish is selected) - displayControlWrap - .append('span') - .attr('class', 'displayControlAnnotation span-description') - .style('color', 'blue') - .text( - 'Note: Baseline defined as ' + - chart.config.baseline.value_col + - ' = ' + - chart.config.baseline.values.join(',') - ) - .style('display', config.display == 'relative_baseline' ? null : 'none'); - - displayControl.on('change', function (d) { - var currentLabel = this.value; - var currentValue = config.display_options.find(function (f) { - return f.label == currentLabel; - }).value; - config.display = currentValue; - - if (currentValue == 'relative_baseline') { - displayControlWrap.select('span.displayControlAnnotation').style('display', null); - } else { - displayControlWrap.select('span.displayControlAnnotation').style('display', 'none'); - } - - config.cuts.display_change = true; - - chart.draw(); - }); - } - - function layoutPanels() { - this.wrap.style('display', 'inline-block').style('width', '75%'); - - this.controls.wrap - .style('display', 'inline-block') - .style('width', '25%') - .style('vertical-align', 'top'); - - this.controls.wrap.selectAll('div.control-group').style('display', 'block'); - this.controls.wrap - .selectAll('div.control-group') - .select('select') - .style('width', '200px'); - } - - function initTitle() { - this.titleDiv = this.controls.wrap - .insert('div', '*') - .attr('class', 'title') - .style('margin-right', '1em') - .style('margin-bottom', '1em'); - - this.titleDiv - .append('span') - .text(this.config.title) - .style('font-size', '1.5em') - .style('font-weight', 'strong') - .style('display', 'block'); - } - - function add(messageText, type, label, messages, callback) { - var messageObj = { - id: messages.list.length + 1, - type: type, - message: messageText, - label: label, - hidden: false, - callback: callback - }; - messages.list.push(messageObj); - messages.update(messages); - } - - function remove(id, label, messages) { - // hide the the message(s) by id or label - if (id) { - var matches = messages.list.filter(function (f) { - return +f.id == +id; - }); - } else if (label.length) { - var matches = messages.list.filter(function (f) { - return label == 'all' ? true : f.label == label; - }); - } - matches.forEach(function (d) { - d.hidden = true; - }); - messages.update(messages); - } - - function update(messages) { - function jsUcfirst(string) { - return string.charAt(0).toUpperCase() + string.slice(1); - } - - var visibleMessages = messages.list.filter(function (f) { - return f.hidden == false; - }); - - //update title - messages.header.title.text('Messages (' + visibleMessages.length + ')'); - - // - var messageDivs = messages.wrap.selectAll('div.message').data(visibleMessages, function (d) { - return d.id; - }); - - var newMessages = messageDivs - .enter() - .append('div') - .attr('class', function (d) { - return d.type + ' message ' + d.label; - }) - .html(function (d) { - var messageText = '' + jsUcfirst(d.type) + ': ' + d.message; - return messageText.split('.')[0] + '.'; - }) - .style('border-radius', '.5em') - .style('margin-right', '1em') - .style('margin-bottom', '0.5em') - .style('padding', '0.2em') - .style('font-size', '0.9em'); - - newMessages - .append('div.expand') - .html('•••') - .style('background', 'white') - .style('display', 'inline-block') - .style('border', '1px solid #999') - .style('padding', '0 0.2em') - .style('margin-left', '0.3em') - .style('font-size', '0.4em') - .style('border-radius', '0.6em') - .style('cursor', 'pointer') - .on('click', function (d) { - d3.select(this.parentNode) - .html(function (d) { - return '' + jsUcfirst(d.type) + ': ' + d.message; - }) - .each(function (d) { - if (d.callback) { - d.callback.call(this.parentNode); - } - }); - }); - - messageDivs.each(function (d) { - var type = d.type; - var thisMessage = d3.select(this); - if (type == 'caution') { - thisMessage - .style('border', '1px solid #faebcc') - .style('color', '#8a6d3b') - .style('background-color', '#fcf8e3'); - } else if (type == 'warning') { - thisMessage - .style('border', '1px solid #ebccd1') - .style('color', '#a94442') - .style('background-color', '#f2dede'); - } else { - thisMessage - .style('border', '1px solid #999') - .style('color', '#999') - .style('background-color', null); - } - - if (d.callback) { - d.callback.call(this); - } - }); - - messageDivs.exit().remove(); - } - - function init$1() { - var chart = this; - this.messages = { - add: add, - remove: remove, - update: update - }; - // this.messages.add = addMessage; - // this.messages.remove = removeMessage; - this.messages.list = []; - this.messages.wrap = this.controls.wrap.insert('div', '*').style('margin', '0 1em 1em 0'); - this.messages.header = this.messages.wrap - .append('div') - .style('border-top', '1px solid black') - .style('border-bottom', '1px solid black') - .style('font-weight', 'strong') - .style('margin', '0 1em 1em 0'); - - this.messages.header.title = this.messages.header - .append('div') - .attr('class', 'title') - .style('display', 'inline-block') - .text('Messages (0)'); - - this.messages.header.clear = this.messages.header - .append('div') - .text('Clear') - .style('font-size', '0.8em') - .style('vertical-align', 'center') - .style('display', 'inline-block') - .style('float', 'right') - .style('color', 'blue') - .style('cursor', 'pointer') - .style('text-decoration', 'underline') - .on('click', function () { - chart.messages.remove(null, 'all', chart.messages); - }); - } - - function initCustomWarning() { - if (this.config.warningText) { - this.messages.add( - this.config.warningText, - 'caution', - 'validationCaution', - this.messages - ); - } - } - - function downloadCSV(data, cols, file) { - var CSVarray = []; - - //add headers to CSV array - var cols = cols ? cols : Object.keys(data[0]); - var headers = cols.map(function (header) { - return '"' + header.replace(/"/g, '""') + '"'; - }); - CSVarray.push(headers); - //add rows to CSV array - data.forEach(function (d, i) { - var row = cols.map(function (col) { - var value = d[col]; - - if (typeof value === 'string') value = value.replace(/"/g, '""'); - - return '"' + value + '"'; - }); - - CSVarray.push(row); - }); - - //transform blob array into a blob of characters - var blob = new Blob([CSVarray.join('\n')], { - type: 'text/csv;charset=utf-8;' - }); - var fileCore = file ? file : 'eDish'; - var fileName = fileCore + '_' + d3.time.format('%Y-%m-%dT%H-%M-%S')(new Date()) + '.csv'; - var link = d3.select(this); - - if (navigator.msSaveBlob) - //IE - navigator.msSaveBlob(blob, fileName); - else if (link.node().download !== undefined) { - //21st century browsers - var url = URL.createObjectURL(blob); - link.node().setAttribute('href', url); - link.node().setAttribute('download', fileName); - } - } - - function initDroppedRowsWarning() { - var chart = this; - if (this.dropped_rows.length > 0) { - var warningText = - this.dropped_rows.length + - ' rows were removed. This is probably because of non-numeric or missing data provided for key variables. Click here to download a csv with a brief explanation of why each row was removed.'; - - this.messages.add(warningText, 'caution', 'droppedRows', this.messages, function () { - //custom callback to activate the droppedRows download - d3.select(this) - .select('a.rowDownload') - .style('color', 'blue') - .style('text-decoration', 'underline') - .style('cursor', 'pointer') - .datum(chart.dropped_rows) - .on('click', function (d) { - var systemVars = d3.merge([ - ['dropReason', 'NONE'], - Object.keys(chart.config.measure_values) - ]); - var cols = d3.merge([ - ['dropReason'], - Object.keys(d[0]).filter(function (f) { - return systemVars.indexOf(f) == -1; - }) - ]); - downloadCSV.call(this, d, cols, 'eDishDroppedRows'); - }); - }); - } - } - - function initControlLabels() { - var config = this.config; - - //Add settings label - var first_setting = this.controls.wrap - .selectAll('div.control-group') - .filter(function (f) { - return f.type != 'subsetter'; - }) - .filter(function (f) { - return f.option != 'r_ratio[0]'; - }) - .filter(function (f, i) { - return i == 0; - }) - .attr('class', 'first-setting'); - - this.controls.setting_header = this.controls.wrap - .insert('div', '.first-setting') - .attr('class', 'subtitle') - .style('border-top', '1px solid black') - .style('border-bottom', '1px solid black') - .style('margin-right', '1em') - .style('margin-bottom', '1em'); - - this.controls.setting_header - .append('span') - .text('Settings') - .style('font-weight', 'strong') - .style('display', 'block'); - - //Add filter label if at least 1 filter exists - if (config.r_ratio_filter || config.filters.length > 0) { - //insert a header before the first filter - var control_wraps = this.controls.wrap - .selectAll('div') - .filter(function (controlInput) { - return ( - controlInput.label === 'R Ratio Range' || controlInput.type === 'subsetter' - ); - }) - .classed('subsetter', true); - - this.controls.filter_header = this.controls.wrap - .insert('div', 'div.subsetter') - .attr('class', 'subtitle') - .style('border-top', '1px solid black') - .style('border-bottom', '1px solid black') - .style('margin-right', '1em') - .style('margin-bottom', '1em'); - this.controls.filter_header - .append('span') - .text('Filters') - .style('font-weight', 'strong') - .style('display', 'block'); - var population = d3 - .set( - this.initial_data.map(function (m) { - return m[config.id_col]; - }) - ) - .values().length; - this.controls.filter_header - .append('span') - .attr('class', 'popCount') - .html( - '' + - population + - ' of ' + - population + - ' participants shown.' - ) - .style('font-size', '0.8em'); - - this.controls.filter_numerator = this.controls.filter_header - .select('span.popCount') - .select('span.numerator'); - this.controls.filter_denominator = this.controls.filter_header - .select('span.popCount') - .select('span.denominator'); - } - } - - function addFootnote() { - this.footnote = this.wrap - .append('div') - .attr('class', 'footnote') - .text('Use controls to update chart or click a point to see participant details.') - .style('font-size', '0.7em') - .style('padding-top', '0.1em'); - this.footnote.timing = this.footnote.append('p'); - } - - function addDownloadButton() { - var chart = this; - var config = this.config; - if (config.downloadLink) { - this.titleDiv - .select('span') - .append('a') - .attr('class', 'downloadRaw') - .html('↓ Raw Data') - .attr('title', 'Download Raw Data') - .style('font-size', '.5em') - .style('margin-left', '1em') - .style('border', '1px solid black') - .style('border-radius', '2px') - .style('padding', '2px 4px') - .style('text-align', 'center') - .style('display', 'inline-block') - .style('cursor', 'pointer') - .style('font-weight', 'bold') - .datum(chart.initial_data) - .on('click', function (d) { - var systemVars = [ - 'dropReason', - 'NONE', - 'ALT', - 'TB', - 'impute_flag', - 'key_measure', - 'analysisFlag' - ]; - var cols = Object.keys(d[0]).filter(function (f) { - return systemVars.indexOf(f) == -1; - }); - downloadCSV.call(this, d, cols, 'eDishRawData'); - }); - } - } - - function initEmptyChartWarning() { - this.emptyChartWarning = d3 - .select(this.element) - .append('span') - .text('No data selected. Try updating your settings or resetting the chart. ') - .style('display', 'none') - .style('color', '#a94442') - .style('background-color', '#f2dede') - .style('border', '1px solid #ebccd1') - .style('padding', '0.5em') - .style('margin', '0 2% 12px 2%') - .style('border-radius', '0.2em'); - } - - function relabelMeasureControls() { - var chart = this; - var config = this.config; - var controlLabels = ['X-axis Measure', 'Y-axis Measure', 'Point Size']; - var controlWraps = chart.controls.wrap.selectAll('div').filter(function (controlInput) { - return controlLabels.indexOf(controlInput.label) > -1; - }); - var controls = controlWraps.select('select'); - var options = controls.selectAll('option'); - var allKeys = Object.keys(config.measure_values); - options - .text(function (d) { - return allKeys.indexOf(d) > -1 ? config.measure_values[d] : d; - }) - .property('value', function (d) { - return d; - }); - } - - function stopAnimation() { - var chart = this; - chart.svg - .transition() - .duration(0) - .each('end', function () { - chart.controls.studyDayPlayButton.datum({ state: 'play' }); - chart.controls.studyDayPlayButton.html('►'); - chart.draw(); - }); - } - - function customizePlotStyleToggle() { - var chart = this; - this.controls.wrap - .selectAll('.control-group') - .filter(function (d) { - return d.option === 'plot_max_values'; - }) - .selectAll('input') - .on('change', function (d) { - chart.config.plot_max_values = d; - stopAnimation.call(chart); - }); - } - - function startAnimation() { - var chart = this; - var config = this.config; - - // calculate animation duration - var day_count = chart.controls.studyDayRange[1] - config.plot_day; - var duration = day_count < 100 ? day_count * 100 : 30000; - var day_duration = duration / day_count; - - var base_size = config.marks[0].radius || config.flex_point_size; - var small_size = base_size / 2; - - function reposition(point) { - point - .transition() - .duration(day_duration) - .attr('cx', function (d) { - return chart.x(d[config.x.column]); - }) - .attr('cy', function (d) { - return chart.y(d[config.y.column]); - }) - .attr('r', function (d) { - if (d.outOfRange) { - return small_size; - } else if (config.point_size == 'Uniform') { - return base_size; - } else { - return chart.sizeScale(d[config.point_size]); - } - }) - .attr('fill-opacity', function (d) { - return config.plot_day < d.day_range[0] ? 0 : 0.5; - }); - } - - function updateDatum(d, currentDay) { - // adds temporary x, y and size (if any) measures for the current day to the core datum (instead of under d.value) - // these get removed when the transition ends and chart.draw() is called. - var measures = [config.x.column, config.y.column]; - if (config.point_size != 'Uniform') { - measures = d3.merge([measures, [config.point_size]]); - } - - var raw = d.values.raw[0]; - d.outOfRange = false; - d.day_range = raw.day_range; - d.moved = false; - measures.forEach(function (m) { - var vals = raw[m + '_raw']; - // capture the previous point position - d[m + '_prev'] = d[m]; - - // Did currentDay occur while participant was enrolled? - if (vals && vals.length) { - // && [config.x.column, config.y.column].includes(m)) { // out-of-range should be calculated study day of with x- and y-axis measures - var first = vals[0]; - var last = vals[vals.length - 1]; - var before = currentDay < first.day; - var after = currentDay > last.day; - d.outOfRange = d.outOfRange || before || after; - } - - //Get the most recent data point (or the first point if participant isn't enrolled yet) - var getLastMeasureIndex = d3.bisector(function (d) { - return d.day; - }).right; - var lastMeasureIndexPlusOne = vals ? getLastMeasureIndex(vals, currentDay) : 0; - var lastMeasureIndex = lastMeasureIndexPlusOne - 1; - - d[m] = - lastMeasureIndex >= 0 - ? vals[lastMeasureIndex]['value'] - : vals && vals.length - ? vals[0].value - : null; - - d[m + '_length'] = d[m] - d[m + '_prev']; - if (d[m + '_length']) { - d.moved = true; - } - }); - return d; - } - - function showDay(currentDay) { - //update the controls - config.plot_day = Math.floor(currentDay); - chart.controls.studyDayInput.node().value = config.plot_day; - - //update the label - chart.controls.studyDayControlWrap - .select('span.wc-control-label') - .html('Showing data from: Day ' + config.plot_day + '') - .select('strong') - .style('color', 'blue'); - - //reposition the points - var marks = chart.marks[0]; - - var groups = marks.groups.datum(function (d) { - return updateDatum(d, config.plot_day); - }); - - var points = groups.select('circle').each(function (d) { - if (d.moved) d3.select(this).call(reposition); - }); - - //draw trails - var tails = groups - .filter(function (d) { - return d.moved; - }) - .insert('line', ':first-child') - //static attributes - .attr('x1', function (d) { - return chart.x(d[config.x.column + '_prev']); - }) - .attr('y1', function (d) { - return chart.y(d[config.y.column + '_prev']); - }) - .attr('stroke', function (d) { - return chart.colorScale(d.values.raw[0][config.color_by]); - }) - //.attr('stroke', '#999') - - //transitional attributes - .attr('x2', function (d) { - return chart.x(d[config.x.column + '_prev']); - }) - .attr('y2', function (d) { - return chart.y(d[config.y.column + '_prev']); - }) - .attr('stroke-width', base_size); - tails.each(function (d) { - var path = d3.select(this); - var transition1 = path - .transition() - .duration(day_duration) - .ease('linear') - .attr('x2', function (d) { - return chart.x(d[config.x.column]); - }) - .attr('y2', function (d) { - return chart.y(d[config.y.column]); - }); - var transition2 = transition1 - .transition() - .duration(day_duration * 10) - .attr('stroke-width', '0px'); - }); - } - - function tweenStudyDay() { - var min = config.plot_day; - var max = chart.controls.studyDayRange[1]; - var studyday = d3.interpolateNumber(min, max); - - return function (t) { - showDay(studyday(t)); - }; - } - - //draw the chart to clear details view - chart.draw(); - - //hide quadrant info during startAnimation - chart.config.quadrants.table.wrap.style('display', 'none'); - chart.quadrant_labels.g.attr('display', 'none'); - - //show the stop button - chart.controls.studyDayPlayButton.datum({ state: 'stop' }); - chart.controls.studyDayPlayButton.html('■'); - - // Initialize the Transition - chart.myTransition = chart.svg - .transition() - .duration(duration) - .ease('linear') - .tween('studyday', tweenStudyDay) - .each('end', function () { - chart.draw(); - }); - } - - // ► = play symbol - // ■ = stop symbol - // ↺ = restart symbol - - function initPlayButton() { - var chart = this; - var config = this.config; - chart.controls.studyDayPlayButton = chart.controls.studyDayControlWrap - .append('button') - .datum({ state: 'play' }) - .html('►') //play symbol - .style('padding', '0.2em 0.5em 0.2em 0.5em') - .style('margin-left', '0.5em') - .style('border-radius', '0.4em') - //.style('display', 'none') - .on('click', function (d) { - var button = d3.select(this); - if (d.state === 'play') { - startAnimation.call(chart); - } else if (d.state === 'restart') { - config.plot_day = - chart.controls.studyDayRange[0] > 0 ? chart.controls.studyDayRange[0] : 0; - chart.controls.studyDayInput.node().value = config.plot_day; - chart.draw(); - - startAnimation.call(chart); - } else { - stopAnimation.call(chart); - } - }); - } - - function initStudyDayControl() { - var chart = this; - var config = this.config; - - // Move the study day control beneath the chart - chart.controls.secondary = chart.wrap - .insert('div', 'div.footnote') - .attr('class', 'wc-controls secondary-controls'); - - var removed = chart.controls.wrap - .selectAll('div') - .filter(function (controlInput) { - return controlInput.label === 'Study Day'; - }) - .remove(); - - chart.controls.studyDayControlWrap = chart.controls.secondary.append(function () { - return removed.node(); - }); - - //convert control to a slider - chart.controls.studyDayInput = chart.controls.studyDayControlWrap.select('input'); - chart.controls.studyDayInput.attr('type', 'range'); - - //set min and max values and add annotations - chart.controls.studyDayRange = d3.extent( - chart.imputed_data.filter(function (f) { - return f.key_measure; - }), - function (d) { - return d[config.studyday_col]; - } - ); - chart.controls.studyDayInput.attr('min', chart.controls.studyDayRange[0]); - chart.controls.studyDayInput.attr('max', chart.controls.studyDayRange[1]); - - chart.controls.studyDayControlWrap - .insert('span', 'input') - .attr('class', 'span-description') - .style('display', 'inline-block') - .style('padding-right', '0.2em') - .text(chart.controls.studyDayRange[0]); - chart.controls.studyDayControlWrap - .append('span') - .attr('class', 'span-description') - .style('display', 'inline-block') - .style('padding-left', '0.2em') - .text(chart.controls.studyDayRange[1]); - - //initialize plot_day to day 0 or the min value, whichever is greater - if (config.plot_day === null) { - config.plot_day = - chart.controls.studyDayRange[0] > 0 ? chart.controls.studyDayRange[0] : 0; - chart.controls.studyDayInput.node().value = config.plot_day; - } - - initPlayButton.call(this); - } - - function onLayout() { - layoutPanels.call(this); - - //init messages section - init$1.call(this); - initCustomWarning.call(this); - initDroppedRowsWarning.call(this); - - initTitle.call(this); - addDownloadButton.call(this); - - addFootnote.call(this); - formatRRatioControl.call(this); - initQuadrants.call(this); - initRugs.call(this); - initVisitPath.call(this); - initParticipantDetails.call(this); - initResetButton.call(this); - customizePlotStyleToggle.call(this); - initDisplayControl.call(this); - initControlLabels.call(this); - initEmptyChartWarning.call(this); - initStudyDayControl.call(this); - relabelMeasureControls.call(this); - } - - function updateAxisSettings() { - var config = this.config; - var unit = - config.display == 'relative_uln' - ? ' [xULN]' - : config.display == 'relative_baseline' - ? ' [xBaseline]' - : config.display == 'absolute' - ? ' [raw values]' - : null; - - //Update axis labels. - config.x.label = config.measure_values[config.x.column] + unit; - config.y.label = config.measure_values[config.y.column] + unit; - } - - function updateControlCutpointLabels() { - var config = this.config; - if ( - this.controls.config.inputs.find(function (input) { - return input.description === 'X-axis Reference Line'; - }) - ) - this.controls.wrap - .selectAll('.control-group') - .filter(function (d) { - return d.description === 'X-axis Reference Line'; - }) - .select('.wc-control-label') - .text(config.measure_values[config.x.column] + ' Reference Line'); - if ( - this.controls.config.inputs.find(function (input) { - return input.description === 'Y-axis Reference Line'; - }) - ) - this.controls.wrap - .selectAll('.control-group') - .filter(function (d) { - return d.description === 'Y-axis Reference Line'; - }) - .select('.wc-control-label') - .text(config.measure_values[config.y.column] + ' Reference Line'); - } - - function setMaxRRatio() { - var chart = this; - var config = this.config; - var r_ratio_wrap = chart.controls.wrap.selectAll('.control-group').filter(function (d) { - return d.option === 'r_ratio[0]'; - }); - - //if no max value is defined, use the max value from the data - if (this.config.r_ratio_filter) { - if (!config.r_ratio[1]) { - var raw_max_r_ratio = d3.max(this.raw_data, function (d) { - return d.rRatio_max; - }); - config.max_r_ratio = Math.ceil(raw_max_r_ratio * 10) / 10; //round up to the nearest 0.1 - config.r_ratio[1] = config.max_r_ratio; - chart.controls.wrap - .selectAll('.control-group') - .filter(function (d) { - return d.option === 'r_ratio[0]'; - }) - .select('input#r_ratio_max') - .property('value', config.max_r_ratio); - } - - //make sure r_ratio[0] <= r_ratio[1] - if (config.r_ratio[0] > config.r_ratio[1]) { - config.r_ratio = config.r_ratio.reverse(); - r_ratio_wrap.select('input#r_ratio_min').property('value', config.r_ratio[0]); - r_ratio_wrap.select('input#r_ratio_max').property('value', config.r_ratio[1]); - } - - //Define flag given r-ratio minimum. - this.raw_data.forEach(function (participant_obj) { - var aboveMin = participant_obj.rRatio >= config.r_ratio[0]; - var belowMax = participant_obj.rRatio <= config.r_ratio[1]; - participant_obj.rRatioFlag = aboveMin & belowMax ? 'Y' : 'N'; - }); - } - } - - function addParticipantLevelMetadata(d, participant_obj) { - var varList = []; - if (this.config.filters) { - var filterVars = this.config.filters.map(function (d) { - return d.hasOwnProperty('value_col') ? d.value_col : d; - }); - varList = d3.merge([varList, filterVars]); - } - if (this.config.group_cols) { - var groupVars = this.config.group_cols.map(function (d) { - return d.hasOwnProperty('value_col') ? d.value_col : d; - }); - varList = d3.merge([varList, groupVars]); - } - if (this.config.details) { - var detailVars = this.config.details.map(function (d) { - return d.hasOwnProperty('value_col') ? d.value_col : d; - }); - varList = d3.merge([varList, detailVars]); - } - - varList.forEach(function (v) { - participant_obj[v] = '' + d[0][v]; - }); - } - - function calculateRRatios(d, participant_obj) { - var chart = this; - var config = this.config; - - // R-ratio should be the ratio of ALT to ALP - - // For current time point or maximal values (depends on view) - participant_obj.rRatio_current = - participant_obj['ALT_relative_uln'] / participant_obj['ALP_relative_uln']; - - //get r-ratio data for every visit where both ALT and ALP are available - var allMatches = chart.imputed_data.filter(function (f) { - return f[config.id_col] == participant_obj[config.id_col]; - }); - - var raw_alt = allMatches - .filter(function (d) { - return d[config.measure_col] == config.measure_values.ALT; - }) - .map(function (m) { - m.day = m[config.studyday_col]; - m.alt_relative_uln = m.relative_uln; - return m; - }); - - participant_obj.rRatio_raw = allMatches - .filter(function (f) { - return f[config.measure_col] == config.measure_values.ALP; - }) - .map(function (m) { - m.day = m[config.studyday_col]; - m.alp_relative_uln = m.relative_uln; - return m; - }) - .filter(function (f) { - var matched_alt = raw_alt.find(function (fi) { - return fi.day == f.day; - }); - f.alt_relative_uln = matched_alt ? matched_alt.relative_uln : null; - f.rRatio = f['alt_relative_uln'] / f['alp_relative_uln']; - f.value = f.rRatio; - return f.rRatio; - }); - - //max rRatios across visits - participant_obj.rRatio_max = d3.max(participant_obj.rRatio_raw, function (f) { - return f.rRatio; - }); //max rRatio for all visits - participant_obj.rRatio_max_anly = d3.max( - participant_obj.rRatio_raw.filter(function (f) { - return f.analysisFlag; - }), - function (f) { - return f.rRatio; - } - ); - - // rRatio at time of max ALT - var maxAltRecord = participant_obj.rRatio_raw - .filter(function (f) { - return f.analysisFlag; - }) - .sort(function (a, b) { - return b.alt_relative_uln - a.alt_relative_uln; //descending sort (so max is first value) - })[0]; - - participant_obj.rRatio_max_alt = maxAltRecord ? maxAltRecord.rRatio : null; - - // Use the r ratio at the tme of the max ALT value for standard eDish, otherwise use rRatio from the current time point - participant_obj.rRatio = config.plot_max_values - ? participant_obj.rRatio_max_alt - : participant_obj.rRatio_current; - } - - function getMaxValues(d) { - var chart = this; - var config = this.config; - - var participant_obj = {}; - participant_obj.days_x = null; - participant_obj.days_y = null; - participant_obj.outOfRange = false; - Object.keys(config.measure_values).forEach(function (mKey) { - //get all raw data for the current measure - var all_matches = d - .filter(function (f) { - return config.measure_values[mKey] == f[config.measure_col]; - }) //get matching measures - .sort(function (a, b) { - return a[config.studyday_col] - b[config.studyday_col]; - }); - - //Drop Participants with no analysis data - var analysis_matches = all_matches.filter(function (f) { - return f.analysisFlag; - }); - participant_obj.drop_participant = analysis_matches.length === 0; - - if (participant_obj.drop_participant) { - if (config.debug) { - console.warn( - 'No analysis records found for ' + d[0][config.id_col] + ' for ' + mKey - ); - } - - participant_obj.drop_reason = - 'No analysis results found for 1+ key measure, including ' + mKey + '.'; - } else { - //keep an array of all [value, studyday] pairs for the measure - participant_obj[mKey + '_raw'] = all_matches.map(function (m, i) { - return { - value: m[config.display], - day: m[config.studyday_col], - analysisFlag: m.analysisFlag - }; - }); - - var currentRecord = null; - //get the current record for each participant - if (config.plot_max_values) { - //get record with maximum value for the current display type - var max_value = d3.max(analysis_matches, function (d) { - return +d[config.display]; - }); - var currentRecords = analysis_matches.filter(function (d) { - return max_value == +d[config.display]; - }); - if (config.debug & (currentRecords.length > 1)) { - var firstDay = currentRecords[0][config.studyday_col]; - var id = currentRecords[0][config.id_col]; - console.warn( - 'Found duplicate maximum ' + - mKey + - ' values for ' + - id + - '. Using first value from study day ' + - firstDay + - ' for timing calculations.' - ); - } - var currentRecord = currentRecords[0]; - participant_obj[mKey] = +currentRecord[config.display]; - } else { - //see if all selected config.plot_day was while participant was enrolled - var first = all_matches[0]; - var last = all_matches[all_matches.length - 1]; - - if ([config.x.column, config.y.column].includes(mKey)) { - // out-of-range should be calculated study day of with x- and y-axis measures - var before = config.plot_day < first[config.studyday_col]; - var after = config.plot_day > last[config.studyday_col]; - participant_obj.outOfRange = participant_obj.outOfRange || before || after; - } - - //get the most recent measure on or before config.plot_day - var onOrBefore = all_matches.filter(function (di) { - return di[config.studyday_col] <= config.plot_day; - }); - var latest = onOrBefore[onOrBefore.length - 1]; - - currentRecord = latest ? latest : first; - participant_obj[mKey] = currentRecord[config.display]; - } - // var currentRecord = all_matches.find(d => participant_obj[mKey] == +d[config.display]); - - //map all measure specific values - config.flat_cols.forEach(function (col) { - participant_obj[mKey + '_' + col] = currentRecord ? currentRecord[col] : null; - }); - - //determine whether the value is above the specified threshold - if (config.cuts[mKey][config.display]) { - config.show_quadrants = true; - participant_obj[mKey + '_cut'] = config.cuts[mKey][config.display]; - participant_obj[mKey + '_flagged'] = - participant_obj[mKey] >= participant_obj[mKey + '_cut']; - } else { - config.show_quadrants = false; - participant_obj[mKey + '_cut'] = null; - participant_obj[mKey + '_flagged'] = null; - } - - //save study days for each axis; - if (currentRecord) { - if (mKey == config.x.column) - participant_obj.days_x = currentRecord[config.studyday_col]; - if (mKey == config.y.column) - participant_obj.days_y = currentRecord[config.studyday_col]; - } - } - }); - - //Add participant level metadata - addParticipantLevelMetadata.call(chart, d, participant_obj); - - //Calculate ratios between measures. - calculateRRatios.call(chart, d, participant_obj); - - //calculate the day difference between x and y and total day range for all measure values - participant_obj.day_diff = Math.abs(participant_obj.days_x - participant_obj.days_y); - participant_obj.day_range = d3.extent(d, function (d) { - return d[config.studyday_col]; - }); - - return participant_obj; - } - - function getFlatCols() { - var config = this.config; - - //get list of columns to flatten - var flat_cols = []; - var user_cols = [ - 'measure_col', - 'value_col', - 'studyday_col', - 'normal_col_low', - 'normal_col_high' - ]; - var derived_cols = [ - 'absolute', - 'relative_uln', - 'relative_baseline', - 'baseline_absolute', - 'analysisFlag' - ]; - - // populate the full list of columns to flatten labels - user_cols.forEach(function (d) { - if (Array.isArray(d)) { - d.forEach(function (di) { - flat_cols.push( - di.hasOwnProperty('value_col') ? config[di.value_col] : config[di] - ); - }); - } else { - flat_cols.push(d.hasOwnProperty('value_col') ? config[d.value_col] : config[d]); - } - }); - - //merge in the derived columns - return d3.merge([flat_cols, derived_cols]); - } - - function calculatePalt(pt) { - // Calculates the pAlt value for the given participant ID - // For more on PAlt see the following paper: A Rapid Method to Estimate Hepatocyte Loss Due to Drug-Induced Liver Injury by Chung et al - // Requires: Baseline visit - // Assumes: Units on Alt are IU/L - var config = this.config; - - //Get a list of raw post-baseline ALT values - var alt_values = pt.values.raw - .filter(function (f) { - return f[config.measure_col] == config.measure_values.ALT; - }) - .filter(function (f) { - return f.paltFlag; - }) - .map(function (d) { - var obj = {}; - obj.value = d[config.value_col]; - obj.day = d[config.studyday_col]; - obj.hour = d.day * 24; - return obj; - }); - if (alt_values.length > 1) { - //get peak alt value - var alt_peak = d3.max(alt_values, function (f) { - return f.value; - }); - - //caluculate ALT AUC - var alt_auc = d3.sum(alt_values, function (d, i) { - if (i < alt_values.length - 1) { - var di = d; - var vi = di.value; - var hi = di.hour; - var dii = alt_values[i + 1]; - var vii = dii.value; - var hii = dii.hour; - var v_avg = (vii + vi) / 2; - var h_diff = hii - hi; - var segment_auc = v_avg * h_diff; - - return segment_auc; - } else { - return 0; - } - }); - - //calculate pAlt (ALT AUC + AltPeak ^ 0.18) - var p_alt = (alt_auc * Math.pow(alt_peak, 0.18)) / 100000; - var p_alt_rounded = d3.format('0.2f')(p_alt); - var alt_auc_rounded = d3.format('0.2f')(alt_auc); - var alt_peak_rounded = d3.format('0.2f')(alt_peak); - var note = - 'NOTE: ' + - 'For this participant, PALT was calculated as: ' + - 'ALT AUC * Peak ALT 0.18 / 105 = ' + - alt_auc_rounded + - ' * ' + - alt_peak_rounded + - ' 0.18 / 105 = ' + - p_alt_rounded + - '
' + - 'PALT shows promise in predicting the percentage hepatocyte loss on the basis of the maximum value and the AUC of serum ALT observed during a DILI event. For more details see A Rapid Method to Estimate Hepatocyte Loss Due to Drug-Induced Liver Injury by Chung et al.'; - - var obj = { - value: p_alt, - text_value: p_alt_rounded, - values: alt_values, - note: note, - components: { - peak: alt_peak, - auc: alt_auc - } - }; - - return obj; - } else { - return null; //if no alt values are found - } - } - - //Converts a one record per measure data object to a one record per participant objects - function flattenData() { - var chart = this; - var config = this.config; - - ////////////////////////////////////////////// - //make a data set with one object per ID - ///////////////////////////////////////////// - - //get a list of columns to flatten - config.flat_cols = getFlatCols.call(this); - - //get maximum values for each measure type - var flat_data = d3 - .nest() - .key(function (f) { - return f[config.id_col]; - }) - .rollup(function (d) { - return getMaxValues.call(chart, d); - }) - .entries( - this.imputed_data.filter(function (f) { - return f.key_measure; - }) - ); - - chart.dropped_participants = flat_data - .filter(function (f) { - return f.values.drop_participant; - }) - .map(function (d) { - return { - id: d.key, - drop_reason: d.values.drop_reason, - allrecords: chart.initial_data.filter(function (f) { - return f[config.id_col] == d.key; - }) - }; - }); - var flat_data = flat_data - .filter(function (f) { - return !f.values.drop_participant; - }) - .map(function (m) { - m.values[config.id_col] = m.key; - - //link the raw data to the flattened object - var allMatches = chart.imputed_data.filter(function (f) { - return f[config.id_col] == m.key; - }); - m.values.raw = allMatches; - - m.values.p_alt = config.calculate_palt ? calculatePalt.call(chart, m) : null; - return m.values; - }); - return flat_data; - } - - function setLegendLabel() { - //change the legend label to match the group variable - //or hide legend if group = NONE - this.config.legend.label = - this.config.color_by !== 'NONE' - ? this.config.group_cols[ - this.config.group_cols - .map(function (group) { - return group.value_col; - }) - .indexOf(this.config.color_by) - ].label - : ''; - } - - function showMissingDataWarning() { - var chart = this; - var config = chart.config; - - if (config.debug) { - //confirm participants are only dropped once (?!) - var unique_dropped_participants = d3 - .set( - this.dropped_participants.map(function (m) { - return m.id; - }) - ) - .values().length; - console.warn( - 'Of ' + - this.dropped_participants.length + - ' dropped participants, ' + - unique_dropped_participants + - ' are unique.' - ); - console.warn(this.dropped_participants); - } - - chart.messages.remove(null, 'droppedPts', chart.messages); //remove message from previous render - if (this.dropped_participants.length > 0) { - var warningText = - this.dropped_participants.length + - ' participants are not plotted. They likely have invalid or missing data for key variables in the current chart. Click here to download a csv with a brief explanation of why each participant was not plotted.'; - - this.messages.add(warningText, 'caution', 'droppedPts', this.messages, function () { - //custom callback to activate the droppedRows download - d3.select(this) - .select('a.ptDownload') - .style('color', 'blue') - .style('text-decoration', 'underline') - .style('cursor', 'pointer') - .datum(chart.dropped_participants) - .on('click', function (d) { - var cols = ['id', 'drop_reason']; - downloadCSV.call(this, d, cols, 'eDishDroppedParticipants'); - }); - }); - } - } - - function dropMissingValues() { - var chart = this; - var config = this.config; - //drop records with missing or invalid (negative) values - var missing_count = d3.sum(this.raw_data, function (f) { - return f[config.x.column] <= 0 || f[config.y.column] <= 0; - }); - - if (missing_count > 0) { - this.raw_data = this.raw_data.map(function (d) { - d.nonPositiveFlag = d[config.x.column] <= 0 || d[config.y.column] <= 0; - var type = config.display == 'relative_uln' ? 'eDish' : 'mDish'; - // generate an informative reason the participant was dropped - var dropText = - type + - ' values could not be generated for ' + - config.x.column + - ' or ' + - config.y.column + - '. '; - - // x type is mdish and baseline is missing - if ((type == 'mDish') & !d[config.x.column + '_baseline_absolute']) { - dropText = dropText + 'Baseline for ' + config.x.column + ' is missing. '; - } - - // y type is mdish and baseline is missing - if ((type == 'mDish') & !d[config.y.column + '_baseline_absolute']) { - dropText = dropText + 'Baseline for ' + config.y.column + ' is missing. '; - } - - d.drop_reason = d.nonPositiveFlag ? dropText : ''; - return d; - }); - - this.dropped_participants = d3.merge([ - this.dropped_participants, - this.raw_data - .filter(function (f) { - return f.nonPositiveFlag; - }) - .map(function (m) { - return { id: m[config.id_col], drop_reason: m.drop_reason }; - }) - ]); - - this.dropped_participants.map(function (m) { - m.raw = chart.initial_data.filter(function (f) { - return f[config.id_col] == m.id; - }); - }); - } - - this.raw_data = this.raw_data.filter(function (f) { - return !f.nonPositiveFlag; - }); - showMissingDataWarning.call(this); - } - - function onPreprocess() { - updateAxisSettings.call(this); //update axis label based on display type - updateControlCutpointLabels.call(this); //update cutpoint control labels given x- and y-axis variables - this.raw_data = flattenData.call(this); //convert from visit-level data to participant-level data - setMaxRRatio.call(this); - setLegendLabel.call(this); //update legend label based on group variable - dropMissingValues.call(this); - } - - function onDataTransform() { } - - function updateQuadrantData() { - var chart = this; - var config = this.config; - - //add "eDISH_quadrant" column to raw_data - var x_var = this.config.x.column; - var y_var = this.config.y.column; - - var x_cut = this.config.cuts[x_var][config.display]; - var y_cut = this.config.cuts[y_var][config.display]; - - this.filtered_data.forEach(function (d) { - var x_cat = d[x_var] >= x_cut ? 'xHigh' : 'xNormal'; - var y_cat = d[y_var] >= y_cut ? 'yHigh' : 'yNormal'; - d['eDISH_quadrant'] = x_cat + ':' + y_cat; - }); - - //update Quadrant data - config.quadrants.forEach(function (quad) { - quad.count = chart.filtered_data.filter(function (d) { - return d.eDISH_quadrant == quad.dataValue; - }).length; - quad.total = chart.filtered_data.length; - quad.percent = d3.format('0.1%')(quad.count / quad.total); - }); - } - - function setDomain(dimension) { - var config = this.config; - var domain = this[dimension].domain(); - var measure = config[dimension].column; - var measure_long = config.measure_values[measure]; - var cut = config.cuts[measure][config.display]; - var values = this.imputed_data - .filter(function (f) { - return f[config.measure_col] == measure_long; - }) - .map(function (m) { - return +m[config.display]; - }) - .filter(function (m) { - return m > 0; - }) - .sort(function (a, b) { - return a - b; - }); - var val_extent = d3.extent(values); - - //make sure the domain contains the cut point and the max possible value for the measure - domain[1] = d3.max([domain[1], cut * 1.01, val_extent[1]]); - - // make sure the domain lower limit captures all of the raw Values - if (this.config[dimension].type == 'linear') { - // just use the lower limit of 0 for continuous - domain[0] = 0; - } else if (this.config[dimension].type == 'log') { - // use the smallest raw value for a log axis - - var minValue = val_extent[0]; - - if (minValue < domain[0]) { - domain[0] = minValue; - } - - //throw a warning if the domain is > 0 if using log scale - if (this[dimension].type == 'log' && domain[0] <= 0) { - console.warn( - "Can't draw a log " + dimension + '-axis because there are values <= 0.' - ); - } - } - this[dimension + '_dom'] = domain; - } - - function clearVisitPath() { - this.visitPath.selectAll('*').remove(); - } - - function clearParticipantHeader() { - this.participantDetails.header.selectAll('*').remove(); //clear participant header - } - - function hideMeasureTable() { - this.measureTable.draw([]); - this.measureTable.wrap.selectAll('*').style('display', 'none'); - } - - function clearRugs(axis) { - this[axis + '_rug'].selectAll('*').remove(); - } - - function formatPoints() { - var chart = this; - var config = this.config; - var points = this.svg - .select('g.point-supergroup.mark1') - .selectAll('g.point') - .select('circle'); - - points - .attr('stroke', function (d) { - var disabled = d3.select(this).classed('disabled'); - var raw = d.values.raw[0], - pointColor = chart.colorScale(raw[config.color_by]); - return disabled ? '#ccc' : pointColor; - }) - .attr('fill', function (d) { - var disabled = d3.select(this).classed('disabled'); - var raw = d.values.raw[0], - pointColor = chart.colorScale(raw[config.color_by]); - return disabled ? 'white' : pointColor; - }) - .attr('stroke-width', 1) - .style('clip-path', null); - } - - function clearParticipantDetails() { - var chart = this; - var config = this.config; - var points = this.svg - .select('g.point-supergroup.mark1') - .selectAll('g.point') - .select('circle'); - - points.classed('disabled', false); - - // update - chart.participantsSelected = []; - chart.events.participantsSelected.data = chart.participantsSelected; - chart.wrap.node().dispatchEvent(chart.events.participantsSelected); - - // remove/hide details - this.config.quadrants.table.wrap.style('display', null); - clearVisitPath.call(this); //remove path - clearParticipantHeader.call(this); - clearRugs.call(this, 'x'); //clear rugs - clearRugs.call(this, 'y'); - hideMeasureTable.call(this); //remove the detail table - formatPoints.call(this); - - this.participantDetails.wrap.selectAll('*').style('display', 'none'); - } - - function updateFilterLabel() { - if (this.controls.filter_numerator) { - this.controls.filter_numerator.text(this.filtered_data.length); - } - } - - function setCutpointMinimums() { - var chart = this; - var config = this.config; - var lower_limits = { - x: chart['x_dom'][0], - y: chart['y_dom'][0] - }; - - //Make sure cutpoint isn't below lower domain - Comes in to play when changing from log to linear axes - Object.keys(lower_limits).forEach(function (dimension) { - var measure = config[dimension].column; - var current_cut = config.cuts[measure][config.display]; - var min = lower_limits[dimension]; - if (current_cut < min) { - config.cuts[measure][config.display] = min; - chart.controls.wrap - .selectAll('div.control-group') - .filter(function (f) { - return f.description - ? f.description.toLowerCase() == dimension + '-axis reference line' - : false; - }) - .select('input') - .node().value = min; - } - }); - - //Update cut point controls - var controlWraps = this.controls.wrap - .selectAll('.control-group') - .filter(function (d) { - return /.-axis Reference Line/i.test(d.description); - }) - .attr('min', function (d) { - return lower_limits[d.description.split('-')[0]]; - }); - - controlWraps.select('input').on('change', function (d) { - var dimension = d.description.split('-')[0].toLowerCase(); - var min = chart[dimension + '_dom'][0]; - var input = d3.select(this); - - //Prevent a cutpoint less than the lower domain. - if (input.property('value') < min) input.property('value', min); - - //Update chart setting. - var measure = config[dimension].column; - config.cuts[measure][config.display] = input.property('value'); - chart.draw(); - }); - } - - function syncCutpoints() { - var chart = this; - var config = this.config; - - //check to see if the cutpoint used is current - if ( - config.cuts.x != config.x.column || - config.cuts.y != config.y.column || - config.cuts.display != config.display - ) { - // if not, update it! - - // track the current cut point variables - config.cuts.x = config.x.column; - config.cuts.y = config.y.column; - config.cuts.display = config.display; - - // update the cutpoint shown in the control - config.cuts.display_change = false; //reset the change flag; - var dimensions = ['x', 'y']; - dimensions.forEach(function (dimension) { - //change the control to point at the correct cut point - var dimInput = chart.controls.wrap - .selectAll('div.control-group') - .filter(function (f) { - return f.description - ? f.description.toLowerCase() == dimension + '-axis reference line' - : false; - }) - .select('input'); - - dimInput.node().value = config.cuts[config[dimension].column][config.display]; - - //don't think this actually changes functionality, but nice to have it accurate just in case - dimInput.option = - 'settings.cuts.' + [config[dimension].column] + '.' + [config.display]; - }); - } - } - - function hideEmptyChart() { - var emptyChart = this.filtered_data.length == 0; - this.wrap.style('display', emptyChart ? 'none' : 'inline-block'); - this.emptyChartWarning.style('display', emptyChart ? 'inline-block' : 'none'); - } - - function updateStudyDayControl() { - var chart = this; - var config = this.config; - - // cancel the animation (if any is running) - var activeAnimation = chart.controls.studyDayPlayButton.datum().state == 'stop'; - if (activeAnimation) { - chart.svg.transition().duration(0); - } - - // hide study day control if viewing max values - chart.controls.studyDayControlWrap.style('display', config.plot_max_values ? 'none' : null); - - //set the status of the play button - if (config.plot_day >= chart.controls.studyDayRange[1]) { - chart.controls.studyDayPlayButton.datum({ state: 'restart' }); - chart.controls.studyDayPlayButton.html('↺'); - } else { - chart.controls.studyDayPlayButton.datum({ state: 'play' }); - chart.controls.studyDayPlayButton.html('►'); - } - - //update the study day control label with the currently selected values - var currentValue = chart.controls.studyDayControlWrap.select('input').property('value'); - chart.controls.studyDayControlWrap - .select('span.wc-control-label') - .html('Showing data from: Day ' + currentValue + '') - .select('strong') - .style('color', 'blue'); - } - - function onDraw() { - //show/hide the study day controls - updateStudyDayControl.call(this); - - //clear participant Details (if they exist) - clearParticipantDetails.call(this); - - //get correct cutpoint for the current view - syncCutpoints.call(this); - - //update domains to include cut lines - setDomain.call(this, 'x'); - setDomain.call(this, 'y'); - - //Set update cutpoint interactivity - setCutpointMinimums.call(this); - - //Classify participants in to eDISH quadrants - updateQuadrantData.call(this); - - //update the count in the filter label - updateFilterLabel.call(this); - hideEmptyChart.call(this); - } - - //draw marginal rug for visit-level measures - function drawRugs(d, axis) { - var chart = this; - var config = this.config; - - //get matching measures - var allMatches = d.values.raw[0].raw; - var measure = config.measure_values[config[axis].column]; - var matches = allMatches.filter(function (f) { - return f[config.measure_col] == measure; - }); - - //draw the rug - var min_value = axis == 'x' ? chart.y.domain()[0] : chart.x.domain()[0]; - var rugs = chart[axis + '_rug'] - .selectAll('text') - .data(matches) - .enter() - .append('text') - .attr({ - class: 'rug-tick', - x: function x(di) { - return axis === 'x' ? chart.x(di[config.display]) : chart.x(min_value); - }, - y: function y(di) { - return axis === 'y' ? chart.y(di[config.display]) : chart.y(min_value); - }, - dy: axis === 'y' ? 4 : 0, - 'text-anchor': axis === 'y' ? 'end' : null, - 'alignment-baseline': axis === 'x' ? 'hanging' : null, - 'font-size': axis === 'x' ? '6px' : null, - stroke: function stroke(di) { - return chart.colorScale(di[config.color_by]); - }, - 'stroke-width': function strokeWidth(di) { - return di[config.display] === d.values[axis] ? '3px' : '1px'; - } - }) - .text(function (d) { - return axis === 'x' ? '|' : '–'; - }); - - //Add tooltips to rugs. - rugs.append('svg:title').text(function (d) { - return ( - d[config.measure_col] + - '=' + - d3.format('.2f')(d.absolute) + - ' (' + - d3.format('.2f')(d.relative_uln) + - ' xULN) @ ' + - d[config.visit_col] + - '/Study Day ' + - d[config.studyday_col] - ); - }); - } - - function addPointMouseover() { - var chart = this; - var config = this.config; - var points = this.marks[0].circles; - - //add event listener to all participant level points - points - .filter(function (d) { - var disabled = d3.select(this).classed('disabled'); - return !disabled; - }) - .on('mouseover', function (d) { - //disable mouseover when highlights (onClick) are visible - var disabled = d3.select(this).classed('disabled'); - if (!disabled) { - //clear previous mouseover if any - points.attr('stroke-width', 1); - clearRugs.call(chart, 'x'); - clearRugs.call(chart, 'y'); - - //draw the rugs - d3.select(this).attr('stroke-width', 3); - drawRugs.call(chart, d, 'x'); - drawRugs.call(chart, d, 'y'); - } - }); - } - - function drawVisitPath(d) { - var chart = this; - var config = chart.config; - - var allMatches = d.values.raw[0].raw; - var x_measure = config.measure_values[config.x.column]; - var y_measure = config.measure_values[config.y.column]; - var matches = allMatches.filter(function (f) { - return f[config.measure_col] == x_measure || f[config.measure_col] == y_measure; - }); - - //get coordinates by visit - var visits = d3 - .set( - matches.map(function (m) { - return m[config.studyday_col]; - }) - ) - .values(); - var visit_data = visits - .map(function (m) { - var visitObj = {}; - visitObj.studyday = +m; - visitObj.visit = config.visit_col - ? matches.filter(function (f) { - return f[config.studyday_col] == m; - })[0][config.visit_col] - : null; - visitObj.visitn = config.visitn_col - ? matches.filter(function (f) { - return f[config.studyday_col] == m; - })[0][config.visitn_col] - : null; - visitObj[config.color_by] = matches[0][config.color_by]; - - //get x coordinate - var x_match = matches - .filter(function (f) { - return f[config.studyday_col] == m; - }) - .filter(function (f) { - return f[config.measure_col] == x_measure; - }); - - if (x_match.length) { - visitObj.x = x_match[0][config.display]; - visitObj.xMatch = x_match[0]; - } else { - visitObj.x = null; - visitObj.xMatch = null; - } - - //get y coordinate - var y_match = matches - .filter(function (f) { - return f[config.studyday_col] == m; - }) - .filter(function (f) { - return f[config.measure_col] == y_measure; - }); - if (y_match.length) { - visitObj.y = y_match[0][config.display]; - visitObj.yMatch = y_match[0]; - } else { - visitObj.y = null; - visitObj.yMatch = null; - } - - //get rRatio - var rRatio_match = d.values.raw[0].rRatio_raw.filter(function (f) { - return f[config.studyday_col] == m; - }); - visitObj.rRatio = rRatio_match.length ? rRatio_match[0].rRatio : null; - - return visitObj; - }) - .sort(function (a, b) { - return a.studyday - b.studyday; - }) - .filter(function (f) { - return (f.x > 0) & (f.y > 0); - }); - - //draw the path - var myLine = d3.svg - .line() - .x(function (d) { - return chart.x(d.x); - }) - .y(function (d) { - return chart.y(d.y); - }); - - chart.visitPath.selectAll('*').remove(); - chart.visitPath.moveToFront(); - - var path = chart.visitPath - .append('path') - .attr('class', 'participant-visits') - .datum(visit_data) - .attr('d', myLine) - .attr('stroke', function (d) { - return chart.colorScale(matches[0][config.color_by]); - }) - .attr('stroke-width', '2px') - .attr('fill', 'none'); - - //Little trick for animating line drawing - var totalLength = path.node().getTotalLength(); - path.attr('stroke-dasharray', totalLength + ' ' + totalLength) - .attr('stroke-dashoffset', totalLength) - .transition() - .duration(2000) - .ease('linear') - .attr('stroke-dashoffset', 0); - - //draw visit points - var visitPoints = chart.visitPath - .selectAll('g.visit-point') - .data(visit_data) - .enter() - .append('g') - .attr('class', 'visit-point'); - - visitPoints - .append('circle') - .attr('class', 'participant-visits') - .attr('r', 0) - .attr('stroke', function (d) { - return chart.colorScale(d[config.color_by]); - }) - .attr('stroke-width', 1) - .attr('cx', function (d) { - return chart.x(d.x); - }) - .attr('cy', function (d) { - return chart.y(d.y); - }) - .attr('fill', function (d) { - return chart.colorScale(d[config.color_by]); - }) - .attr('fill-opacity', 0) - .transition() - .delay(2000) - .duration(200) - .attr('r', 4); - - //custom titles for points on mouseover - visitPoints.append('title').text(function (d) { - var xvar = config.x.column; - var yvar = config.y.column; - - var studyday_label = 'Study day: ' + d.studyday + '\n', - visitn_label = d.visitn ? 'Visit Number: ' + d.visitn + '\n' : '', - visit_label = d.visit ? 'Visit: ' + d.visit + '\n' : '', - x_label = config.x.label + ': ' + d3.format('0.3f')(d.x) + '\n', - y_label = config.y.label + ': ' + d3.format('0.3f')(d.y), - rRatio_label = d.rRatio ? '\nR Ratio: ' + d3.format('0.2f')(d.rRatio) : ''; - - return studyday_label + visit_label + visitn_label + x_label + y_label + rRatio_label; - }); - } - - function makeNestedData(d) { - var chart = this; - var config = chart.config; - var allMatches = d.values.raw[0].raw; - - var ranges = d3 - .nest() - .key(function (d) { - return d[config.measure_col]; - }) - .rollup(function (d) { - var vals = d - .map(function (m) { - return m[config.value_col]; - }) - .sort(function (a, b) { - return a - b; - }); - var lower_extent = d3.quantile(vals, config.measureBounds[0]), - upper_extent = d3.quantile(vals, config.measureBounds[1]); - return [lower_extent, upper_extent]; - }) - .entries(chart.initial_data); - - //make nest by measure - var nested = d3 - .nest() - .key(function (d) { - return d[config.measure_col]; - }) - .rollup(function (d) { - var measureObj = {}; - measureObj.eDish = chart; - measureObj.key = d[0][config.measure_col]; - measureObj.raw = d; - measureObj.values = d.map(function (d) { - return +d[config.value_col]; - }); - measureObj.max = +d3.format('0.2f')(d3.max(measureObj.values)); - measureObj.min = +d3.format('0.2f')(d3.min(measureObj.values)); - measureObj.median = +d3.format('0.2f')(d3.median(measureObj.values)); - measureObj.n = measureObj.values.length; - measureObj.spark = 'spark!'; - measureObj.population_extent = ranges.find(function (f) { - return measureObj.key == f.key; - }).values; - var hasColor = - chart.spaghetti.colorScale.domain().indexOf(d[0][config.measure_col]) > -1; - measureObj.color = hasColor - ? chart.spaghetti.colorScale(d[0][config.measure_col]) - : 'black'; - measureObj.spark_data = d.map(function (m) { - var obj = { - id: m[config.id_col], - lab: m[config.measure_col], - visit: config.visit_col ? m[config.visit_col] : null, - visitn: config.visitn_col ? +m[config.visitn_col] : null, - studyday: +m[config.studyday_col], - value: +m[config.value_col], - lln: config.normal_col_low ? +m[config.normal_col_low] : null, - uln: +m[config.normal_col_high], - population_extent: measureObj.population_extent, - outlier_low: config.normal_col_low - ? +m[config.value_col] < +m[config.normal_col_low] - : null, - outlier_high: +m[config.value_col] > +m[config.normal_col_high] - }; - obj.outlier = obj.outlier_low || obj.outlier_high; - return obj; - }); - return measureObj; - }) - .entries(allMatches); - - var nested = nested - .map(function (m) { - return m.values; - }) - .sort(function (a, b) { - var a_order = Object.keys(config.measure_values) - .map(function (e) { - return config.measure_values[e]; - }) - .indexOf(a.key); - var b_order = Object.keys(config.measure_values) - .map(function (e) { - return config.measure_values[e]; - }) - .indexOf(b.key); - return b_order - a_order; - }); - return nested; - } - - function addSparkLines(d) { - if (this.data.raw.length > 0) { - //don't try to draw sparklines if the table is empty - this.tbody - .selectAll('tr') - .style('background', 'none') - .style('border-bottom', '.5px solid black') - .each(function (row_d) { - //Spark line cell - var cell = d3 - .select(this) - .select('td.spark') - .classed('minimized', true) - .text(''), - toggle = cell - .append('span') - .html('▽') - .style('cursor', 'pointer') - .style('color', '#999') - .style('vertical-align', 'middle'), - width = 100, - height = 25, - offset = 4, - overTime = row_d.spark_data.sort(function (a, b) { - return +a.studyday - +b.studyday; - }), - color = row_d.color; - - var x = d3.scale - .linear() - .domain( - d3.extent(overTime, function (m) { - return m.studyday; - }) - ) - .range([offset, width - offset]); - - //y-domain includes 99th population percentile + any participant outliers - var y_min = d3.min(d3.merge([row_d.values, row_d.population_extent])) * 0.99; - var y_max = d3.max(d3.merge([row_d.values, row_d.population_extent])) * 1.01; - var y = d3.scale - .linear() - .domain([y_min, y_max]) - .range([height - offset, offset]); - - //render the svg - var svg = cell - .append('svg') - .attr({ - width: width, - height: height - }) - .append('g'); - - //draw the normal range polygon ULN and LLN - var upper = overTime.map(function (m) { - return { studyday: m.studyday, value: m.uln }; - }); - var lower = overTime - .map(function (m) { - return { studyday: m.studyday, value: m.lln }; - }) - .reverse(); - var normal_data = d3.merge([upper, lower]).filter(function (m) { - return m.value; - }); - - var drawnormal = d3.svg - .line() - .x(function (d) { - return x(d.studyday); - }) - .y(function (d) { - return y(d.value); - }); - - var normalpath = svg - .append('path') - .datum(normal_data) - .attr({ - class: 'normalrange', - d: drawnormal, - fill: '#eee', - stroke: 'none' - }); - - //draw lines at the population guidelines - svg.selectAll('lines.guidelines') - .data(row_d.population_extent) - .enter() - .append('line') - .attr('class', 'guidelines') - .attr('x1', 0) - .attr('x2', width) - .attr('y1', function (d) { - return y(d); - }) - .attr('y2', function (d) { - return y(d); - }) - .attr('stroke', '#ccc') - .attr('stroke-dasharray', '2 2'); - - //draw the sparkline - var draw_sparkline = d3.svg - .line() - .interpolate('cardinal') - .x(function (d) { - return x(d.studyday); - }) - .y(function (d) { - return y(d.value); - }); - var sparkline = svg - .append('path') - .datum(overTime) - .attr({ - class: 'sparkLine', - d: draw_sparkline, - fill: 'none', - stroke: color - }); - - //draw outliers - var outliers = overTime.filter(function (f) { - return f.outlier; - }); - var outlier_circles = svg - .selectAll('circle.outlier') - .data(outliers) - .enter() - .append('circle') - .attr('class', 'circle outlier') - .attr('cx', function (d) { - return x(d.studyday); - }) - .attr('cy', function (d) { - return y(d.value); - }) - .attr('r', '2px') - .attr('stroke', color) - .attr('fill', color); - }); - } - } - - function insertAfter(newNode, referenceNode) { - referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); - } - - var defaultSettings = { - max_width: 800, - aspect: 4, - x: { - column: 'studyday', - type: 'linear', - label: 'Study Day' - }, - y: { - column: 'value', - type: 'linear', - label: '', - format: '.1f' - }, - marks: [ - { - type: 'line', - per: ['lab'] - }, - { - type: 'circle', - radius: 4, - per: ['lab', 'studyday'] //, - // values: { outlier: [true] }, - // attributes: { - // 'fill-opacity': 1 - // } - } - ], - margin: { top: 20 }, - gridlines: 'x', - colors: [] - }; - - function setDomain$1(d) { - //y-domain includes 99th population percentile + any participant outliers - var raw_values = this.raw_data.map(function (m) { - return m.value; - }); - var population_extent = this.raw_data[0].population_extent; - var y_min = d3.min(d3.merge([raw_values, population_extent])) * 0.99; - var y_max = d3.max(d3.merge([raw_values, population_extent])) * 1.01; - this.y.domain([y_min, y_max]); - this.y_dom = [y_min, y_max]; - } - - function drawPopulationExtent() { - var lineChart = this; - this.svg - .selectAll('line.guidelines') - .data(lineChart.raw_data[0].population_extent) - .enter() - .append('line') - .attr('class', 'guidelines') - .attr('x1', 0) - .attr('x2', lineChart.plot_width) - .attr('y1', function (d) { - return lineChart.y(d); - }) - .attr('y2', function (d) { - return lineChart.y(d); - }) - .attr('stroke', '#ccc') - .attr('stroke-dasharray', '2 2'); - } - - function drawNormalRange() { - var lineChart = this; - var upper = this.raw_data.map(function (m) { - return { studyday: m.studyday, value: m.uln }; - }); - var lower = this.raw_data - .map(function (m) { - return { studyday: m.studyday, value: m.lln }; - }) - .reverse(); - var normal_data = d3.merge([upper, lower]).filter(function (f) { - return f.value || f.value == 0; - }); - var drawnormal = d3.svg - .line() - .x(function (d) { - return lineChart.x(d.studyday); - }) - .y(function (d) { - return lineChart.y(d.value); - }); - var normalpath = this.svg - .append('path') - .datum(normal_data) - .attr({ - class: 'normalrange', - d: drawnormal, - fill: '#eee', - stroke: 'none' - }); - normalpath.moveToBack(); - } - - function addPointTitles() { - var config = this.edish.config; - var points = this.marks[1].circles; - points.select('title').remove(); - points.append('title').text(function (d) { - var raw = d.values.raw[0]; - var xvar = config.x.column; - var yvar = config.y.column; - var studyday_label = 'Study day: ' + raw.studyday + '\n', - visitn_label = raw.visitn ? 'Visit Number: ' + raw.visitn + '\n' : '', - visit_label = raw.visit ? 'Visit: ' + raw.visit + '\n' : '', - lab_label = raw.lab + ': ' + d3.format('0.3f')(raw.value); - return studyday_label + visit_label + visitn_label + lab_label; - }); - } - - function updatePointFill() { - var points = this.marks[1].circles; - points.attr('fill-opacity', function (d) { - var outlier = d.values.raw[0].outlier; - return outlier ? 1 : 0; - }); - } - - function init$2(d, edish) { - //layout the new cells on the DOM (slightly easier than using D3) - var summaryRow_node = this.parentNode; - var chartRow_node = document.createElement('tr'); - var chartCell_node = document.createElement('td'); - insertAfter(chartRow_node, summaryRow_node); - chartRow_node.appendChild(chartCell_node); - - //update the row styles - d3.select(chartRow_node) - .style('background', 'none') - .style('border-bottom', '0.5px solid black'); - - //layout the svg with D3 - var cellCount = d3.select(summaryRow_node).selectAll('td')[0].length; - var chartCell = d3.select(chartCell_node).attr('colspan', cellCount); - - //draw the chart - defaultSettings.colors = [d.color]; - var lineChart = webcharts.createChart(chartCell_node, defaultSettings); - lineChart.on('draw', function () { - setDomain$1.call(this); - }); - lineChart.edish = edish; - lineChart.on('resize', function () { - drawPopulationExtent.call(this); - drawNormalRange.call(this); - addPointTitles.call(this); - updatePointFill.call(this); - }); - lineChart.init(d.spark_data); - lineChart.row = chartRow_node; - return lineChart; - } - - function addSparkClick() { - var edish = this.edish; - if (this.data.raw.length > 0) { - this.tbody - .selectAll('tr') - .select('td.spark') - .on('click', function (d) { - if (d3.select(this).classed('minimized')) { - d3.select(this).classed('minimized', false); - d3.select(this.parentNode).style('border-bottom', 'none'); - - this.lineChart = init$2.call(this, d, edish); - d3.select(this) - .select('svg') - .style('display', 'none'); - - d3.select(this) - .select('span') - .html('△ Minimize Chart'); - } else { - d3.select(this).classed('minimized', true); - - d3.select(this.parentNode).style('border-bottom', '0.5px solid black'); - - d3.select(this) - .select('span') - .html('▽'); - - d3.select(this) - .select('svg') - .style('display', null); - - d3.select(this.lineChart.row).remove(); - this.lineChart.destroy(); - } - }); - } - } - - function addFootnote$1() { - var footnoteText = [ - 'The y-axis for each chart is set to the ' + - this.edish.config.measureBounds - .map(function (bound) { - var percentile = '' + Math.round(bound * 100); - var lastDigit = +percentile.substring(percentile.length - 1); - var text = - percentile + - ([0, 4, 5, 6, 7, 8, 9].indexOf(lastDigit) > -1 - ? 'th' - : lastDigit === 3 - ? 'rd' - : lastDigit === 2 - ? 'nd' - : 'st'); - return text; - }) - .join(' and ') + - " percentiles of the entire population's results for that measure. " + - 'Values outside the normal range are plotted as individual points. ' + - 'Click a sparkline to view a more detailed version of the chart.' - ]; - var footnotes = this.wrap.selectAll('span.footnote').data(footnoteText, function (d) { - return d; - }); - - footnotes - .enter() - .append('span') - .attr('class', 'footnote') - .style('font-size', '0.7em') - .style('padding-top', '0.1em') - .text(function (d) { - return d; - }); - - footnotes.exit().remove(); - } - - function addExtraMeasureToggle() { - var measureTable = this; - var chart = this.edish; - var config = chart.config; - - measureTable.wrap.selectAll('div.wc-controls').remove(); - - //check to see if there are extra measures in the MeasureTable - var specifiedMeasures = Object.keys(config.measure_values).map(function (e) { - return config.measure_values[e]; - }); - var tableMeasures = measureTable.data.raw.map(function (f) { - return f.key; - }); - - //if extra measure exist... - if (tableMeasures.length > specifiedMeasures.length) { - var extraRows = measureTable.table - .select('tbody') - .selectAll('tr') - .filter(function (f) { - return specifiedMeasures.indexOf(f.key) == -1; - }); - - //hide extra rows by default - extraRows.style('display', 'none'); - - //add a toggle - var toggleDiv = measureTable.wrap - .insert('div', '*') - .attr('class', 'wc-controls') - .append('div') - .attr('class', 'control-group'); - var extraCount = tableMeasures.length - specifiedMeasures.length; - toggleDiv - .append('span') - .attr('class', 'wc-control-label') - .style('display', 'inline-block') - .style('padding-right', '.3em') - .text( - 'Show ' + - extraCount + - ' additional measure' + - (extraCount == 1 ? '' : 's') + - ':' - ); - var toggle = toggleDiv.append('input').property('type', 'checkbox'); - toggle.on('change', function () { - var showRows = this.checked; - extraRows.style('display', showRows ? null : 'none'); - }); - } - } - - function drawMeasureTable(d) { - var nested = makeNestedData.call(this, d); - - //draw the measure table - this.measureTable.edish = this; - this.measureTable.on('draw', function () { - addSparkLines.call(this); - addSparkClick.call(this); - addExtraMeasureToggle.call(this); - addFootnote$1.call(this); - }); - this.measureTable.draw(nested); - } - - function makeParticipantHeader(d) { - var chart = this; - var wrap = this.participantDetails.header; - var raw = d.values.raw[0]; - var title = this.participantDetails.header - .append('h3') - .attr('class', 'id') - .html('Participant Details') - .style('border-top', '2px solid black') - .style('border-bottom', '2px solid black') - .style('padding', '.2em'); - - if (chart.config.participantProfileURL) { - title - .append('a') - .html('Full Participant Profile') - .attr('href', chart.config.participantProfileURL) - .style('font-size', '0.8em') - .style('padding-left', '1em'); - } - - title - .append('Button') - .text('Clear') - .style('margin-left', '1em') - .style('float', 'right') - .on('click', function () { - clearParticipantDetails.call(chart); - }); - - //show detail variables in a ul - var ul = this.participantDetails.header - .append('ul') - .style('list-style', 'none') - .style('padding', '0'); - - var lis = ul - .selectAll('li') - .data(chart.config.details) - .enter() - .append('li') - .style('', 'block') - .style('display', 'inline-block') - .style('text-align', 'center') - .style('padding', '0.5em'); - - lis.append('div') - .text(function (d) { - return d.label; - }) - .style('font-size', '0.8em'); - - lis.append('div').text(function (d) { - return raw[d.value_col]; - }); - - //show overall rRatio - var rratio_li = ul - .append('li') - .style('', 'block') - .style('display', 'inline-block') - .style('text-align', 'center') - .style('padding', '0.5em'); - - rratio_li - .append('div') - .html('R Ratio') - .style('font-size', '0.8em'); - - rratio_li.append('div').text(d3.format('0.2f')(raw.rRatio)); - - //show PALT` - if (raw.p_alt) { - var palt_li = ul - .append('li') - .style('', 'block') - .style('display', 'inline-block') - .style('text-align', 'center') - .style('padding', '0.5em'); - - palt_li - .append('div') - .html('PALT') - .style('font-size', '0.8em'); - - palt_li - .append('div') - .text(raw.p_alt.text_value) - .style('border-bottom', '1px dotted #999') - .style('cursor', 'pointer') - .on('click', function () { - wrap.select('p.footnote') - .attr('class', 'footnote') - .html(raw.p_alt.note); - }); - } - - //initialize empty footnote - wrap.append('p') - .attr('class', 'footnote') - .style('font-size', '0.7em') - .style('padding', '0.5em'); - } - - var defaultSettings$1 = { - max_width: 600, - x: { - column: null, - type: 'linear', - label: 'Study Day' - }, - y: defineProperty( - { - column: 'relative_uln', - type: 'linear', - label: null, // set in ../callbacks/onPreprocess - domain: null, - format: '.1f' - }, - 'domain', - [0, null] - ), - marks: [ - { - type: 'line', - per: [] - }, - { - type: 'circle', - radius: 4, - per: [] - } - ], - margin: { top: 20, bottom: 70 }, // bottom margin provides space for exposure plot - gridlines: 'xy', - color_by: null, - colors: ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#ffff33', '#a65628'], - aspect: 2 - }; - - var controlInputs$1 = [ - { - type: 'subsetter', - label: 'Select Labs', - value_col: null, - multiple: true - }, - { - type: 'dropdown', - label: 'Y-axis Display Type', - description: null, - option: 'displayLabel', - start: null, - values: null, - require: true - } - ]; - - function onLayout$1() { - var spaghetti = this; - var eDish = this.edish; - - //customize the display control - var displayControlWrap = spaghetti.controls.wrap - .selectAll('div') - .filter(function (controlInput) { - return controlInput.label === 'Y-axis Display Type'; - }); - - var displayControl = displayControlWrap.select('select'); - - //set the start value - var start_value = eDish.config.display_options.find(function (f) { - return f.value == eDish.config.display; - }).label; - - displayControl.selectAll('option').attr('selected', function (d) { - return d == start_value ? 'selected' : null; - }); - - displayControl.on('change', function (d) { - var currentLabel = this.value; - var currentValue = eDish.config.display_options.find(function (f) { - return f.label == currentLabel; - }).value; - spaghetti.config.y.column = currentValue; - spaghetti.draw(); - }); - } - - function onPreprocess$1() { - var config = this.config; - var unit = this.config.y.column == 'relative_uln' ? '[xULN]' : '[xBaseline]'; - config.y.label = 'Standardized Result ' + unit; - } - - function drawCutLine(d) { - //bit of a hack to make this work with paths and circles - var spaghetti = this; - var config = this.config; - var raw = d.values.raw ? d.values.raw[0] : d.values[0].values.raw[0]; - var cut = raw[config.y.column + '_cut']; - var param = raw[config.color_by]; - spaghetti.cutLine = spaghetti.svg - .append('line') - .attr('y1', spaghetti.y(cut)) - .attr('y2', spaghetti.y(cut)) - .attr('x1', 0) - .attr('x2', spaghetti.plot_width) - .attr('stroke', spaghetti.colorScale(param)) - .attr('stroke-dasharray', '3 3'); - spaghetti.cutLabel = spaghetti.svg - .append('text') - .attr('y', spaghetti.y(cut)) - .attr('dy', '-0.2em') - .attr('x', spaghetti.plot_width) - .attr('text-anchor', 'end') - .attr('alignment-baseline', 'baseline') - .attr('fill', spaghetti.colorScale(param)) - .text(d3.format('0.1f')(cut)); - } - - function addPointTitles$1() { - var spaghetti = this; - var config = this.edish.config; - var points = this.marks[1].circles; - points.select('title').remove(); - points.append('title').text(function (d) { - var raw = d.values.raw[0]; - var ylabel = spaghetti.config.displayLabel; - var yvar = spaghetti.config.y.column; - var studyday_label = 'Study day: ' + raw[config.studyday_col] + '\n', - visitn_label = config.visitn_col - ? 'Visit Number: ' + raw[config.visitn_col] + '\n' - : '', - visit_label = config.visit_col ? 'Visit: ' + raw[config.visit_col] + '\n' : '', - raw_label = - 'Raw ' + - raw[config.measure_col] + - ': ' + - d3.format('0.3f')(raw[config.value_col]) + - '\n', - adj_label = - 'Adjusted ' + raw[config.measure_col] + ': ' + d3.format('0.3f')(raw[yvar]); - return studyday_label + visit_label + visitn_label + raw_label + adj_label; - }); - } - - function addExposure() { - var context = this; - this.svg.select('.se-exposure-supergroup').remove(); - - //If exposure data exists, annotate exposures beneath x-axis. - if (this.edish.exposure.include) { - var supergroup = this.svg - .insert('g', '.supergroup') - .classed('se-exposure-supergroup', true); - var dy = 20; // offset from chart - var strokeWidth = 5; // width/diameter of marks - this.svg.selectAll('.x.axis .tick text').attr('dy', dy + strokeWidth * 3 + 'px'); // offset x-axis tick labels - - //top boundary line - supergroup.append('line').attr({ - x1: -this.margin.left, - y1: this.plot_height + dy - strokeWidth * 2, - x2: this.plot_width, - y2: this.plot_height + dy - strokeWidth * 2, - stroke: 'black', - 'stroke-opacity': 0.1 - }); - - //Exposure text - supergroup - .append('text') - .attr({ - x: -3, - y: this.plot_height + dy + strokeWidth, - 'text-anchor': 'end', - textLength: this.margin.left - 3 - }) - .text('Exposure'); - - //bottom boundary line - supergroup.append('line').attr({ - x1: -this.margin.left, - y1: this.plot_height + dy + strokeWidth * 2, - x2: this.plot_width, - y2: this.plot_height + dy + strokeWidth * 2, - stroke: 'black', - 'stroke-opacity': 0.1 - }); - - //Exposures - var groups = supergroup - .selectAll('g.se-exposure-group') - .data(this.exposure_data) - .enter() - .append('g') - .classed('se-exposure-group', true); - groups.each(function (d) { - var group = d3.select(this); - - //draw a line if exposure start and end dates are unequal - if ( - d[context.edish.config.exposure_stdy_col] !== - d[context.edish.config.exposure_endy_col] - ) { - group - .append('line') - .classed('se-exposure-line', true) - .attr({ - x1: function x1(d) { - return context.x(+d[context.edish.config.exposure_stdy_col]); - }, - y1: context.plot_height + dy, - x2: function x2(d) { - return context.x(+d[context.edish.config.exposure_endy_col]); - }, - y2: context.plot_height + dy, - stroke: 'black', - 'stroke-width': strokeWidth, - 'stroke-opacity': 0.25 - }) - .on('mouseover', function (d) { - this.setAttribute('stroke-width', strokeWidth * 2); - - //annotate a rectangle in the chart - group - .append('rect') - .classed('se-exposure-highlight', true) - .attr({ - x: function x(d) { - return context.x( - +d[context.edish.config.exposure_stdy_col] - ); - }, - y: 0, - width: function width(d) { - return ( - context.x(+d[context.edish.config.exposure_endy_col]) - - context.x(+d[context.edish.config.exposure_stdy_col]) - ); - }, - height: context.plot_height, - fill: 'black', - 'fill-opacity': 0.25 - }); - }) - .on('mouseout', function (d) { - this.setAttribute('stroke-width', strokeWidth); - - //remove rectangle from the chart - group.select('.se-exposure-highlight').remove(); - }) - .append('title') - .text( - 'Study Day: ' + - d[context.edish.config.exposure_stdy_col] + - '-' + - d[context.edish.config.exposure_endy_col] + - ' (' + - (+d[context.edish.config.exposure_endy_col] - - +d[context.edish.config.exposure_stdy_col] + - (+d[context.edish.config.exposure_endy_col] >= - +d[context.edish.config.exposure_stdy_col])) + - ' days)\nTreatment: ' + - d[context.edish.config.exposure_trt_col] + - '\nDose: ' + - d[context.edish.config.exposure_dose_col] + - ' ' + - d[context.edish.config.exposure_dosu_col] - ); - } - //draw a circle if exposure start and end dates are equal - else { - group - .append('circle') - .classed('se-exposure-circle', true) - .attr({ - cx: function cx(d) { - return context.x(+d[context.edish.config.exposure_stdy_col]); - }, - cy: context.plot_height + dy, - r: strokeWidth / 2, - fill: 'black', - 'fill-opacity': 0.25, - stroke: 'black', - 'stroke-opacity': 1 - }) - .on('mouseover', function (d) { - this.setAttribute('r', strokeWidth); - - //annotate a vertical line in the chart - group - .append('line') - .classed('se-exposure-highlight', true) - .attr({ - x1: context.x(+d[context.edish.config.exposure_stdy_col]), - y1: 0, - x2: context.x(+d[context.edish.config.exposure_stdy_col]), - y2: context.plot_height, - stroke: 'black', - 'stroke-width': 1, - 'stroke-opacity': 0.5, - 'stroke-dasharray': '3 1' - }); - }) - .on('mouseout', function (d) { - this.setAttribute('r', strokeWidth / 2); - - //remove vertical line from the chart - group.select('.se-exposure-highlight').remove(); - }) - .append('title') - .text( - 'Study Day: ' + - d[context.edish.config.exposure_stdy_col] + - '\nTreatment: ' + - d[context.edish.config.exposure_trt_col] + - '\nDose: ' + - d[context.edish.config.exposure_dose_col] + - ' ' + - d[context.edish.config.exposure_dosu_col] - ); - } - }); - } - } - - function onResize() { - var spaghetti = this; - var config = this.config; - - addPointTitles$1.call(this); - - //fill circles above the cut point - var y_col = this.config.y.column; - this.marks[1].circles - .attr('fill-opacity', function (d) { - return d.values.raw[0][y_col + '_flagged'] ? 1 : 0; - }) - .attr('fill-opacity', function (d) { - return d.values.raw[0][y_col + '_flagged'] ? 1 : 0; - }); - - //Show cut lines on mouseover - this.marks[1].circles - .on('mouseover', function (d) { - drawCutLine.call(spaghetti, d); - }) - .on('mouseout', function () { - spaghetti.cutLine.remove(); - spaghetti.cutLabel.remove(); - }); - - this.marks[0].paths - .on('mouseover', function (d) { - drawCutLine.call(spaghetti, d); - }) - .on('mouseout', function () { - spaghetti.cutLine.remove(); - spaghetti.cutLabel.remove(); - }); - - //annotate treatment exposure - addExposure.call(this); - - //embiggen clip-path so points aren't clipped - var radius = this.config.marks.find(function (mark) { - return mark.type === 'circle'; - }).radius; - this.svg - .select('.plotting-area') - .attr('width', this.plot_width + radius * 2 + 2) // plot width + circle radius * 2 + circle stroke width * 2 - .attr('height', this.plot_height + radius * 2 + 2) // plot height + circle radius * 2 + circle stroke width * 2 - .attr( - 'transform', - 'translate(-' + - (radius + 1) + // translate left circle radius + circle stroke width - ',-' + - (radius + 1) + // translate up circle radius + circle stroke width - ')' - ); - } - - function onDraw$1() { - var _this = this; - - var spaghetti = this; - var eDish = this.edish; - - //make sure x-domain includes the extent of the exposure data - if (this.edish.exposure.include) { - this.exposure_data = this.edish.exposure.data.filter(function (d) { - return d[_this.edish.config.id_col] === _this.edish.participantsSelected[0]; - }); - var extent = [ - d3.min(this.exposure_data, function (d) { - return +d[_this.edish.config.exposure_stdy_col]; - }), - d3.max(this.exposure_data, function (d) { - return +d[_this.edish.config.exposure_endy_col]; - }) - ]; - if (extent[0] < this.x_dom[0]) this.x_dom[0] = extent[0]; - if (extent[1] > this.x_dom[1]) this.x_dom[1] = extent[1]; - } - - //make sure y domain includes the current cut point for all measures - var max_value = d3.max(spaghetti.filtered_data, function (f) { - return f[spaghetti.config.y.column]; - }); - var max_cut = d3.max(spaghetti.filtered_data, function (f) { - return f[spaghetti.config.y.column + '_cut']; - }); - var y_max = d3.max([max_value, max_cut]); - spaghetti.config.y.domain = [0, y_max]; - spaghetti.y_dom = spaghetti.config.y.domain; - - //initialize the measureTable - if (spaghetti.config.firstDraw) { - drawMeasureTable.call(eDish, this.participant_data); - spaghetti.config.firstDraw = false; - } - } - - function init$3(d) { - var chart = this; //the full eDish object - var config = this.config; //the eDish config - var matches = d.values.raw[0].raw.filter(function (f) { - return f.key_measure; - }); - - if ('spaghetti' in chart) { - chart.spaghetti.destroy(); - } - - //sync settings - defaultSettings$1.x.column = config.studyday_col; - defaultSettings$1.color_by = config.measure_col; - defaultSettings$1.marks[0].per = [config.id_col, config.measure_col]; - defaultSettings$1.marks[1].per = [config.id_col, config.studyday_col, config.measure_col]; - defaultSettings$1.firstDraw = true; //only initailize the measure table on first draw - - //flag variables above the cut-off - matches.forEach(function (d) { - var measure = d[config['measure_col']]; - var label = Object.keys(config.measure_values).find(function (key) { - return config.measure_values[key] == measure; - }); - - d.relative_uln_cut = config.cuts[label].relative_uln; - d.relative_baseline_cut = config.cuts[label].relative_baseline; - - d.relative_uln_flagged = d.relative_uln >= d.relative_uln_cut; - d.relative_baseline_flagged = d.relative_baseline >= d.relative_baseline_cut; - }); - - //update the controls - var spaghettiElement = this.element + ' .participantDetails .spaghettiPlot .chart'; - - //Add y axis type options - controlInputs$1.find(function (f) { - return f.label == 'Y-axis Display Type'; - }).values = config.display_options.map(function (m) { - return m.label; - }); - - //sync parameter filter - controlInputs$1.find(function (f) { - return f.label == 'Select Labs'; - }).value_col = config.measure_col; - - var spaghettiControls = webcharts.createControls(spaghettiElement, { - location: 'top', - inputs: controlInputs$1 - }); - - //draw that chart - if (!this.exposure.include) delete defaultSettings$1.margin.bottom; // use default bottom margin when not plotting exposure - chart.spaghetti = webcharts.createChart( - spaghettiElement, - defaultSettings$1, - spaghettiControls - ); - - chart.spaghetti.edish = chart; //link the full eDish object - chart.spaghetti.participant_data = d; //include the passed data (used to initialize the measure table) - chart.spaghetti.on('layout', onLayout$1); - chart.spaghetti.on('preprocess', onPreprocess$1); - chart.spaghetti.on('draw', onDraw$1); - chart.spaghetti.on('resize', onResize); - chart.spaghetti.init(matches); - - //add informational footnote - chart.spaghetti.wrap - .append('div') - .attr('class', 'footnote') - .style('font-size', '0.7em') - .style('padding-top', '0.1em') - .text( - 'Points are filled for values above the current reference value. Mouseover a line to see the reference line for that lab.' - ); - } - - function drawCutLines() { - var chart = this; - var config = this.config; - - //line at R Ratio = 2 - chart.svg - .append('line') - .attr('y1', chart.y(2)) - .attr('y2', chart.y(2)) - .attr('x1', 0) - .attr('x2', chart.plot_width) - .attr('stroke', '#999') - .attr('stroke-dasharray', '3 3'); - - chart.svg - .append('text') - .attr('y', chart.y(2)) - .attr('dy', '-0.2em') - .attr('x', chart.plot_width) - .attr('text-anchor', 'end') - .attr('alignment-baseline', 'baseline') - .attr('fill', '#999') - .text('2'); - - //line at R Ratio = 5 - chart.svg - .append('line') - .attr('y1', chart.y(5)) - .attr('y2', chart.y(5)) - .attr('x1', 0) - .attr('x2', chart.plot_width) - .attr('stroke', '#999') - .attr('stroke-dasharray', '3 3'); - - chart.svg - .append('text') - .attr('y', chart.y(5)) - .attr('dy', '-0.2em') - .attr('x', chart.plot_width) - .attr('text-anchor', 'end') - .attr('alignment-baseline', 'baseline') - .attr('fill', '#999') - .text('5'); - } - - function updateClipPath() { - //embiggen clip-path so points aren't clipped - var radius = this.config.marks.find(function (mark) { - return mark.type === 'circle'; - }).radius; - this.svg - .select('.plotting-area') - .attr('width', this.plot_width + radius * 2 + 2) // plot width + circle radius * 2 + circle stroke width * 2 - .attr('height', this.plot_height + radius * 2 + 2) // plot height + circle radius * 2 + circle stroke width * 2 - .attr( - 'transform', - 'translate(-' + - (radius + 1) + // translate left circle radius + circle stroke width - ',-' + - (radius + 1) + // translate up circle radius + circle stroke width - ')' - ); - } - - function addPointTitles$2() { - var config = this.edish.config; - var points = this.marks[1].circles; - points.select('title').remove(); - points.append('title').text(function (d) { - var raw = d.values.raw[0]; - - var studyday_label = 'Study day: ' + raw[config.studyday_col] + '\n', - visitn_label = config.visitn_col - ? 'Visit Number: ' + raw[config.visitn_col] + '\n' - : '', - visit_label = config.visit_col ? 'Visit: ' + raw[config.visit_col] + '\n' : '', - rratio_label = 'R Ratio: ' + d3.format('0.2f')(raw.rRatio); - return studyday_label + visit_label + visitn_label + rratio_label; - }); - } - - function onResize$1() { - drawCutLines.call(this); - updateClipPath.call(this); - addPointTitles$2.call(this); - } - - function onDraw$2() { - var chart = this; - var config = this.config; - - //make sure y domain includes the current cut point for all measures - var max_value = d3.max(chart.filtered_data, function (f) { - return f[chart.config.y.column]; - }); - var max_cut = 5; - var y_max = d3.max([max_value, max_cut]); - chart.config.y.domain = [0, y_max]; - chart.y_dom = chart.config.y.domain; - } - - var defaultSettings$2 = { - max_width: 600, - x: { - column: 'day', - type: 'linear', - label: 'Study Day' - }, - y: { - column: 'rRatio', - type: 'linear', - label: 'R Ratio [(ALT/ULN) / (ALP/ULN)]', - format: '.1f', - domain: [0, null] - }, - marks: [ - { - type: 'line', - per: ['id'] - }, - { - type: 'circle', - radius: 4, - per: ['id', 'day'] - } - ], - margin: { top: 20 }, - gridlines: 'xy', - aspect: 2 - }; - - function init$4(d) { - var chart = this; //the full eDish object - var config = this.config; //the eDish config - var matches = d.values.raw[0].rRatio_raw; - - if ('rRatioChart' in chart) { - chart.rRatioChart.destroy(); - } - - //sync settings - defaultSettings$2.marks[0].per = [config.id_col]; - defaultSettings$2.marks[1].per = [config.id_col, config.studyday_col]; - - //draw that chart - var rrElement = this.element + ' .participantDetails .rrPlot .chart'; - chart.rRatioChart = webcharts.createChart(rrElement, defaultSettings$2); - - chart.rRatioChart.edish = chart; //link the full eDish object - - chart.rRatioChart.on('draw', onDraw$2); - chart.rRatioChart.on('resize', onResize$1); - - chart.rRatioChart.init(matches); - } - - function addPointClick() { - var chart = this; - var config = this.config; - var points = this.marks[0].circles; - - //add event listener to all participant level points - points.on('click', function (d) { - //Stop animation. - chart.svg.transition().duration(0); - chart.controls.studyDayPlayButton.datum({ state: 'play' }); - chart.controls.studyDayPlayButton.html('►'); - - // Reset the details view - clearParticipantDetails.call(chart, d); //clear the previous participant - chart.config.quadrants.table.wrap.style('display', 'none'); //hide the quadrant summary - - //Update chart object & trigger the participantsSelected event on the overall chart. - chart.participantsSelected = [d.key]; - chart.events.participantsSelected.data = chart.participantsSelected; - chart.wrap.node().dispatchEvent(chart.events.participantsSelected); - - //format the eDish chart - points - .attr('stroke', '#ccc') //set all points to gray - .attr('fill', 'white') - .classed('disabled', true); //disable mouseover while viewing participant details - - d3.select(this) - .attr('stroke', function (d) { - return chart.colorScale(d.values.raw[0][config.color_by]); - }) //highlight selected point - .attr('stroke-width', 3); - - //Add elements to the eDish chart - drawVisitPath.call(chart, d); //draw the path showing participant's pattern over time - drawRugs.call(chart, d, 'x'); - drawRugs.call(chart, d, 'y'); - - //draw the "detail view" for the clicked participant - chart.participantDetails.wrap.selectAll('*').style('display', null); - makeParticipantHeader.call(chart, d); - init$3.call(chart, d); //NOTE: the measure table is initialized from within the spaghettiPlot - init$4.call(chart, d); - }); - } - - function addPointTitles$3() { - var config = this.config; - var points = this.marks[0].circles; - points.select('title').remove(); - points.append('title').text(function (d) { - var xvar = config.x.column; - var yvar = config.y.column; - var raw = d.values.raw[0], - xLabel = - config.x.label + - ': ' + - d3.format('0.2f')(raw[xvar]) + - ' @ Day ' + - raw[xvar + '_' + config.studyday_col], - yLabel = - config.y.label + - ': ' + - d3.format('0.2f')(raw[yvar]) + - ' @ Day ' + - raw[yvar + '_' + config.studyday_col], - dayDiff = raw['day_diff'] + ' days apart', - idLabel = 'Participant ID: ' + raw[config.id_col], - rRatioLabel = config.r_ratio_filter - ? '\n' + 'R Ratio: ' + d3.format('0.2f')(raw.rRatio) - : ''; - return idLabel + rRatioLabel + '\n' + xLabel + '\n' + yLabel + '\n' + dayDiff; - }); - } - - function setPointSize() { - var _this = this; - - var chart = this; - var config = this.config; - var points = this.marks[0].circles; - - //create the scale - var base_size = config.marks[0].radius || config.flex_point_size; - var max_size = base_size * 5; - var small_size = base_size / 2; - - if (config.point_size != 'Uniform') { - var sizeValues_all = d3.merge( - chart.raw_data.map(function (m) { - return m[config.point_size + '_raw']; - }) - ); - var sizeDomain_all = d3.extent(sizeValues_all, function (f) { - return f.value; - }); - var sizeDomain_max = d3.extent( - chart.raw_data.map(function (m) { - return m[config.point_size]; - }) - ); - var sizeDomain_rRatio = [ - 0, - d3.max(this.raw_data, function (d) { - return d.rRatio_max; - }) - ]; - var sizeDomain = - config.point_size == 'rRatio' - ? sizeDomain_rRatio - : config.plot_max_values - ? sizeDomain_max - : sizeDomain_all; - chart.sizeScale = d3.scale - .linear() - .range([base_size, max_size]) - .domain(sizeDomain); - } - - //TODO: draw a legend (coming later?) - - //set the point radius - points - .transition() - .attr('r', function (d) { - // console.log(config.point_size); - var raw = d.values.raw[0]; - // console.log(raw); - if (raw.outOfRange) { - return small_size; - } else if (config.point_size == 'Uniform') { - return base_size; - } else { - return chart.sizeScale(raw[config.point_size]); - } - }) - .attr('cx', function (d) { - return _this.x(d.values.x); - }) - .attr('cy', function (d) { - return _this.y(d.values.y); - }); - } - - function setPointOpacity() { - var config = this.config; - var points = this.marks[0].circles; - - points.attr('fill-opacity', function (d) { - var raw = d.values.raw[0]; - if (config.plot_max_values) { - // if viewing max values, fill based on time window - return raw.day_diff <= config.visit_window ? 0.5 : 0; - } else { - //fill points after participant's first day - return config.plot_day < raw.day_range[0] ? 0 : 0.5; - } - }); - } - - // Reposition any exisiting participant marks when the chart is resized - function updateParticipantMarks() { - var chart = this; - var config = this.config; - - //reposition participant visit path - var myNewLine = d3.svg - .line() - .x(function (d) { - return chart.x(d.x); - }) - .y(function (d) { - return chart.y(d.y); - }); - - chart.visitPath - .select('path') - .transition() - .attr('d', myNewLine); - - //reposition participant visit circles and labels - chart.visitPath - .selectAll('g.visit-point') - .select('circle') - .transition() - .attr('cx', function (d) { - return chart.x(d.x); - }) - .attr('cy', function (d) { - return chart.y(d.y); - }); - - chart.visitPath - .selectAll('g.visit-point') - .select('text.participant-visits') - .transition() - .attr('x', function (d) { - return chart.x(d.x); - }) - .attr('y', function (d) { - return chart.y(d.y); - }); - - //reposition axis rugs - chart.x_rug - .selectAll('text') - .transition() - .attr('x', function (d) { - return chart.x(d[config.display]); - }) - .attr('y', function (d) { - return chart.y(chart.y.domain()[0]); - }); - - chart.y_rug - .selectAll('text') - .transition() - .attr('x', function (d) { - return chart.x(chart.x.domain()[0]); - }) - .attr('y', function (d) { - return chart.y(d[config.display]); - }); - } - - function customizePoints() { - addPointMouseover.call(this); - addPointClick.call(this); - addPointTitles$3.call(this); - formatPoints.call(this); - setPointSize.call(this); - setPointOpacity.call(this); - updateParticipantMarks.call(this); - } - - function drawQuadrants() { - var _this = this; - - var config = this.config; - var x_var = this.config.x.column; - var y_var = this.config.y.column; - - var x_cut = this.config.cuts[x_var][config.display]; - var y_cut = this.config.cuts[y_var][config.display]; - - //position for cut-point lines - this.cut_lines.lines - .filter(function (d) { - return d.dimension == 'x'; - }) - .attr('x1', this.x(x_cut)) - .attr('x2', this.x(x_cut)) - .attr('y1', this.plot_height) - .attr('y2', 0); - - this.cut_lines.lines - .filter(function (d) { - return d.dimension == 'y'; - }) - .attr('x1', 0) - .attr('x2', this.plot_width) - .attr('y1', function (d) { - return _this.y(y_cut); - }) - .attr('y2', function (d) { - return _this.y(y_cut); - }); - - this.cut_lines.backing - .filter(function (d) { - return d.dimension == 'x'; - }) - .attr('x1', this.x(x_cut)) - .attr('x2', this.x(x_cut)) - .attr('y1', this.plot_height) - .attr('y2', 0); - - this.cut_lines.backing - .filter(function (d) { - return d.dimension == 'y'; - }) - .attr('x1', 0) - .attr('x2', this.plot_width) - .attr('y1', function (d) { - return _this.y(y_cut); - }) - .attr('y2', function (d) { - return _this.y(y_cut); - }); - - //position labels - this.quadrant_labels.g.attr('display', null); //show labels if they're hidden - this.quadrant_labels.g - .select('text.upper-right') - .attr('x', this.plot_width) - .attr('y', 0); - - this.quadrant_labels.g - .select('text.upper-left') - .attr('x', 0) - .attr('y', 0); - - this.quadrant_labels.g - .select('text.lower-right') - .attr('x', this.plot_width) - .attr('y', this.plot_height); - - this.quadrant_labels.g - .select('text.lower-left') - .attr('x', 0) - .attr('y', this.plot_height); - - this.quadrant_labels.text.text(function (d) { - return d.label + ' (' + d.percent + ')'; - }); - } - - function addAxisLabelTitles() { - var chart = this; - var config = this.config; - - var details = - config.display == 'relative_uln' - ? 'Values are plotted as multiples of the upper limit of normal for the measure.' - : config.display == 'relative_baseline' - ? "Values are plotted as multiples of the partipant's baseline value for the measure." - : config.display == 'absolute' - ? ' Values are plotted using the raw units for the measure.' - : null; - - var axisLabels = chart.svg - .selectAll('.axis') - .select('.axis-title') - .select('tspan') - .remove(); - - var axisLabels = chart.svg - .selectAll('.axis') - .select('.axis-title') - .append('tspan') - .html(function (d) { - //var current = d3.select(this).text(); - return ' ⓘ'; - }) - .attr('font-size', '0.8em') - .style('cursor', 'help') - .append('title') - .text(details); - } - - function toggleLegend() { - var hideLegend = this.config.color_by == 'NONE'; - this.wrap.select('.legend').style('display', hideLegend ? 'None' : 'block'); - } - - function dragStarted() { - var dimension = d3.select(this).classed('x') ? 'x' : 'y'; - var chart = d3.select(this).datum().chart; - - d3.select(this) - .select('line.cut-line') - .attr('stroke-width', '2') - .attr('stroke-dasharray', '2,2'); - - chart.quadrant_labels.g.style('display', 'none'); - } - - function dragged() { - var chart = d3.select(this).datum().chart; - - var x = d3.event.dx; - var y = d3.event.dy; - - var line = d3.select(this).select('line.cut-line'); - var lineBack = d3.select(this).select('line.cut-line-backing'); - - var dimension = d3.select(this).classed('x') ? 'x' : 'y'; - - // Update the line properties - var attributes = { - x1: Math.max(0, parseInt(line.attr('x1')) + (dimension == 'x' ? x : 0)), - x2: Math.max(0, parseInt(line.attr('x2')) + (dimension == 'x' ? x : 0)), - y1: Math.min(chart.plot_height, parseInt(line.attr('y1')) + (dimension == 'y' ? y : 0)), - y2: Math.min(chart.plot_height, parseInt(line.attr('y2')) + (dimension == 'y' ? y : 0)) - }; - - line.attr(attributes); - lineBack.attr(attributes); - - var rawCut = line.attr(dimension + '1'); - var current_cut = +d3.format('0.1f')(chart[dimension].invert(rawCut)); - - //update the cut control in real time - chart.controls.wrap - .selectAll('div.control-group') - .filter(function (f) { - return f.description - ? f.description.toLowerCase() == dimension + '-axis reference line' - : false; - }) - .select('input') - .node().value = current_cut; - var measure = chart.config[dimension].column; - chart.config.cuts[measure][chart.config.display] = current_cut; - } - - function dragEnded() { - var chart = d3.select(this).datum().chart; - - d3.select(this) - .select('line.cut-line') - .attr('stroke-width', '1') - .attr('stroke-dasharray', '5,5'); - chart.quadrant_labels.g.style('display', null); - - //redraw the chart (updates the needed cutpoint settings and quadrant annotations) - chart.draw(); - } - - // credit to https://bl.ocks.org/dimitardanailov/99950eee511375b97de749b597147d19 - - function init$5() { - var drag = d3.behavior - .drag() - .origin(function (d) { - return d; - }) - .on('dragstart', dragStarted) - .on('drag', dragged) - .on('dragend', dragEnded); - - this.cut_lines.wrap.moveToFront(); - this.cut_lines.g.call(drag); - } - - function addBoxPlot( - svg, - results, - height, - width, - domain, - boxPlotWidth, - boxColor, - boxInsideColor, - fmt, - horizontal, - log - ) { - //set default orientation to "horizontal" - var horizontal = horizontal == undefined ? true : horizontal; - - //make the results numeric and sort - var results = results - .map(function (d) { - return +d; - }) - .sort(d3.ascending); - - //set up d3.scales - if (horizontal) { - var y = log ? d3.scale.log() : d3.scale.linear(); - y.range([height, 0]).domain(domain); - var x = d3.scale.linear().range([0, width]); - } else { - var x = log ? d3.scale.log() : d3.scale.linear(); - x.range([0, width]).domain(domain); - var y = d3.scale.linear().range([height, 0]); - } - - 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]); - } - - var boxplot = svg - .append('g') - .attr('class', 'boxplot') - .datum({ values: results, probs: probs }); - - //draw rectangle from q1 to q3 - var box_x = horizontal ? x(0.5 - boxPlotWidth / 2) : x(probs[1]); - var box_width = horizontal - ? x(0.5 + boxPlotWidth / 2) - x(0.5 - boxPlotWidth / 2) - : x(probs[3]) - x(probs[1]); - var box_y = horizontal ? y(probs[3]) : y(0.5 + boxPlotWidth / 2); - var box_height = horizontal - ? -y(probs[3]) + y(probs[1]) - : y(0.5 - boxPlotWidth / 2) - y(0.5 + boxPlotWidth / 2); - - boxplot - .append('rect') - .attr('class', 'boxplot fill') - .attr('x', box_x) - .attr('width', box_width) - .attr('y', box_y) - .attr('height', box_height) - .style('fill', boxColor); - - //draw dividing lines at d3.median, 95% and 5% - var iS = [0, 2, 4]; - var iSclass = ['', 'd3.median', '']; - var iSColor = [boxColor, boxInsideColor, boxColor]; - for (var i = 0; i < iS.length; i++) { - boxplot - .append('line') - .attr('class', 'boxplot ' + iSclass[i]) - .attr('x1', horizontal ? x(0.5 - boxPlotWidth / 2) : x(probs[iS[i]])) - .attr('x2', horizontal ? x(0.5 + boxPlotWidth / 2) : x(probs[iS[i]])) - .attr('y1', horizontal ? y(probs[iS[i]]) : y(0.5 - boxPlotWidth / 2)) - .attr('y2', horizontal ? y(probs[iS[i]]) : y(0.5 + boxPlotWidth / 2)) - .style('fill', iSColor[i]) - .style('stroke', iSColor[i]); - } - - //draw lines from 5% to 25% and from 75% to 95% - var iS = [[0, 1], [3, 4]]; - for (var i = 0; i < iS.length; i++) { - boxplot - .append('line') - .attr('class', 'boxplot') - .attr('x1', horizontal ? x(0.5) : x(probs[iS[i][0]])) - .attr('x2', horizontal ? x(0.5) : x(probs[iS[i][1]])) - .attr('y1', horizontal ? y(probs[iS[i][0]]) : y(0.5)) - .attr('y2', horizontal ? y(probs[iS[i][1]]) : y(0.5)) - .style('stroke', boxColor); - } - - boxplot - .append('circle') - .attr('class', 'boxplot d3.mean') - .attr('cx', horizontal ? x(0.5) : x(d3.mean(results))) - .attr('cy', horizontal ? y(d3.mean(results)) : y(0.5)) - .attr('r', horizontal ? x(boxPlotWidth / 3) : y(1 - boxPlotWidth / 3)) - .style('fill', boxInsideColor) - .style('stroke', boxColor); - - boxplot - .append('circle') - .attr('class', 'boxplot d3.mean') - .attr('cx', horizontal ? x(0.5) : x(d3.mean(results))) - .attr('cy', horizontal ? y(d3.mean(results)) : y(0.5)) - .attr('r', horizontal ? x(boxPlotWidth / 6) : y(1 - boxPlotWidth / 6)) - .style('fill', boxColor) - .style('stroke', 'None'); - - var formatx = fmt ? d3.format(fmt) : d3.format('.2f'); - - boxplot - .selectAll('.boxplot') - .append('title') - .text(function (d) { - return ( - 'N = ' + - d.values.length + - '\n' + - 'Min = ' + - d3.min(d.values) + - '\n' + - '5th % = ' + - formatx(d3.quantile(d.values, 0.05)) + - '\n' + - 'Q1 = ' + - formatx(d3.quantile(d.values, 0.25)) + - '\n' + - 'Median = ' + - formatx(d3.median(d.values)) + - '\n' + - 'Q3 = ' + - formatx(d3.quantile(d.values, 0.75)) + - '\n' + - '95th % = ' + - formatx(d3.quantile(d.values, 0.95)) + - '\n' + - 'Max = ' + - d3.max(d.values) + - '\n' + - 'Mean = ' + - formatx(d3.mean(d.values)) + - '\n' + - 'StDev = ' + - formatx(d3.deviation(d.values)) - ); - }); - } - - function init$6() { - // Draw box plots - this.svg.selectAll('g.yMargin').remove(); - this.svg.selectAll('g.xMargin').remove(); - - // Y-axis box plot - var yValues = this.current_data.map(function (d) { - return d.values.y; - }); - var ybox = this.svg.append('g').attr('class', 'yMargin'); - addBoxPlot( - ybox, - yValues, - this.plot_height, - 1, - this.y_dom, - 10, - '#bbb', - 'white', - '0.2f', - true, - this.config.y.type == 'log' - ); - ybox.select('g.boxplot').attr( - 'transform', - 'translate(' + (this.plot_width + this.config.margin.right / 2) + ',0)' - ); - - //X-axis box plot - var xValues = this.current_data.map(function (d) { - return d.values.x; - }); - var xbox = this.svg.append('g').attr('class', 'xMargin'); - addBoxPlot( - xbox, //svg element - xValues, //values - 1, //height - this.plot_width, //width - this.x_dom, //domain - 10, //box plot width - '#bbb', //box color - 'white', //detail color - '0.2f', //format - false, // horizontal? - this.config.y.type == 'log' // log? - ); - xbox.select('g.boxplot').attr( - 'transform', - 'translate(0,' + -(this.config.margin.top / 2) + ')' - ); - } - - function adjustTicks() { - this.svg - .selectAll('.x.axis .tick text') - .attr({ - transform: 'rotate(-45)', - dx: -10, - dy: 10 - }) - .style('text-anchor', 'end'); - } - - function updateTimingFootnote() { - var config = this.config; - if (config.plot_max_values) { - var windowText = - config.visit_window == 0 - ? 'on the same day' - : config.visit_window == 1 - ? 'within 1 day' - : 'within ' + config.visit_window + ' days'; - var timingFootnote = - ' Points where maximum ' + - config.measure_values[config.x.column] + - ' and ' + - config.measure_values[config.y.column] + - ' values were collected ' + - windowText + - ' are filled, others are empty.'; - - this.footnote.timing.text(timingFootnote); - } else { - var timingFootnote = - "Small points are drawn when the selected day occurs outside of the participant's study enrollment; either the first collected measures (empty circle) or last collected measures (filled circle) for the participant are plotted for these points."; - this.footnote.timing.text(timingFootnote); - } - } - - function onResize$2() { - //add maximum point interactivity, custom title and formatting - customizePoints.call(this); - - //draw the quadrants and add drag interactivity - updateSummaryTable.call(this); - drawQuadrants.call(this); - init$5.call(this); - - // hide the legend if no group options are given - toggleLegend.call(this); - - // add boxplots - init$6.call(this); - - //axis formatting - adjustTicks.call(this); - addAxisLabelTitles.call(this); - - //add timing footnote - updateTimingFootnote.call(this); - } - - function onDestroy() { - this.emptyChartWarning.remove(); - } - - var callbacks = { - onInit: onInit, - onLayout: onLayout, - onPreprocess: onPreprocess, - onDataTransform: onDataTransform, - onDraw: onDraw, - onResize: onResize$2, - onDestroy: onDestroy - }; - - function init$7() { - var lb = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; - var ex = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; - - //const data = mergeData(lb,ex); - this.data = { - lb: lb, - ex: ex - }; - this.chart.exposure = { - include: Array.isArray(ex) && ex.length, - data: ex - }; - this.chart.init(lb); - } - - function destroy() { - this.chart.destroy(); - } - - function hepexplorer(element, settings) { - var initial_settings = clone(settings); - var defaultSettings = configuration.settings(); - var controlInputs = configuration.controlInputs(); - var mergedSettings = Object.assign({}, defaultSettings, settings); - var syncedSettings = configuration.syncSettings(mergedSettings); - var syncedControlInputs = configuration.syncControlInputs(controlInputs, syncedSettings); - var controls = webcharts.createControls(element, { - location: 'top', - inputs: syncedControlInputs - }); - var chart = webcharts.createChart(element, syncedSettings, controls); - - chart.element = element; - chart.initial_settings = initial_settings; - - //Define callbacks. - for (var callback in callbacks) { - chart.on(callback.substring(2).toLowerCase(), callbacks[callback]); - } - var hepexplorer = { - element: element, - settings: settings, - chart: chart, - init: init$7, - destroy: destroy - }; - - return hepexplorer; - } - - return hepexplorer; -}); diff --git a/inst/htmlwidgets/lib/paneled-outlier-explorer-1.1.4/paneledOutlierExplorer.js b/inst/htmlwidgets/lib/paneled-outlier-explorer-1.1.4/paneledOutlierExplorer.js deleted file mode 100644 index 1899da22..00000000 --- a/inst/htmlwidgets/lib/paneled-outlier-explorer-1.1.4/paneledOutlierExplorer.js +++ /dev/null @@ -1,2101 +0,0 @@ -(function(global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' - ? (module.exports = factory(require('d3'), require('webcharts'))) - : typeof define === 'function' && define.amd - ? define(['d3', 'webcharts'], factory) - : (global.paneledOutlierExplorer = factory(global.d3, global.webCharts)); -})(this, function(d3, webcharts) { - 'use strict'; - - if (typeof Object.assign != 'function') { - (function() { - Object.assign = function(target) { - if (target === undefined || target === null) { - throw new TypeError('Cannot convert undefined or null to object'); - } - - var output = Object(target); - for (var index = 1; index < arguments.length; index++) { - var source = arguments[index]; - if (source !== undefined && source !== null) { - for (var nextKey in source) { - if (source.hasOwnProperty(nextKey)) { - output[nextKey] = source[nextKey]; - } - } - } - } - return output; - }; - })(); - } - - if (!Array.prototype.find) { - Object.defineProperty(Array.prototype, 'find', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, "length")). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return kValue. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return kValue; - } - // e. Increase k by 1. - k++; - } - - // 7. Return undefined. - return undefined; - } - }); - } - - (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 defineStyles() { - var styles = [ - /***--------------------------------------------------------------------------------------\ - Controls - \--------------------------------------------------------------------------------------***/ - - '#paneled-outlier-explorer #controls-header {' + - ' margin: 0;' + - ' overflow: hidden;' + - ' background-color: #333;' + - ' width: 24%;' + - ' float: left;' + - ' font-size: 150%;' + - ' display: block;' + - ' color: white;' + - ' padding: 14px 16px;' + - ' box-sizing: border-box;' + - '}', - '#paneled-outlier-explorer #left-side {' + - ' width: 24%;' + - ' float: left;' + - '}', - '#paneled-outlier-explorer #left-side > * {' + - ' width: 100%;' + - ' display: inline-block;' + - '}', - '#paneled-outlier-explorer #left-side .wc-controls {' + - ' padding: 10px 0;' + - '}', - '#paneled-outlier-explorer #left-side .wc-controls .control-group {' + - ' float: left;' + - ' clear: left;' + - ' margin: 0 0 2px 0;' + - ' border: 1px solid white;' + - '}', - '#paneled-outlier-explorer #left-side .wc-controls .control-group.inlier-highlighting {' + - ' background-color: rgba(0,255,0,.05);' + - ' border: 1px solid green;' + - '}', - '#paneled-outlier-explorer #left-side .wc-controls .control-group > * {' + - ' display: inline-block;' + - ' margin-left: 3px;' + - '}', - '#paneled-outlier-explorer #left-side #measure-list-container {' + - ' padding:0' + - '}', - '#paneled-outlier-explorer #left-side #measure-list-container #measure-list-header {' + - ' font-size: 150%;' + - ' border-top: 1px solid lightgray;' + - ' font-weight: lighter;' + - ' padding: 14px 0;' + - '}', - '#paneled-outlier-explorer #left-side #measure-list-container #measure-list-checkbox {' + - ' margin: 5px;' + - '}', - '#paneled-outlier-explorer #left-side #measure-list-container #measure-list {' + - ' list-style-type: none;' + - ' font-weight: lighter;' + - '}', - '#paneled-outlier-explorer #left-side #measure-list-container .measure-item {' + - '}', - '#paneled-outlier-explorer #left-side #measure-list-container .measure-item-container {' + - '}', - '#paneled-outlier-explorer #left-side #measure-list-container .measure-checkbox {' + - ' margin: 5px;' + - '}', - - /***--------------------------------------------------------------------------------------\ - Navigation - \--------------------------------------------------------------------------------------***/ - - '#paneled-outlier-explorer ul#navigation-bar {' + - ' list-style-type: none;' + - ' margin: 0;' + - ' padding: 0;' + - ' overflow: hidden;' + - ' background-color: #333;' + - ' width: 75%;' + - ' float: right;' + - '}', - '#paneled-outlier-explorer ul#navigation-bar li {' + - ' display: block;' + - ' color: white;' + - ' text-align: center;' + - '}', - '#paneled-outlier-explorer ul#navigation-bar li.navigation {' + - ' float: left;' + - ' cursor: pointer;' + - ' font-size: 24px;' + - ' padding: 14px 16px;' + - ' text-decoration: none;' + - '}', - '#paneled-outlier-explorer ul#navigation-bar li#population-annotation {' + - ' float: right;' + - ' font-size: 16px;' + - ' padding: 18px 16px;' + - '}', - '#paneled-outlier-explorer ul#navigation-bar li.navigation.active {' + - ' background-color: #111;' + - '}', - '#paneled-outlier-explorer ul#navigation-bar li.navigation:hover {' + - ' background-color: #111;' + - '}', - '#paneled-outlier-explorer ul#navigation-bar li.navigation#Listing-nav.brushed {' + - ' color: orange;' + - '}', - - /***--------------------------------------------------------------------------------------\ - Charts - \--------------------------------------------------------------------------------------***/ - - '#paneled-outlier-explorer div.wc-layout.wc-small-multiples#Charts {' + - ' width: 75%;' + - ' float: right;' + - ' padding-top: 10px;' + - '}', - '#paneled-outlier-explorer div.wc-layout.wc-small-multiples#Charts > div.wc-chart {' + - ' padding: 0 1em 0 0;' + - '}', - '#paneled-outlier-explorer div.wc-layout.wc-small-multiples#Charts > div.wc-chart.expanded {' + - ' width: 100%;' + - ' }', - '#paneled-outlier-explorer div.wc-layout.wc-small-multiples#Charts > div.wc-chart .wc-chart-title {' + - ' text-align: left;' + - ' font-size: .9em;' + - '}', - '#paneled-outlier-explorer div.wc-layout.wc-small-multiples#Charts > div.wc-chart .chart-button {' + - ' float: right;' + - ' cursor: pointer;' + - ' border: 1px solid black;' + - ' border-radius: 3px;' + - ' padding: 0px 3px 1px 3px;' + - ' font-size: 75%;' + - ' margin-left: 5px;' + - ' visibility: hidden;' + - '}', - '#paneled-outlier-explorer div.wc-layout.wc-small-multiples#Charts > div.wc-chart .chart-button:hover {' + - ' background: black;' + - ' color: white;' + - '}', - '#paneled-outlier-explorer div.wc-layout.wc-small-multiples#Charts > div.wc-chart text.no-data {' + - ' fill: red;' + - ' font-size: 0.8em;' + - '}', - '#paneled-outlier-explorer .normal-range {' + - ' fill: green;' + - ' fill-opacity: .05;' + - ' stroke: green;' + - ' stroke-opacity: 1;' + - '}', - '#paneled-outlier-explorer .n-inlier {' + ' cursor: help;' + '}', - '#paneled-outlier-explorer .n-inlier text {' + - ' fill: green;' + - ' text-anchor: end;' + - ' font-size: 10px;' + - ' font-weight: bold;' + - '}', - '#paneled-outlier-explorer .n-inlier rect {' + - ' fill: green;' + - ' fill-opacity: .05;' + - ' stroke: green;' + - ' stroke-opacity: 1;' + - '}', - '#paneled-outlier-explorer .single-point {' + - ' stroke-width: 3;' + - ' stroke-opacity: 1;' + - '}', - - /***--------------------------------------------------------------------------------------\ - Listing - \--------------------------------------------------------------------------------------***/ - - '#paneled-outlier-explorer div.wc-chart#Listing {' + - ' width: 75%;' + - ' float: right;' + - ' padding-top: 10px;' + - ' overflow-x: scroll;' + - '}', - '#paneled-outlier-explorer div.wc-chart#Listing table {' + - ' width: 100%;' + - ' display: table;' + - '}', - - /***--------------------------------------------------------------------------------------\ - General styles - \--------------------------------------------------------------------------------------***/ - - '#paneled-outlier-explorer .hidden {' + ' display: none !important;' + '}', - '#paneled-outlier-explorer path.brushed {' + - ' stroke: orange;' + - ' stroke-width: 3px;' + - ' stroke-opacity: 1;' + - '}', - '#paneled-outlier-explorer tr.brushed {' + - ' border: 2px solid orange !important;' + - '}' - ], - style = document.createElement('style'); - style.type = 'text/css'; - style.innerHTML = styles.join('\n'); - - document.getElementsByTagName('head')[0].appendChild(style); - } - - var _typeof = - typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' - ? function(obj) { - return typeof obj; - } - : function(obj) { - return obj && - typeof Symbol === 'function' && - obj.constructor === Symbol && - obj !== Symbol.prototype - ? 'symbol' - : typeof obj; - }; - - function clone(obj) { - var copy = void 0; - - //boolean, number, string, null, undefined - if ('object' != (typeof obj === 'undefined' ? 'undefined' : _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.'); - } - - var rendererSettings = { - measure_col: 'TEST', - time_cols: [ - { - value_col: 'VISIT', - type: 'ordinal', - order: null, - order_col: 'VISITNUM', - label: 'Visit', - rotate_tick_labels: true, - vertical_space: 75 - }, - { - value_col: 'DY', - type: 'linear', - order: null, - order_col: 'DY', - label: 'Study Day', - rotate_tick_labels: false, - vertical_space: 0 - } - ], - value_col: 'STRESN', - id_col: 'USUBJID', - unit_col: 'STRESU', - lln_col: 'STNRLO', - uln_col: 'STNRHI', - measures: null, - filters: null, - multiples_sizing: { - width: 350, - height: 175 - }, - inliers: false, - normal_range_method: 'LLN-ULN', - normal_range_sd: 1.96, - normal_range_quantile_low: 0.05, - normal_range_quantile_high: 0.95, - visits_without_data: false, - unscheduled_visits: false, - unscheduled_visit_pattern: '/unscheduled|early termination/i', - unscheduled_visit_values: null // takes precedence over unscheduled_visit_pattern visits_without_data: false, - }; - - var webchartsSettings = { - x: { - type: null, // sync to [ time_cols[0].type ] - column: null, // sync to [ time_cols[0].value_col ] - label: '' // sync to [ time_cols[0].label ] - }, - y: { - type: 'linear', - column: null, // sync to [ value_col ] - label: '' - }, - marks: [ - { - type: 'line', - per: null, // sync to [ id_col ] and [ measure_col ] - attributes: { - 'stroke-width': 1, - 'stroke-opacity': 0.2, - stroke: 'black' - } - } - ], - resizable: false, - scale_text: false, - margin: { - bottom: 0, - left: 50 - }, - gridlines: 'xy' - }; - - var defaultSettings = Object.assign(rendererSettings, webchartsSettings); - - function syncSettings(settings) { - var syncedSettings = clone(settings); - syncedSettings.x.type = settings.time_cols[0].type; - syncedSettings.x.order = settings.time_cols[0].order; - syncedSettings.x.column = settings.time_cols[0].value_col; - syncedSettings.x.rotate_tick_labels = settings.time_cols[0].rotate_tick_labels; - syncedSettings.y.column = settings.value_col; - syncedSettings.marks[0].per = [settings.id_col, settings.measure_col]; - syncedSettings.width = syncedSettings.multiples_sizing.width; - syncedSettings.height = syncedSettings.multiples_sizing.height; - - //handle a string arguments to array settings - var array_settings = ['filters']; - array_settings.forEach(function(s) { - if (!(syncedSettings[s] instanceof Array)) - syncedSettings[s] = - typeof syncedSettings[s] === 'string' ? [syncedSettings[s]] : []; - }); - - //Convert unscheduled_visit_pattern from string to regular expression. - if ( - typeof syncedSettings.unscheduled_visit_pattern === 'string' && - syncedSettings.unscheduled_visit_pattern !== '' - ) { - var flags = settings.unscheduled_visit_pattern.replace(/.*?\/([gimy]*)$/, '$1'), - pattern = settings.unscheduled_visit_pattern.replace( - new RegExp('^/(.*?)/' + flags + '$'), - '$1' - ); - syncedSettings.unscheduled_visit_regex = new RegExp(pattern, flags); - } - - return syncedSettings; - } - - var controlInputs = [ - { - type: 'dropdown', - label: 'X-axis', - option: 'x.column', - require: true - }, - { - type: 'checkbox', - label: 'Visits without data', - option: 'visits_without_data' - }, - { - type: 'checkbox', - label: 'Unscheduled visits', - option: 'unscheduled_visits' - }, - { - type: 'checkbox', - label: 'Normal range inliers', - option: 'inliers' - }, - { - type: 'dropdown', - label: 'Normal range method', - option: 'normal_range_method', - values: ['None', 'LLN-ULN', 'Standard Deviation', 'Quantiles'], - require: true - }, - { - type: 'number', - label: 'Number of standard deviations', - option: 'normal_range_sd' - }, - { - type: 'number', - label: 'Lower quantile', - option: 'normal_range_quantile_low' - }, - { - type: 'number', - label: 'Upper quantile', - option: 'normal_range_quantile_high' - } - ]; - - function syncControlInputs(controlInputs, settings) { - var syncedControlInputs = clone(controlInputs); - - syncedControlInputs.filter(function(controlInput) { - return controlInput.label === 'X-axis'; - })[0].values = settings.time_cols.map(function(d) { - return d.value_col || d; - }); - - if (settings.filters.length > 0) - settings.filters.forEach(function(filter) { - syncedControlInputs.push({ - type: 'subsetter', - value_col: filter.value_col || filter, - label: filter.label || filter.value_col || filter, - description: 'filter', - multiple: false - }); - }); - - //Remove unscheduled visit control if unscheduled visit pattern is unscpecified. - if (!(settings.unscheduled_visit_regex || settings.unscheduled_visit_values)) - controlInputs.splice( - controlInputs - .map(function(controlInput) { - return controlInput.label; - }) - .indexOf('Unscheduled visits'), - 1 - ); - - return syncedControlInputs; - } - - function removeVariables() { - var _this = this; - - //Define set of required variables. - this.config.variables = d3 - .set( - d3.merge([ - [this.config.measure_col], - [this.config.id_col], - this.config.time_cols.map(function(time_col) { - return time_col.value_col; - }), - this.config.time_cols.map(function(time_col) { - return time_col.order_col; - }), - [this.config.value_col], - [this.config.unit_col], - [this.config.lln_col], - [this.config.uln_col], - this.config.filters - ? this.config.filters.map(function(filter) { - return filter.value_col ? filter.value_col : filter; - }) - : [] - ]) - ) - .values() - .filter(function(variable) { - return Object.keys(_this.data.initial[0]).indexOf(variable) > -1; - }); - - //Delete extraneous variables. - this.data.initial.forEach(function(d) { - for (var variable in d) { - if (_this.config.variables.indexOf(variable) < 0) delete d[variable]; - } - }); - - //If data do not have normal range variables update normal range method setting and options. - if ( - this.config.variables.indexOf(this.config.lln_col) < 0 || - this.config.variables.indexOf(this.config.uln_col) < 0 - ) { - if (this.config.normal_range_method === 'LLN-ULN') - this.config.normal_range_method = 'Standard Deviation'; - this.controls.config.inputs - .find(function(input) { - return input.option === 'normal_range_method'; - }) - .values.splice(1, 1); - } - } - - function deriveVariables() { - var _this = this; - - var ordinalTimeSettings = this.config.time_cols.find(function(time_col) { - return time_col.type === 'ordinal'; - }); - - this.data.raw.forEach(function(d) { - //Concatenate measure and unit. - if (d[_this.config.unit_col]) - d.measure_unit = - d[_this.config.measure_col] + ' (' + d[_this.config.unit_col] + ')'; - else d.measure_unit = d[_this.config.measure_col]; - - //Identify unscheduled visits. - d.unscheduled = false; - if (ordinalTimeSettings) { - if (_this.config.unscheduled_visit_values) - d.unscheduled = - _this.config.unscheduled_visit_values.indexOf( - d[ordinalTimeSettings.value_col] - ) > -1; - else if (_this.config.unscheduled_visit_regex) - d.unscheduled = _this.config.unscheduled_visit_regex.test( - d[ordinalTimeSettings.value_col] - ); - } - }); - } - - function defineData(data) { - var _this = this; - - this.data = { - initial: data - }; - - //Remove extraneous variables. - removeVariables.call(this); - - //Define arrays of all IDs, filtered IDs, and selected IDs. - this.data.IDs = { - raw: d3 - .set( - this.data.initial.map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values() - }; - - //Remove invalid data. - this.data.raw = this.data.initial.filter(function(d) { - return ( - !/^\s*$/.test(d[_this.config.measure_col]) && - /^[0-9.]+$/.test(d[_this.config.value_col]) - ); - }); - this.data.filtered = this.data.raw; - this.data.IDs.filtered = d3 - .set( - this.data.filtered.map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values(); - this.data.IDs.selected = []; - - //Derive additional variables. - deriveVariables.call(this); - - //Warn user of dropped records. - if (this.data.raw.length !== this.data.initial.length) - console.warn( - this.data.initial.length - - this.data.raw.length + - ' non-numeric observations have been removed from the data.' - ); - } - - function captureMeasures() { - var _this = this; - - this.config.allMeasures = d3 - .set( - this.data.raw.map(function(d) { - return d.measure_unit; - }) - ) - .values() - .sort(function(a, b) { - var leftSort = a < b, - rightSort = a > b; - - if (_this.config.measures && _this.config.measures.length) { - var aPos = _this.config.measures.indexOf(a), - bPos = _this.config.measures.indexOf(b), - diff = aPos > -1 && bPos > -1 ? aPos - bPos : null; - - return diff - ? diff - : aPos > -1 - ? -1 - : bPos > -1 - ? 1 - : leftSort - ? -1 - : rightSort - ? 1 - : 0; - } else return leftSort ? -1 : rightSort ? 1 : 0; - }); - this.config.measures = - this.config.measures && this.config.measures.length - ? this.config.measures - : this.config.allMeasures; - } - - function defineVisitOrder() { - var _this = this; - - this.config.time_cols.forEach(function(time_settings) { - if (time_settings.type === 'ordinal') { - var visits = void 0, - visitOrder = void 0; - - //Given an ordering variable sort a unique set of visits by the ordering variable. - if ( - time_settings.order_col && - _this.data.raw[0].hasOwnProperty(time_settings.order_col) - ) { - //Define a unique set of visits with visit order concatenated. - visits = d3 - .set( - _this.data.raw.map(function(d) { - return ( - d[time_settings.order_col] + '|' + d[time_settings.value_col] - ); - }) - ) - .values(); - - //Sort visits. - visitOrder = visits - .sort(function(a, b) { - var aOrder = a.split('|')[0], - bOrder = b.split('|')[0], - diff = +aOrder - +bOrder; - return diff ? diff : d3.ascending(a, b); - }) - .map(function(visit) { - return visit.split('|')[1]; - }); - } else { - //Otherwise sort a unique set of visits alphanumerically. - //Define a unique set of visits. - visits = d3 - .set( - _this.data.raw.map(function(d) { - return d[time_settings.value_col]; - }) - ) - .values(); - - //Sort visits; - visitOrder = visits.sort(); - } - - //Set x-axis domain. - if (time_settings.order) { - //If a visit order is specified, use it and concatenate any unspecified visits at the end. - time_settings.order = time_settings.order.concat( - visitOrder.filter(function(visit) { - return time_settings.order.indexOf(visit) < 0; - }) - ); - } - //Otherwise use data-driven visit order. - else time_settings.order = visitOrder; - - //Define domain. - time_settings.domain = time_settings.order; - } else if (time_settings.type === 'linear') { - time_settings.order = null; - time_settings.domain = d3.extent(_this.data.raw, function(d) { - return +d[time_settings.value_col]; - }); - } - }); - } - - function toggleCharts(chart) { - var toggle = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; - - var measureListCheckbox = d3.select('#measure-list-checkbox'), - checked = measureListCheckbox.property('checked'), - measureItems = d3.selectAll('li.measure-item'), - anyUnchecked = measureItems[0].some(function(measureItem) { - return !measureItem.getElementsByTagName('input')[0].checked; - }); - - //Handle overall toggle. - if (toggle) { - measureListCheckbox.attr('title', checked ? 'Remove all charts' : 'Display all charts'); - measureItems.each(function(d) { - d3.select(this) - .select('input') - .property('checked', checked); - toggleChart(chart, this, d); - }); - measureListCheckbox.property('checked', checked); - } else { - //Handle individual toggles. - measureListCheckbox.attr( - 'title', - anyUnchecked ? 'Display all charts' : 'Remove all charts' - ); - measureListCheckbox.property('checked', !anyUnchecked); - } - } - - function toggleChart(chart, li) { - //Determine state of checkbox. - var checkbox = d3.select(li).select('input'), - checked = checkbox.property('checked'); - checkbox.attr('title', checked ? 'Remove chart' : 'Display chart'); - d3.select(chart.div) - .selectAll('.wc-chart') - .filter(function(di) { - return di.measure === d3.select(li).datum(); - }) - .classed('hidden', !checked); - - //If any checkbox is unchecked, uncheck measureListCheckbox. - toggleCharts(chart, false); - } - - function layout() { - this.wrap.attr('id', 'Charts'); - this.listing.wrap.attr('id', 'Listing').classed('hidden', true); - - var chart = this; - - //Navigation bar. - var navigationBar = this.container - .insert('ul', ':first-child') - .attr('id', 'navigation-bar'); - var navigationButtons = navigationBar - .selectAll('li.navigation') - .data(['Charts', 'Listing']) - .enter() - .append('li') - .classed('navigation', true) - .classed('active', function(d) { - return d === 'Charts'; - }) - .attr('id', function(d) { - return d + '-nav'; - }) - .text(function(d) { - return d; - }) - .on('click', function(d) { - navigationButtons - .filter(function(di) { - return di === d; - }) - .classed('active', true); - navigationButtons - .filter(function(di) { - return di !== d; - }) - .classed('active', false); - if (d === 'Charts') { - d3.select('#Listing').classed('hidden', true); - d3.select('#Charts').classed('hidden', false); - } else { - d3.select('#Charts').classed('hidden', true); - d3.select('#Listing').classed('hidden', false); - } - }); - - //Population annotation - this.populationAnnotation = navigationBar - .append('li') - .attr('id', 'population-annotation') - .html( - ' of ' + - ' participant(s) shown (' + - ')' - ); - - //Create controls header. - var controlsTab = this.container - .insert('div', ':first-child') - .attr('id', 'controls-header') - .text('Controls'); - - //Define all-chart toggle. - var measureListContainer = this.container - .select('#left-side') - .append('ul') - .attr('id', 'measure-list-container'); - var measureListHeader = measureListContainer - .append('div') - .attr('id', 'measure-list-header'); - var measureListCheckbox = measureListHeader - .append('input') - .attr({ - id: 'measure-list-checkbox', - type: 'checkbox', - title: - this.config.measures.length === this.config.allMeasures.length - ? 'Remove all charts' - : 'Display all charts' - }) - .property('checked', this.config.measures.length === this.config.allMeasures.length) - .on('click', function() { - toggleCharts(chart, this); - }); - measureListHeader.append('span').text('Measures'); - var measureList = measureListContainer //Define individual chart toggles. - .append('ul') - .attr('id', 'measure-list'); - var measureItems = measureList - .selectAll('li.measure-item') - .data(this.config.allMeasures) - .enter() - .append('li') - .attr('class', function(d) { - return 'measure-item ' + d.replace(/[^a-z0-9-]/gi, '-'); - }) - .each(function(d) { - //Append div inside list item. - var measureItemContainer = d3 - .select(this) - .append('div') - .classed('measure-item-container', true); - //Check whether measure should by displayed initially. - var checked = chart.config.measures.indexOf(d) > -1; //Append checkbox inside div. - var measureItemCheckbox = measureItemContainer - .append('input') - .classed('measure-checkbox', true) - .attr({ - type: 'checkbox', - title: checked ? 'Remove chart' : 'Display chart' - }) - .property('checked', checked); - var measureItemLabel = measureItemContainer.append('span').text(function(d) { - return d; - }); - }) - .on('change', function(d) { - toggleChart(chart, this); - }); - } - - function updatePopulationAnnotation() { - this.populationAnnotation.select('#n-participants').text(this.data.IDs.filtered.length); - this.populationAnnotation.select('#N-participants').text(this.data.IDs.raw.length); - this.populationAnnotation - .select('#n-N-rate') - .text(d3.format('%')(this.data.IDs.filtered.length / this.data.IDs.raw.length)); - } - - function applyFilters(d) { - var _this = this; - - this.data.IDs.selected = []; - - //Reset brush. - this.multiples.forEach(function(multiple) { - multiple.package.overlay.call(multiple.package.brush.clear()); - multiple.config.extent = multiple.package.brush.extent(); - }); - - //De-highlight brushed lines. - this.wrap.selectAll('.line-supergroup g.line path').classed('brushed', false); - - //De-highlight listing. - d3.select('#Listing-nav').classed('brushed', false); - - //Define filtered data. - if (d.type === 'subsetter') { - this.data.filtered = this.data.raw; - this.controls.config.inputs - .filter(function(input) { - return ( - input.type === 'subsetter' && - input.value !== 'All' && - input.value !== undefined - ); - }) - .forEach(function(input) { - _this.data.filtered = _this.data.filtered.filter(function(d) { - return input.value === d[input.value_col]; - }); - }); - this.data.IDs.filtered = d3 - .set( - this.data.filtered.map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values(); - updatePopulationAnnotation.call(this); - } - } - - function customizeControls() { - var _this = this; - - var context = this, - controls = this.controls.wrap - .selectAll('.control-group') - .classed('hidden', function(d) { - return ( - (_this.config.normal_range_method !== 'Standard Deviation' && - /standard deviation/i.test(d.label)) || - (_this.config.normal_range_method !== 'Quantiles' && - /quantile/i.test(d.label)) - ); - }); - - //Define x-axis option labels. - controls - .filter(function(control) { - return control.label === 'X-axis'; - }) - .selectAll('option') - .property('label', function(d) { - return _this.config.time_cols - .filter(function(time_col) { - return time_col.value_col === d; - }) - .pop().label; - }); - - //Define x-axis option labels. - controls - .filter(function(control) { - return control.label === 'X-axis'; - }) - .selectAll('option') - .property('label', function(d) { - return _this.config.time_cols - .filter(function(time_col) { - return time_col.value_col === d; - }) - .pop().label; - }); - - //Add custom x-domain and filter functionality. - controls - .filter(function(d) { - return d.type === 'subsetter' || d.label === 'X-axis'; - }) - .on('change', function(d) { - d.value = d3 - .select(this) - .selectAll('option') - .filter(function() { - return this.selected; - }) - .text(); - applyFilters.call(context, d); - }); - - //Add custom normal range functionality. - var normalRangeControl = controls.filter(function(d) { - return d.label === 'Normal range method'; - }); - normalRangeControl.on('change', function(d) { - var normal_range_method = d3 - .select(this) - .select('option:checked') - .text(); - - controls.classed('hidden', function(d) { - return ( - (normal_range_method !== 'Standard Deviation' && - /standard deviation/i.test(d.label)) || - (normal_range_method !== 'Quantiles' && /quantile/i.test(d.label)) - ); - }); - }); - } - - function initCustomEvents() { - var chart = this; - chart.participantsSelected = []; - chart.events.participantsSelected = new CustomEvent('participantsSelected'); - } - - function init(data) { - //Attach various data arrays to charts. - defineData.call(this, data); - - //Capture unique set of measures in data. - captureMeasures.call(this); - - //Capture ordered set of visits. - defineVisitOrder.call(this); - - //Define layout of renderer. - layout.call(this); - - //Update population annotation with initial statistics. - updatePopulationAnnotation.call(this); - - //Initialize charts. - webcharts.multiply(this, this.data.raw, 'measure_unit', this.config.allMeasures); - - //Initialize listing. - this.listing.config.cols = Object.keys(data[0]).filter(function(key) { - return ['measure_unit', 'unscheduled', 'outlier'].indexOf(key) === -1; - }); // remove system variables from listing - this.listing.init(this.data.raw); - - //Define custom event listener for filters. - customizeControls.call(this); - - //initialize custom events - initCustomEvents.call(this); - } - - function defineData$1() { - var _this = this; - - this.data = { - measure: this.filters[0].val - }; - this.data.raw = this.raw_data.filter(function(d) { - return d.measure_unit === _this.data.measure; - }); - this.data.results = this.data.raw - .map(function(d) { - return +d[_this.config.value_col]; - }) - .sort(function(a, b) { - return a - b; - }); - this.data.yDomain = d3.extent(this.data.results); - this.data.yRange = this.data.yDomain[1] - this.data.yDomain[0]; - this.data.yFormat = - this.data.yRange < 0.1 - ? '.3f' - : this.data.yRange < 1 - ? '.2f' - : this.data.yRange < 10 - ? '.1f' - : '1d'; - this.data.IDs = { - raw: d3 - .set( - this.data.raw.map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values() - .sort() - }; - } - - function onInit() { - defineData$1.call(this); - } - - function minimize(chart) { - delete chart.parent.expandedChart; - //Modify chart config and redraw. - chart.wrap - .select('.m__imize-chart') - .html('+') - .attr('title', 'Maximize chart'); - chart.wrap.classed('expanded', false); - - chart.config.previous_plot_width = chart.plot_width; - chart.config.width = chart.config.initialSettings.width; - chart.config.max_width = null; - chart.config.height = chart.config.initialSettings.height; - chart.config.aspect = null; - - chart.draw(); - } - - function m__imize(chart) { - chart.config.previous_plot_width = chart.plot_width; - - //Maximize chart. - if (!chart.wrap.classed('expanded')) { - //Clear previously expanded chart. - if (chart.parent.expandedChart) minimize(chart.parent.expandedChart); - - //Attach expanded chart to parent. - chart.parent.expandedChart = chart; - - //Modify chart configuation and redraw. - chart.wrap - .select('.m__imize-chart') - .html('−') - .attr('title', 'Minimize chart'); - chart.wrap.classed('expanded', true); - - chart.config.width = null; - chart.config.max_width = 9999; - chart.config.height = null; - chart.config.aspect = 2.5; - - chart.draw(); - - //Sort expanded chart first. - chart.parent.wrap.selectAll('.wc-chart').sort(function(a, b) { - return a.measure === chart.data.measure - ? -1 - : b.measure === chart.data.measure - ? 1 - : chart.config.measures.indexOf(a.measure) - - chart.config.measures.indexOf(b.measure); - }); - - //Scroll window to expanded chart. - var bodyRect = document.body.getBoundingClientRect(), - elemRect = chart.wrap.node().getBoundingClientRect(), - offset = elemRect.top - bodyRect.top; - window.scrollTo(0, offset); - } else { - //Minimize chart - minimize(chart); - - //Revert to default sort. - chart.parent.wrap.selectAll('.wc-chart').sort(function(a, b) { - return ( - chart.config.measures.indexOf(a.measure) - - chart.config.measures.indexOf(b.measure) - ); - }); - } - } - - function removeChart() { - var _this = this; - - this.wrap - .on('mouseover', function() { - _this.wrap.selectAll('.wc-chart-title span').style('visibility', 'visible'); - }) - .on('mouseout', function() { - _this.wrap.selectAll('.wc-chart-title span').style('visibility', 'hidden'); - }) - .select('.wc-chart-title') - .append('span') - .classed('remove-chart chart-button', true) - .html('✖') - .attr('title', 'Remove chart') - .style('visibility', 'hidden') - .on('click', function() { - //Minimize chart. - if (_this.wrap.classed('full-screen')) m__imize(_this); - - var li = d3.select( - 'li.measure-item.' + _this.data.measure.replace(/[^a-z0-9-]/gi, '-') - ); - li.select('input').property('checked', false); - toggleChart(_this, li.node()); - }); - } - - function m__imizeChart() { - var _this = this; - - var m__imizeButton = this.wrap - .select('.wc-chart-title') - .append('span') - .classed('m__imize-chart chart-button', true) - .html('+') - .attr('title', 'Maximize chart'); - m__imizeButton.on('click', function() { - m__imize(_this); - }); - } - - function classChart() { - this.wrap - .classed(this.data.measure.replace(/[^a-z0-9-]/gi, '-'), true) - .classed('hidden', this.config.measures.indexOf(this.data.measure) === -1); - } - - function addInlierAnnotation() { - this.inliersAnnotation = { - g: this.svg.append('g').classed('n-inlier', true) - }; - this.inliersAnnotation.text = this.inliersAnnotation.g.append('text'); - this.inliersAnnotation.rect = this.inliersAnnotation.g.append('rect'); - this.inliersAnnotation.title = this.inliersAnnotation.g.append('title'); - } - - function onLayout() { - //Add button to the chart title that removes chart. - removeChart.call(this); - - //Add button to the chart title that maximizes/minimizes chart. - m__imizeChart.call(this); - - //Add measure-specific chart class and class that hides chart as needed. - classChart.call(this); - - //Add node to svg for inliers annotation. - addInlierAnnotation.call(this); - } - - function removeVisitsWithoutData() { - var _this = this; - - if (!this.config.visits_without_data) { - this.config.x.domain = this.config.x.domain.filter(function(visit) { - return ( - d3 - .set( - _this.data.raw.map(function(d) { - return d[_this.config.x.column]; - }) - ) - .values() - .indexOf(visit) > -1 - ); - }); - } - } - - function removeUnscheduledVisits() { - var _this = this; - - if (!this.config.unscheduled_visits) { - if (this.config.unscheduled_visit_values) - this.config.x.domain = this.config.x.domain.filter(function(visit) { - return _this.config.unscheduled_visit_values.indexOf(visit) < 0; - }); - else if (this.config.unscheduled_visit_regex) - this.config.x.domain = this.config.x.domain.filter(function(visit) { - return !_this.config.unscheduled_visit_regex.test(visit); - }); - } - } - - function defineXsettings() { - var _this = this; - - //Update x-object. - Object.assign( - this.config.x, - this.config.time_cols.find(function(time_col) { - return time_col.value_col === _this.config.x.column; - }) - ); - this.config.x.label = ''; - - //Remove visits without data from x-domain if x-type is ordinal. - if (this.config.x.type === 'ordinal') { - this.config.x.domain = this.config.x.order; - removeVisitsWithoutData.call(this); - removeUnscheduledVisits.call(this); - } - - //Delete domain setting if x-type is linear - if (this.config.x.type !== 'ordinal') delete this.config.x.domain; - - //Update bottom margin. - this.config.margin.bottom = this.config.x.vertical_space; - } - - function defineYsettings() { - this.config.y.domain = this.data.domain; - this.config.y.format = this.data.format; - } - - function deriveStatistics() { - var _this = this; - - if (this.config.normal_range_method === 'LLN-ULN') { - this.lln = function(d) { - return d instanceof Object - ? +d[_this.config.lln_col] - : d3.median(_this.data.raw, function(d) { - return +d[_this.config.lln_col]; - }); - }; - this.uln = function(d) { - return d instanceof Object - ? +d[_this.config.uln_col] - : d3.median(_this.data.raw, function(d) { - return +d[_this.config.uln_col]; - }); - }; - } else if (this.config.normal_range_method === 'Standard Deviation') { - this.mean = d3.mean(this.data.results); - this.sd = d3.deviation(this.data.results); - this.lln = function() { - return _this.mean - _this.config.normal_range_sd * _this.sd; - }; - this.uln = function() { - return _this.mean + _this.config.normal_range_sd * _this.sd; - }; - } else if (this.config.normal_range_method === 'Quantiles') { - this.lln = function() { - return d3.quantile(_this.data.results, _this.config.normal_range_quantile_low); - }; - this.uln = function() { - return d3.quantile(_this.data.results, _this.config.normal_range_quantile_high); - }; - } else { - this.lln = function(d) { - return d instanceof Object ? d[_this.config.value_col] + 1 : _this.data.results[0]; - }; - this.uln = function(d) { - return d instanceof Object - ? d[_this.config.value_col] - 1 - : _this.data.results[_this.data.results.length - 1]; - }; - } - } - - function deriveVariables$1() { - var _this = this; - - this.data.raw.forEach(function(d) { - d.outlier = - d[_this.config.value_col] < _this.lln(d) || - d[_this.config.value_col] > _this.uln(d); - }); - this.data.IDs.outliers = d3 - .set( - this.data.raw - .filter(function(d) { - return d.outlier; - }) - .map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values(); - } - - function defineFilteredData() { - var _this = this; - - this.data.filtered = this.data.raw; - - //Apply current filters. - this.filters - .filter(function(filter) { - return filter.col !== 'measure_unit' && filter.val !== 'All'; - }) - .forEach(function(filter) { - _this.data.filtered = _this.data.filtered.filter(function(d) { - return Array.isArray(filter.val) - ? filter.val.indexOf(d[filter.col]) > -1 - : filter.val === d[filter.col]; - }); - }); - - //Capture IDs with data matching the current filters. - this.data.IDs.filtered = d3 - .set( - this.data.filtered.map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values() - .sort(); - } - - function defineDisplayedData() { - var _this = this; - - this.data.displayed = this.data.filtered; - - //Remove unscheduled visits. - if (!this.config.unscheduled_visits) - this.data.displayed = this.data.displayed.filter(function(d) { - return !d.unscheduled; - }); - - //Remove inliers. - if (!this.config.inliers) - this.data.displayed = this.data.displayed.filter(function(d) { - return ( - _this.data.IDs.outliers.indexOf(d[_this.config.id_col]) > -1 || - _this.parent.data.IDs.selected.indexOf(d[_this.config.id_col]) > -1 - ); - }); - - //Capture IDs that will actually be displayed. - this.data.IDs.displayed = d3 - .set( - this.data.displayed.map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values() - .sort(); - } - - function onPreprocess() { - defineXsettings.call(this); - defineYsettings.call(this); - deriveStatistics.call(this); - deriveVariables$1.call(this); - defineFilteredData.call(this); - defineDisplayedData.call(this); - this.raw_data = this.data.displayed; - } - - function onDatatransform() {} - - function onDraw() { - if (this.package) this.package.overlay.call(this.package.brush.clear()); - } - - function resetChart() { - this.svg.selectAll('*').style('visibility', 'visible'); - this.svg.select('text.no-data').remove(); - this.svg.select('.normal-range').remove(); - } - - function definePackage() { - //Capture each multiple's scale. - this.package = { - measure: this.data.measure, - container: this.wrap, - overlay: this.svg.append('g').classed('brush', true), - domain: clone(this.config.y.domain), - xScale: clone(this.x), - yScale: clone(this.y), - brush: d3.svg - .brush() - .x(this.x) - .y(this.y) - }; - this.wrap.datum(this.package); - - //Define invisible brush overlay. - this.package.overlay.append('rect').attr({ - x: 0, - y: 0, - width: this.plot_width, - height: this.plot_height, - 'fill-opacity': 0 - }); - - //Attach additional data to SVG and marks. - this.package.overlay.style('cursor', 'crosshair').datum({ measure: this.data.measure }); - } - - function handleNoData() { - this.svg.selectAll('*').style('visibility', 'hidden'); - this.svg - .append('text') - .classed('no-data', true) - .attr({ - x: 0, - dx: -this.config.margin.left, - y: 0, - dy: 10 - }) - .text('No data selected.'); - } - - function drawNormalRange() { - if (this.config.normal_range_method) - this.svg - .insert('rect', '.line-supergroup') - .classed('normal-range', true) - .attr({ - x: this.x(this.x_dom[0]) - 5, // make sure left side of normal range does not appear in chart - y: this.y(this.uln()), - width: this.plot_width + 10, // make sure right side of normal range does not appear in chart - height: this.y(this.lln()) - this.y(this.uln()), - 'clip-path': 'url(#' + this.id + ')' - }); - } - - function annotateInliers() { - var _this = this; - - this.inliersAnnotation.g.classed('hidden', this.config.inliers); - - if (!this.config.inliers) { - //text - var n = this.data.IDs.filtered.length; - var nDisplayed = this.data.IDs.displayed.length; - var nHidden = n - nDisplayed; - this.inliersAnnotation.text - .attr({ - x: 0, - dx: -10, - y: this.plot_height, - dy: 19 - }) - .text('' + nHidden); - - //text box - var textDimensions = this.inliersAnnotation.text.node().getBBox(); - this.inliersAnnotation.rect.attr({ - x: textDimensions.x - 2, - y: textDimensions.y, - width: textDimensions.width + 4, - height: textDimensions.height - }); - - //tooltip - this.inliersAnnotation.title.text( - nHidden + - ' of ' + - n + - ' participants (' + - d3.format('%')(nHidden / n) + - ') with entirely normal results are hidden.\nToggle the "Normal range inliers" checkbox to display these participants.' - ); - - //mosue hover - this.inliersAnnotation.g - .on('mouseover', function() { - _this.controls.wrap - .selectAll('.control-group') - .filter(function(d) { - return d.option === 'inliers'; - }) - .classed('inlier-highlighting', true); - }) - .on('mouseout', function() { - _this.controls.wrap - .selectAll('.control-group') - .filter(function(d) { - return d.option === 'inliers'; - }) - .classed('inlier-highlighting', false); - }); - } - } - - function attachLines() { - var _this = this; - - this.lines = this.svg.selectAll('.line-supergroup g.line path'); - this.lines.each(function(d, i) { - d.id = d.values[0].values.raw[0][_this.config.id_col]; - d.lln = d.values[0].values.raw[0][_this.config.lln_col]; - d.uln = d.values[0].values.raw[0][_this.config.uln_col]; - d.lines = d.values.map(function(di, i) { - var line; - if (i) { - line = { - x0: - _this.config.x.type === 'linear' - ? d.values[i - 1].values.x - : _this.x(d.values[i - 1].values.x) + _this.x.rangeBand() / 2, - y0: d.values[i - 1].values.y, - x1: - _this.config.x.type === 'linear' - ? di.values.x - : _this.x(di.values.x) + _this.x.rangeBand() / 2, - y1: di.values.y - }; - } - return line; - }); - d.lines.shift(); - }); - } - - function identifyPoints() { - this.lines - .filter(function(d) { - return d.lines.length === 0; - }) - .classed('single-point', true); - } - - d3.selection.prototype.moveToFront = function() { - return this.each(function() { - this.parentNode.appendChild(this); - }); - }; - - /** - * @author Peter Kelley - * @author pgkelley4@gmail.com - */ - - /** - * See if two line segments intersect. This uses the - * vector cross product approach described below: - * http://stackoverflow.com/a/565282/786339 - * - * @param {Object} p point object with x and y coordinates - * representing the start of the 1st line. - * @param {Object} p2 point object with x and y coordinates - * representing the end of the 1st line. - * @param {Object} q point object with x and y coordinates - * representing the start of the 2nd line. - * @param {Object} q2 point object with x and y coordinates - * representing the end of the 2nd line. - */ - - /** - * Subtract the second point from the first. - * - * @param {Object} point1 point object with x and y coordinates - * @param {Object} point2 point object with x and y coordinates - * - * @return the subtraction result as a point object - */ - - function subtractPoints(point1, point2) { - var result = {}; - result.x = point1.x - point2.x; - result.y = point1.y - point2.y; - - return result; - } - - /** - * Calculate the cross product of the two points. - * - * @param {Object} point1 point object with x and y coordinates - * @param {Object} point2 point object with x and y coordinates - * - * @return the cross product result as a float - */ - function crossProduct(point1, point2) { - return point1.x * point2.y - point1.y * point2.x; - } - - /** - * See if the points are equal. - * - * @param {Object} point1 point object with x and y coordinates - * @param {Object} point2 point object with x and y coordinates - * - * @return if the points are equal - */ - function equalPoints(point1, point2) { - return point1.x == point2.x && point1.y == point2.y; - } - - /** - * See if all arguments are equal. - * - * @param {...} args arguments that will be compared by '=='. - * - * @return if all arguments are equal - */ - function allEqual(args) { - var firstValue = arguments[0], - i; - for (i = 1; i < arguments.length; i += 1) { - if (arguments[i] != firstValue) { - return false; - } - } - return true; - } - - function doLineSegmentsIntersect(p, p2, q, q2) { - var r = subtractPoints(p2, p); - var s = subtractPoints(q2, q); - - var uNumerator = crossProduct(subtractPoints(q, p), r); - var denominator = crossProduct(r, s); - - if (uNumerator == 0 && denominator == 0) { - // They are coLlinear - - // Do they touch? (Are any of the points equal?) - if ( - equalPoints(p, q) || - equalPoints(p, q2) || - equalPoints(p2, q) || - equalPoints(p2, q2) - ) { - return true; - } - // Do they overlap? (Are all the point differences in either direction the same sign) - return ( - !allEqual(q.x - p.x < 0, q.x - p2.x < 0, q2.x - p.x < 0, q2.x - p2.x < 0) || - !allEqual(q.y - p.y < 0, q.y - p2.y < 0, q2.y - p.y < 0, q2.y - p2.y < 0) - ); - } - - if (denominator == 0) { - // lines are paralell - return false; - } - - var u = uNumerator / denominator; - var t = crossProduct(subtractPoints(q, p), s) / denominator; - - return t >= 0 && t <= 1 && u >= 0 && u <= 1; - } - - function highlightChart() { - var _this = this; - - var extent = this.package.brush.extent(); - var x0 = extent[0][0]; // top left x-coordinate - var y0 = extent[1][1]; // top left y-coordinate - var x1 = extent[1][0]; // bottom right x-coordinate - var y1 = extent[0][1]; // bottom right y-coordinate - var top = { x0: x1, y0: y0, x1: x0, y1: y0 }; - var right = { x0: x1, y0: y1, x1: x1, y1: y0 }; - var bottom = { x0: x0, y0: y1, x1: x1, y1: y1 }; - var left = { x0: x0, y0: y0, x1: x0, y1: y1 }; - var sides = [top, right, bottom, left]; - - //Determine which lines fall inside the brush. - this.lines.classed('brushed', function(d) { - d.intersection = false; - //lines - if (d.lines.length) { - d.lines.forEach(function(line) { - sides.forEach(function(side) { - if (!d.intersection) - d.intersection = doLineSegmentsIntersect( - { x: line.x0, y: line.y0 }, - { x: line.x1, y: line.y1 }, - { x: side.x0, y: side.y0 }, - { x: side.x1, y: side.y1 } - ); - }); - }); - } else { - //points - var x = - _this.config.x.type === 'linear' - ? d.values[0].values.x - : _this.x(d.values[0].values.x) + _this.x.rangeBand() / 2; - var y = d.values[0].values.y; - d.intersection = x0 <= x && x <= x1 && y1 <= y && y <= y0; - } - - return d.intersection; - }); - - this.parent.data.IDs.selected = this.lines - .data() - .filter(function(d) { - return d.intersection; - }) - .map(function(d) { - return d.id; - }); - } - - function highlightCharts() { - var _this = this; - - //Highlight brushed lines. - this.parent.wrap - .selectAll('.line-supergroup g.line path') - .classed('brushed', false) - .filter(function(d) { - return _this.parent.data.IDs.selected.indexOf(d.id) > -1; - }) - .classed('brushed', true) - .each(function(d) { - d3.select(this.parentNode).moveToFront(); - }); - - //Draw listing displaying brushed IDs first. - if (this.parent.data.IDs.selected.length) { - d3.select('#Listing-nav').classed('brushed', true); - this.parent.listing.data.raw = this.parent.data.raw.filter(function(d) { - return _this.parent.data.IDs.selected.indexOf(d[_this.config.id_col]) > -1; - }); - this.parent.listing.draw(); - } else { - d3.select('#Listing-nav').classed('brushed', false); - this.parent.listing.data.raw = this.parent.data.raw; - this.parent.listing.draw(); - } - } - - function brush() { - var _this = this; - - //Highlight previously brushed points. - if (this.parent.data.IDs.selected.length) { - this.lines - .filter(function(d) { - return _this.parent.data.IDs.selected.indexOf(d.id) > -1; - }) - .classed('brushed', true) - .each(function() { - d3.select(this.parentNode).moveToFront(); - }); - } - - //Apply brush. - this.package.brush - .on('brushstart', function() { - //Clear previous brush. - if (_this.parent.brushedChart && _this.parent.brushedMeasure !== _this.data.measure) - _this.parent.brushedChart.package.overlay.call( - _this.parent.brushedChart.package.brush.clear() - ); - - //Attach current brushed chart to parent. - _this.parent.brushedChart = _this; - _this.parent.brushedMeasure = _this.data.measure; - }) - .on('brush', function() { - //Add highlighting to brushed chart. - highlightChart.call(_this); - }) - .on('brushend', function() { - _this.config.extent = _this.package.brush.extent(); - - //Add highlighting to all charts. - highlightCharts.call(_this); - - //trigger custom participantsSelected event - _this.parent.participantsSelected = _this.parent.data.IDs.selected; - _this.parent.events.participantsSelected.data = _this.parent.participantsSelected; - _this.parent.wrap.node().dispatchEvent(_this.parent.events.participantsSelected); - - //Redraw charts in which the currently brushed ID(s) are inliers. - if (_this.parent.data.IDs.selected.length > 0) - _this.parent.multiples - .filter(function(multiple) { - return ( - _this.parent.data.IDs.selected.filter(function(ID) { - return multiple.data.IDs.displayed.indexOf(ID) < 0; - }).length > 0 - ); - }) - .forEach(function(multiple) { - multiple.draw(); - }); - }); - - //Initialize brush on brush overlay. - this.package.overlay.call(this.package.brush); - - //Maintain brush on redraw. - if (!this.config.extent) this.config.extent = this.package.brush.extent(); - if ( - (this.config.extent[0][0] !== this.package.brush.extent()[0][0] || - this.config.extent[0][1] !== this.package.brush.extent()[0][1] || - this.config.extent[1][0] !== this.package.brush.extent()[1][0] || - this.config.extent[1][1] !== this.package.brush.extent()[1][1]) && - this.data.measure === this.parent.brushedMeasure - ) { - if (this.config.x.type === 'ordinal') { - this.config.extent[0][0] = - (this.config.extent[0][0] * this.plot_width) / this.config.previous_plot_width; - this.config.extent[1][0] = - (this.config.extent[1][0] * this.plot_width) / this.config.previous_plot_width; - } - this.package.brush.extent(this.config.extent); - this.package.overlay.call(this.package.brush); - highlightCharts.call(this); - } - } - - function rotateXaxisTickLabels() { - if (this.config.x.rotate_tick_labels) { - var ticks = this.svg - .selectAll('.' + 'x' + '.axis .tick text') - .attr({ - transform: 'rotate(-45)', - dx: -10, - dy: 10 - }) - .style('text-anchor', 'end'); - - ticks - .filter(function(d) { - return ('' + d).length > 10; - }) - .style('cursor', 'help') - .text(function(d) { - return d.slice(0, 7) + '...'; - }) - .append('title') - .text(function(d) { - return d; - }); - } - } - - function onResize() { - //Reset chart. - resetChart.call(this); - - //Define datum for each multiple and attach it to multiple's container. - definePackage.call(this); - - //Draw normal range. - if (this.filtered_data.length == 0) handleNoData.call(this); - else { - //Draw normal range. - drawNormalRange.call(this); - - //Annotate number of inliers. - annotateInliers.call(this); - - //Attach lines to chart object. - attachLines.call(this); - - //Identify lines with only one node. - identifyPoints.call(this); - - //Add brush functionality. - brush.call(this); - - //Rotate x-axis tick labels. - rotateXaxisTickLabels.call(this); - } - } - - function onDestroy() {} - - var chartCallbacks = { - onInit: onInit, - onLayout: onLayout, - onPreprocess: onPreprocess, - onDatatransform: onDatatransform, - onDraw: onDraw, - onResize: onResize, - onDestroy: onDestroy - }; - - function onInit$1() {} - - function onLayout$1() {} - - function onDraw$1() { - var _this = this; - - //Highlight selected rows. - if (this.data.filtered.length) - this.table.selectAll('tbody tr').classed('brushed', function(d) { - return _this.chart.data.IDs.selected.indexOf(d[_this.chart.config.id_col]) > -1; - }); - } - - function onDestroy$1() {} - - var listingCallbacks = { - onInit: onInit$1, - onLayout: onLayout$1, - onDraw: onDraw$1, - onDestroy: onDestroy$1 - }; - - //Utility polyfills - - function paneledOutlierExplorer() { - var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'body'; - var settings = arguments[1]; - - //Define unique div within passed element argument. - var container = d3 - .select(element) - .append('div') - .attr('id', 'paneled-outlier-explorer'), - containerElement = container.node(), - controlsContainer = container.append('div').attr('id', 'left-side'), - controlsContainerElement = controlsContainer.node(); - - //Define .css styles to avoid requiring a separate .css file. - defineStyles(); - - //Clone, merge, and sync settings and define chart. - var initialSettings = clone(settings), - mergedSettings = Object.assign({}, defaultSettings, initialSettings), - syncedSettings = syncSettings(mergedSettings), - syncedControlInputs = syncControlInputs(controlInputs, syncedSettings), - controls = webcharts.createControls(controlsContainerElement, { - location: 'top', - inputs: syncedControlInputs - }), - chart = webcharts.createChart(containerElement, syncedSettings, controls), - listing = webcharts.createTable(containerElement, { nRowsPerPage: 25 }, controls); - - //Attach stuff to chart. - chart.container = container; - chart.listing = listing; - chart.config.initialSettings = clone(syncedSettings); - - //Attach stuff to listing. - listing.container = container; - listing.chart = chart; - - //Define chart callbacks. - for (var callback in chartCallbacks) { - chart.on(callback.substring(2).toLowerCase(), chartCallbacks[callback]); - } //Define listing callbacks. - for (var _callback in listingCallbacks) { - listing.on(_callback.substring(2).toLowerCase(), listingCallbacks[_callback]); - } //Redefine chart.init() in order to call webCharts.multiply() on paneledOutlierExplorer().init(). - Object.defineProperty(chart, 'init', { - enumerable: false, - configurable: true, - writable: true, - value: init - }); - - return chart; - } - - return paneledOutlierExplorer; -}); diff --git a/inst/htmlwidgets/lib/safety-delta-delta-1.0.0/safetyDeltaDelta.js b/inst/htmlwidgets/lib/safety-delta-delta-1.0.0/safetyDeltaDelta.js deleted file mode 100644 index 5b532a84..00000000 --- a/inst/htmlwidgets/lib/safety-delta-delta-1.0.0/safetyDeltaDelta.js +++ /dev/null @@ -1,1851 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' - ? (module.exports = factory(require('d3'), require('webcharts'))) - : typeof define === 'function' && define.amd - ? define(['d3', 'webcharts'], factory) - : ((global = global || self), - (global.safetyDeltaDelta = factory(global.d3, global.webCharts))); -})(this, function (d3, webcharts) { - 'use strict'; - - if (typeof Object.assign != 'function') { - Object.defineProperty(Object, 'assign', { - value: function assign(target, varArgs) { - if (target == null) { - // TypeError if undefined or null - throw new TypeError('Cannot convert undefined or null to object'); - } - - var to = Object(target); - - for (var index = 1; index < arguments.length; index++) { - var nextSource = arguments[index]; - - if (nextSource != null) { - // Skip over if undefined or null - for (var nextKey in nextSource) { - // Avoid bugs when hasOwnProperty is shadowed - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - - return to; - }, - writable: true, - configurable: true - }); - } - - if (!Array.prototype.find) { - Object.defineProperty(Array.prototype, 'find', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, 'length')). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return kValue. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return kValue; - } - // e. Increase k by 1. - k++; - } - - // 7. Return undefined. - return undefined; - } - }); - } - - if (!Array.prototype.findIndex) { - Object.defineProperty(Array.prototype, 'findIndex', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, "length")). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return k. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return k; - } - // e. Increase k by 1. - k++; - } - - // 7. Return -1. - return -1; - } - }); - } - - Math.log10 = Math.log10 = - Math.log10 || - function (x) { - return Math.log(x) * Math.LOG10E; - }; - - // https://github.com/wbkd/d3-extended - d3.selection.prototype.moveToFront = function () { - return this.each(function () { - this.parentNode.appendChild(this); - }); - }; - - d3.selection.prototype.moveToBack = function () { - return this.each(function () { - var firstChild = this.parentNode.firstChild; - if (firstChild) { - this.parentNode.insertBefore(this, firstChild); - } - }); - }; - - function rendererSettings() { - return { - id_col: 'USUBJID', - visit_col: 'VISIT', - visitn_col: 'VISITNUM', - measure_col: 'TEST', - value_col: 'STRESN', - filters: null, - details: null, - measure: { - x: null, - y: null - }, - visits: { - baseline: [], - comparison: [], - stat: 'mean' - }, - add_regression_line: true - }; - } - - function webchartsSettings() { - return { - x: { - column: null, - type: 'linear', - label: 'x delta', - format: '0.2f' - }, - y: { - column: null, - type: 'linear', - label: 'y delta', - behavior: 'flex', - format: '0.2f' - }, - marks: [ - { - type: 'circle', - per: null, - radius: 4, - attributes: { - 'stroke-width': 0.5, - 'fill-opacity': 0.8 - }, - tooltip: - 'Subject ID: [key]\nX Delta: [delta_x_rounded]\nY Delta: [delta_y_rounded]' - } - ], - gridlines: 'xy', - resizable: false, - margin: { right: 25, top: 25 }, - aspect: 1, - width: 400 - }; - } - - function syncSettings(settings) { - //handle a string argument to filters - if (!(settings.filters instanceof Array)) - settings.filters = typeof settings.filters === 'string' ? [settings.filters] : []; - - //handle a string argument to details - if (!(settings.details instanceof Array)) - settings.details = typeof settings.details === 'string' ? [settings.details] : []; - - //Define default details. - var defaultDetails = [{ value_col: settings.id_col, label: 'Participant ID' }]; - if (Array.isArray(settings.filters)) - settings.filters - .filter(function (filter) { - return filter.value_col !== settings.id_col; - }) - .forEach(function (filter) { - return defaultDetails.push({ - value_col: filter.value_col ? filter.value_col : filter, - label: filter.label - ? filter.label - : filter.value_col - ? filter.value_col - : filter - }); - }); - - //If [settings.details] is not specified: - if (!settings.details) settings.details = defaultDetails; - else { - //If [settings.details] is specified: - //Allow user to specify an array of columns or an array of objects with a column property - //and optionally a column label. - settings.details.forEach(function (detail) { - if ( - defaultDetails - .map(function (d) { - return d.value_col; - }) - .indexOf(detail.value_col ? detail.value_col : detail) === -1 - ) - defaultDetails.push({ - value_col: detail.value_col ? detail.value_col : detail, - label: detail.label - ? detail.label - : detail.value_col - ? detail.value_col - : detail - }); - }); - settings.details = defaultDetails; - } - - return settings; - } - - function controlInputs() { - return [ - { - type: 'dropdown', - values: [], - label: 'Baseline visit(s)', - option: 'visits.baseline', - require: true, - multiple: true - }, - { - type: 'dropdown', - values: [], - label: 'Comparison visit(s)', - option: 'visits.comparison', - require: true, - multiple: true - }, - { - type: 'dropdown', - values: [], - label: 'X Measure', - option: 'measure.x', - require: true - }, - { - type: 'dropdown', - values: [], - label: 'Y Measure', - option: 'measure.y', - require: true - } - ]; - } - - function syncControlInputs(controlInputs, settings) { - //Add filters to default controls. - if (Array.isArray(settings.filters) && settings.filters.length > 0) { - settings.filters.forEach(function (filter) { - var filterObj = { - type: 'subsetter', - value_col: filter.value_col || filter, - label: filter.label || filter.value_col || filter - }; - controlInputs.push(filterObj); - }); - } else delete settings.filters; - return controlInputs; - } - - function listingSettings() { - return { - cols: ['key', 'spark', 'delta'], - headers: ['Measure', '', 'Change over Time'], - searchable: false, - sortable: false, - pagination: false, - exportable: false - }; - } - - var configuration = { - rendererSettings: rendererSettings, - webchartsSettings: webchartsSettings, - settings: Object.assign({}, rendererSettings(), webchartsSettings()), - syncSettings: syncSettings, - controlInputs: controlInputs, - syncControlInputs: syncControlInputs, - listingSettings: listingSettings - }; - - function cleanData() { - var _this = this; - - //Remove missing and non-numeric data. - var preclean = this.raw_data; - var clean = this.raw_data.filter(function (d) { - return /^-?[0-9.]+$/.test(d[_this.config.value_col]); - }); - var nPreclean = preclean.length; - var nClean = clean.length; - var nRemoved = nPreclean - nClean; - - //Warn user of removed records. - if (nRemoved > 0) - console.warn( - nRemoved + - ' missing or non-numeric result' + - (nRemoved > 1 ? 's have' : ' has') + - ' been removed.' - ); - - //Preserve cleaned data. - this.initial_data = clean; - } - - function trimMeasures() { - var _this = this; - - this.initial_data.forEach(function (d) { - d[_this.config.measure_col] = d[_this.config.measure_col].trim(); - }); - } - - function checkFilters() { - var _this = this; - - if (this.config.filters) - this.config.filters = this.config.filters.filter(function (filter) { - var variableExists = _this.raw_data[0].hasOwnProperty(filter.value_col); - var nLevels = d3 - .set( - _this.raw_data.map(function (d) { - return d[filter.value_col]; - }) - ) - .values().length; - - if (!variableExists) - console.warn( - ' The [ ' + - filter.label + - ' ] filter has been removed because the variable does not exist.' - ); - else if (nLevels < 2) - console.warn( - 'The [ ' + - filter.label + - ' ] filter has been removed because the variable has only one level.' - ); - - return variableExists && nLevels > 1; - }); - } - - function getMeasures() { - var _this = this; - - this.measures = d3 - .set( - this.initial_data.map(function (d) { - return d[_this.config.measure_col]; - }) - ) - .values() - .sort(); - } - - function getVisits() { - var _this = this; - - if (this.config.visitn_col && this.initial_data[0].hasOwnProperty(this.config.visitn_col)) - this.visits = d3 - .set( - this.initial_data.map(function (d) { - return d[_this.config.visit_col] + '||' + d[_this.config.visitn_col]; - }) - ) - .values() - .sort(function (a, b) { - var aSplit = a.split('||'); - var aVisit = aSplit[0]; - var aOrder = aSplit[1]; - var bSplit = b.split('||'); - var bVisit = bSplit[0]; - var bOrder = bSplit[1]; - var diff = aOrder - bOrder; - return diff - ? diff - : aOrder < bOrder - ? -1 - : aOrder > bOrder - ? 1 - : aVisit < bVisit - ? -1 - : 1; - }) - .map(function (visit) { - return visit.split('||')[0]; - }); - else - this.visits = d3 - .set( - this.initial_data.map(function (d) { - return d[_this.config.visit_col]; - }) - ) - .values() - .sort(); - } - - function updateControlInputs() { - var x_control = this.controls.config.inputs.find(function (input) { - return input.option === 'measure.x'; - }); - x_control.values = this.measures; - x_control.start = this.config.measure.x; - - var y_control = this.controls.config.inputs.find(function (input) { - return input.option === 'measure.y'; - }); - y_control.values = this.measures; - y_control.start = this.config.measure.y; - - var baseline_control = this.controls.config.inputs.find(function (input) { - return input.option === 'visits.baseline'; - }); - baseline_control.values = this.visits; - baseline_control.start = this.config.visits.baseline; - - var comparison_control = this.controls.config.inputs.find(function (input) { - return input.option === 'visits.comparison'; - }); - comparison_control.values = this.visits; - comparison_control.start = this.config.visits.comprarison; - } - - function initCustomEvents() { - var chart = this; - chart.participantsSelected = []; - chart.events.participantsSelected = new CustomEvent('participantsSelected'); - } - - function initSettings() { - //Set initial measures. - this.config.measure.x = this.config.measure.x || this.measures[0]; - - // this.config.x.column = this.config.measure.x; - this.config.measure.y = this.config.measure.y || this.measures[1]; - - //Set baseline and comparison visits. - this.config.visits.baseline = - this.config.visits.baseline.length > 0 ? this.config.visits.baseline : [this.visits[0]]; - - this.config.visits.comparison = - this.config.visits.comparison.length > 0 - ? this.config.visits.comparison - : [this.visits[this.visits.length - 1]]; - } - - function onInit() { - // 1. Remove invalid data. - cleanData.call(this); - - // 2. trim measures. - trimMeasures.call(this); - - // 3a Check filters against data. - checkFilters.call(this); - - // 3b Get list of measures. - getMeasures.call(this); - - // 3c Get list of visits. - getVisits.call(this); - - //4a. Initialize the delta-delta settings & Update control inputs. - initSettings.call(this); - updateControlInputs.call(this); - - //initialize custom events - initCustomEvents.call(this); - } - - function initNotes() { - //Add footnote element. - this.wrap - .insert('p', ':first-child') - .attr('class', 'record-note') - .style('text-align', 'center') - .style('font-weight', 'bold') - .text('Click a point to see details.'); - - //Add header element in which to list visits at which measure is captured. - this.wrap.append('p', 'svg').attr('class', 'possible-visits'); - - //Add element for participant counts. - this.controls.wrap - .append('em') - .classed('annote', true) - .style('display', 'block'); - } - - function updateVisitControls() { - var _this = this; - - var config = this.config; - var baselineSelect = this.controls.wrap - .selectAll('.control-group') - .filter(function (f) { - return f.option === 'visits.baseline'; - }) - .select('select'); - baselineSelect - .selectAll('option') - .filter(function (f) { - return _this.config.visits.baseline.indexOf(f) > -1; - }) - .attr('selected', 'selected'); - - var comparisonSelect = this.controls.wrap - .selectAll('.control-group') - .filter(function (f) { - return f.option === 'visits.comparison'; - }) - .select('select'); - comparisonSelect - .selectAll('option') - .filter(function (f) { - return _this.config.visits.comparison.indexOf(f) > -1; - }) - .attr('selected', 'selected'); - } - - function onLayout() { - initNotes.call(this); - updateVisitControls.call(this); - } - - function addParticipantLevelMetadata(d, participant_obj) { - var varList = []; - if (this.config.filters) { - var filterVars = this.config.filters.map(function (d) { - return d.hasOwnProperty('value_col') ? d.value_col : d; - }); - varList = d3.merge([varList, filterVars]); - } - if (this.config.group_cols) { - var groupVars = this.config.group_cols.map(function (d) { - return d.hasOwnProperty('value_col') ? d.value_col : d; - }); - varList = d3.merge([varList, groupVars]); - } - if (this.config.details) { - var detailVars = this.config.details.map(function (d) { - return d.hasOwnProperty('value_col') ? d.value_col : d; - }); - varList = d3.merge([varList, detailVars]); - } - - varList.forEach(function (v) { - participant_obj[v] = '' + d[0][v]; - }); - } - - function getMeasureDetails(pt_data) { - var config = this.config; - var measure_details = d3 - .nest() - .key(function (d) { - return d[config.measure_col]; - }) - .rollup(function (di) { - var measure_obj = {}; - measure_obj.key = di[0][config.measure_col]; - measure_obj.spark = 'sparkline placeholder'; - measure_obj.toggle = '+'; - measure_obj.raw = di; - measure_obj.axisFlag = - measure_obj.key == config.measure.x - ? 'X' - : measure_obj.key == config.measure.y - ? 'Y' - : ''; - measure_obj.raw.forEach(function (dii) { - dii.baseline = config.visits.baseline.indexOf(dii[config.visit_col]) > -1; - dii.comparison = config.visits.comparison.indexOf(dii[config.visit_col]) > -1; - dii.color = dii.baseline ? 'blue' : dii.comparison ? 'orange' : '#999'; - }); - - ['baseline', 'comparison'].forEach(function (t) { - measure_obj[t + '_records'] = di.filter(function (f) { - return config.visits[t].indexOf(f[config.visit_col]) > -1; - }); - - measure_obj[t + '_value'] = d3.mean(measure_obj[t + '_records'], function (d) { - return d[config.value_col]; - }); - }); - measure_obj['delta'] = measure_obj.comparison_value - measure_obj.baseline_value; - return measure_obj; - }) - .entries(pt_data); - measure_details = measure_details - .map(function (m) { - return m.values; - }) - .sort(function (a, b) { - if (a.axisFlag == 'X') return -1; - else if (b.axisFlag == 'X') return 1; - else if (a.axisFlag == 'Y') return -1; - else if (b.axisFlag == 'Y') return 1; - else if (a.key < b.key) return -1; - else if (b.key > a.key) return 1; - else return 0; - }); - return measure_details; - } - - function flattenData(rawData) { - var _this = this; - - var nested = d3 - .nest() - .key(function (d) { - return d[_this.config.id_col]; - }) - .rollup(function (d) { - var obj = {}; - obj.key = d[0][_this.config.id_col]; - obj.raw = d; - obj.measures = getMeasureDetails.call(_this, d); - - obj.x_details = obj.measures.find(function (f) { - return f.key == _this.config.measure.x; - }); - obj.delta_x = obj.x_details ? obj.x_details.delta : null; - obj.delta_x_rounded = obj.x_details ? d3.format('0.2f')(obj.delta_x) : ''; - - obj.y_details = obj.measures.find(function (f) { - return f.key == _this.config.measure.y; - }); - obj.delta_y = obj.y_details ? obj.y_details.delta : null; - obj.delta_y_rounded = obj.y_details ? d3.format('0.2f')(obj.delta_y) : ''; - - addParticipantLevelMetadata.call(_this, d, obj); - - return obj; - }) - .entries(rawData); - - return nested.map(function (m) { - return m.values; - }); - } - - function updateAxisSettings() { - var config = this.config; - - //set config properties here since they aren't available in onInit - config.x.column = 'delta_x'; - config.y.column = 'delta_y'; - config.marks[0].per = ['key']; - - config.x.label = 'Change in ' + config.measure.x; - config.y.label = 'Change in ' + config.measure.y; - } - - function onPreprocess() { - updateAxisSettings.call(this); - this.raw_data = flattenData.call(this, this.initial_data); - } - - function onDatatransform() { } - - /*------------------------------------------------------------------------------------------------\ - Annotate number of participants based on current filters, number of participants in all, and - the corresponding percentage. - - Inputs: - - chart - a webcharts chart object - id_unit - a text string to label the units in the annotation (default = 'participants') - selector - css selector for the annotation - \------------------------------------------------------------------------------------------------*/ - - function updateParticipantCount(chart, selector, id_unit) { - //count the number of unique ids in the data set - var totalObs = d3 - .set( - chart.initial_data.map(function (d) { - return d[chart.config.id_col]; - }) - ) - .values().length; - - //count the number of unique ids in the current chart and calculate the percentage - var currentObs = chart.filtered_data.filter(function (f) { - return ( - !isNaN(f.delta_x) && f.delta_x !== null && !isNaN(f.delta_y) && f.delta_y !== null - ); - }).length; // TODO: remove these records as part of the data flow - - var percentage = d3.format('0.1%')(currentObs / totalObs); - - //clear the annotation - var annotation = d3.select(selector); - annotation.selectAll('*').remove(); - - //update the annotation - var units = id_unit ? ' ' + id_unit : ' participant(s)'; - annotation.text(currentObs + ' of ' + totalObs + units + ' shown (' + percentage + ')'); - } - - function reset() { - this.svg.selectAll('g.boxplot').remove(); - this.svg - .selectAll('g.point') - .classed('selected', false) - .select('circle') - .style('fill', this.config.colors[0]); - this.wrap - .select('.record-note') - .style('text-align', 'center') - .text('Click a point to see details.'); - this.listing.draw([]); - this.listing.wrap.style('display', 'none'); - } - - function onDraw() { - //Annotate selected and total number of participants. - updateParticipantCount(this, '.annote'); - - //Reset things. - reset.call(this); - } - - function drawBoxPlot( - svg, - results, - height, - width, - domain, - boxPlotWidth, - boxColor, - boxInsideColor, - fmt, - horizontal - ) { - //set default orientation to "horizontal" - var horizontal = horizontal == undefined ? true : horizontal; - - //make the results numeric and sort - var results = results - .map(function (d) { - return +d; - }) - .sort(d3.ascending); - - //set up scales - var y = d3.scale.linear().range([height, 0]); - - var x = d3.scale.linear().range([0, width]); - - if (horizontal) { - y.domain(domain); - } else { - x.domain(domain); - } - - 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]); - } - - var boxplot = svg - .append('g') - .attr('class', 'boxplot') - .datum({ values: results, probs: probs }); - - //draw rectangle from q1 to q3 - var box_x = horizontal ? x(0.5 - boxPlotWidth / 2) : x(probs[1]); - var box_width = horizontal - ? x(0.5 + boxPlotWidth / 2) - x(0.5 - boxPlotWidth / 2) - : x(probs[3]) - x(probs[1]); - var box_y = horizontal ? y(probs[3]) : y(0.5 + boxPlotWidth / 2); - var box_height = horizontal - ? -y(probs[3]) + y(probs[1]) - : y(0.5 - boxPlotWidth / 2) - y(0.5 + boxPlotWidth / 2); - - boxplot - .append('rect') - .attr('class', 'boxplot fill') - .attr('x', box_x) - .attr('width', box_width) - .attr('y', box_y) - .attr('height', box_height) - .style('fill', boxColor); - - //draw dividing lines at median, 95% and 5% - var iS = [0, 2, 4]; - var iSclass = ['', 'median', '']; - var iSColor = [boxColor, boxInsideColor, boxColor]; - for (var i = 0; i < iS.length; i++) { - boxplot - .append('line') - .attr('class', 'boxplot ' + iSclass[i]) - .attr('x1', horizontal ? x(0.5 - boxPlotWidth / 2) : x(probs[iS[i]])) - .attr('x2', horizontal ? x(0.5 + boxPlotWidth / 2) : x(probs[iS[i]])) - .attr('y1', horizontal ? y(probs[iS[i]]) : y(0.5 - boxPlotWidth / 2)) - .attr('y2', horizontal ? y(probs[iS[i]]) : y(0.5 + boxPlotWidth / 2)) - .style('fill', iSColor[i]) - .style('stroke', iSColor[i]); - } - - //draw lines from 5% to 25% and from 75% to 95% - var iS = [[0, 1], [3, 4]]; - for (var i = 0; i < iS.length; i++) { - boxplot - .append('line') - .attr('class', 'boxplot') - .attr('x1', horizontal ? x(0.5) : x(probs[iS[i][0]])) - .attr('x2', horizontal ? x(0.5) : x(probs[iS[i][1]])) - .attr('y1', horizontal ? y(probs[iS[i][0]]) : y(0.5)) - .attr('y2', horizontal ? y(probs[iS[i][1]]) : y(0.5)) - .style('stroke', boxColor); - } - - boxplot - .append('circle') - .attr('class', 'boxplot mean') - .attr('cx', horizontal ? x(0.5) : x(d3.mean(results))) - .attr('cy', horizontal ? y(d3.mean(results)) : y(0.5)) - .attr('r', horizontal ? x(boxPlotWidth / 3) : y(1 - boxPlotWidth / 3)) - .style('fill', boxInsideColor) - .style('stroke', boxColor); - - boxplot - .append('circle') - .attr('class', 'boxplot mean') - .attr('cx', horizontal ? x(0.5) : x(d3.mean(results))) - .attr('cy', horizontal ? y(d3.mean(results)) : y(0.5)) - .attr('r', horizontal ? x(boxPlotWidth / 6) : y(1 - boxPlotWidth / 6)) - .style('fill', boxColor) - .style('stroke', 'None'); - - var formatx = fmt ? d3.format(fmt) : d3.format('.2f'); - - boxplot - .selectAll('.boxplot') - .append('title') - .text(function (d) { - return ( - 'N = ' + - d.values.length + - '\n' + - 'Min = ' + - d3.min(d.values) + - '\n' + - '5th % = ' + - formatx(d3.quantile(d.values, 0.05)) + - '\n' + - 'Q1 = ' + - formatx(d3.quantile(d.values, 0.25)) + - '\n' + - 'Median = ' + - formatx(d3.median(d.values)) + - '\n' + - 'Q3 = ' + - formatx(d3.quantile(d.values, 0.75)) + - '\n' + - '95th % = ' + - formatx(d3.quantile(d.values, 0.95)) + - '\n' + - 'Max = ' + - d3.max(d.values) + - '\n' + - 'Mean = ' + - formatx(d3.mean(d.values)) + - '\n' + - 'StDev = ' + - formatx(d3.deviation(d.values)) - ); - }); - } - - function addBoxPlots() { - // Y-axis box plot - var yValues = this.current_data.map(function (d) { - return d.values.y; - }); - var ybox = this.svg.append('g').attr('class', 'yMargin'); - drawBoxPlot(ybox, yValues, this.plot_height, 1, this.y_dom, 10, '#bbb', 'white'); - ybox.select('g.boxplot').attr( - 'transform', - 'translate(' + (this.plot_width + this.config.margin.right / 2) + ',0)' - ); - - //X-axis box plot - var xValues = this.current_data.map(function (d) { - return d.values.x; - }); - var xbox = this.svg.append('g').attr('class', 'xMargin'); - drawBoxPlot( - xbox, //svg element - xValues, //values - 1, //height - this.plot_width, //width - this.x_dom, //domain - 10, //box plot width - '#bbb', //box color - 'white', //detail color - '0.2f', //format - false // horizontal? - ); - xbox.select('g.boxplot').attr( - 'transform', - 'translate(0,' + -(this.config.margin.top / 2) + ')' - ); - } - - function updateClipPath() { - //embiggen clip-path so points aren't clipped - var radius = this.config.marks.find(function (mark) { - return mark.type === 'circle'; - }).radius; - this.svg - .select('.plotting-area') - .attr('width', this.plot_width + radius * 2 + 2) // plot width + circle radius * 2 + circle stroke width * 2 - .attr('height', this.plot_height + radius * 2 + 2) // plot height + circle radius * 2 + circle stroke width * 2 - .attr( - 'transform', - 'translate(-' + - (radius + 1) + // translate left circle radius + circle stroke width - ',-' + - (radius + 1) + // translate up circle radius + circle stroke width - ')' - ); - } - - function addSparkLines(d) { - var chart = this.chart; - var config = this.chart.config; - - if (this.data.raw.length > 0) { - //don't try to draw sparklines if the table is empty - this.tbody - .selectAll('tr') - .style('background', 'none') - .style('border-bottom', '.5px solid black') - .each(function (row_d) { - //Spark line cell - var cell = d3 - .select(this) - .select('td.spark') - .classed('minimized', true) - .text(''), - toggle = d3 - .select(this) - .select('td.toggle') - .html('▽') - .style('cursor', 'pointer') - .style('color', '#999') - .style('vertical-align', 'middle'), - width = 100, - height = 25, - offset = 4, - overTime = row_d.raw.sort(function (a, b) { - return +a[config.visitn_col] - +b[config.visitn_col]; - }); - - var x = d3.scale - .linear() - .domain( - d3.extent(overTime, function (m) { - return +m[config.visitn_col]; - }) - ) - .range([offset, width - offset]); - - //y-domain includes 99th population percentile + any participant outliers - var y = d3.scale - .linear() - .domain( - d3.extent(overTime, function (m) { - return +m[config.value_col]; - }) - ) - .range([height - offset, offset]); - - //render the svg - var canvas = cell - .append('svg') - .attr({ - width: width, - height: height - }) - .append('g'); - - //draw the sparkline - var draw_sparkline = d3.svg - .line() - .interpolate('linear') - .x(function (d) { - return x(d[config.visitn_col]); - }) - .y(function (d) { - return y(d[config.value_col]); - }); - var sparkline = canvas - .append('path') - .datum(overTime) - .attr({ - class: 'sparkLine', - d: draw_sparkline, - fill: 'none', - stroke: '#999' - }); - - //draw baseline values - - var circles = canvas - .selectAll('circle') - .data(overTime) - .enter() - .append('circle') - .attr('class', 'circle outlier') - .attr('cx', function (d) { - return x(d[config.visitn_col]); - }) - .attr('cy', function (d) { - return y(d[config.value_col]); - }) - .attr('r', '2px') - .attr('stroke', function (d) { - return d.color; - }) - .attr('fill', function (d) { - return d.color == '#999' ? 'transparent' : d.color; - }) - .append('title') - .text(function (d) { - return ( - 'Value = ' + - d[config.value_col] + - ' @ Visit ' + - d[config.visitn_col] - ); - }); - }); - } - } - - function addFootnote() { - this.wrap.select('span.footnote').remove(); - this.wrap - .append('span') - .attr('class', 'footnote') - .style('font-size', '0.7em') - .style('color', '#999') - .text( - 'This table shows all lab values collected for the selected participant. Filled blue and orange circles indicate baseline and comparison visits respectively - all other visits are draw for reference using with empty gray circles. Change over time values greater than 0 are shown in green; values less than 0 shown in red.' - ); - } - - function formatDelta() { - this.tbody - .selectAll('tr') - .select('td.delta') - .text(function (d) { - return isNaN(d.delta) ? 'NA' : d3.format('+0.2f')(d.delta); - }) - .style('color', function (d) { - return isNaN(d.delta) - ? '#ccc' - : d.delta > 0 - ? 'green' - : d.delta < 0 - ? 'red' - : '#999'; - }); - } - - function addAxisFlag() { - var table = this; - ['X', 'Y'].forEach(function (axis) { - var cell = table.tbody - .selectAll('tr') - .filter(function (d) { - return d.axisFlag == axis; - }) - .select('td.key') - .text(''); - - cell.append('span') - .attr('class', 'sdd-axisLabel') - .text(axis + '-axis'); - - cell.append('span').text(function (d) { - return d.key; - }); - }); - } - - function showParticipantDetails(d) { - var table = this; - var chart = this.chart; - var raw = d.raw[0]; - - //show detail variables in a ul - table.wrap.select('ul.pdd-pt-details').remove(); - var ul = table.wrap - .insert('ul', '*') - .attr('class', 'pdd-pt-details') - .style('list-style', 'none') - .style('padding', '0'); - - var lis = ul - .selectAll('li') - .data(chart.config.details) - .enter() - .append('li') - .style('', 'block') - .style('display', 'inline-block') - .style('text-align', 'center') - .style('padding', '0.5em'); - - lis.append('div') - .text(function (d) { - return d.label; - }) - .attr('div', 'label') - .style('font-size', '0.8em'); - - lis.append('div') - .text(function (d) { - return raw[d.value_col]; - }) - .attr('div', 'value'); - } - - function drawMeasureTable(d) { - var chart = this; - var config = this.config; - - var point_data = d.values.raw[0]; - chart.listing.wrap.style('display', null); - - chart.listing.on('draw', function () { - showParticipantDetails.call(this, point_data); - addSparkLines.call(this); - formatDelta.call(this); - addAxisFlag.call(this); - addFootnote.call(this); - - this.thead.style('border-top', '2px solid black'); - }); - chart.listing.draw(point_data.measures); - } - - function addPointClick() { - var chart = this; - var config = this.config; - var points = this.marks[0].circles; - - points.on('click', function (d) { - points - .attr('stroke', function (d) { - return chart.colorScale(d.values.raw[0][config.color_by]); - }) - .attr('stroke-width', 0.5); - - d3.select(this) - .attr('stroke-width', 3) - .attr('stroke', 'black'); - drawMeasureTable.call(chart, d); - }); - } - - var commonjsGlobal = - typeof globalThis !== 'undefined' - ? globalThis - : typeof window !== 'undefined' - ? window - : typeof global !== 'undefined' - ? global - : typeof self !== 'undefined' - ? self - : {}; - - function createCommonjsModule(fn, module) { - return (module = { exports: {} }), fn(module, module.exports), module.exports; - } - - var regression = createCommonjsModule(function (module, exports) { - (function (global, factory) { - { - factory(module); - } - })(commonjsGlobal, function (module) { - function _defineProperty(obj, key, value) { - if (key in obj) { - Object.defineProperty(obj, key, { - value: value, - enumerable: true, - configurable: true, - writable: true - }); - } else { - obj[key] = value; - } - - return obj; - } - - var _extends = - Object.assign || - function (target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i]; - - for (var key in source) { - if (Object.prototype.hasOwnProperty.call(source, key)) { - target[key] = source[key]; - } - } - } - - return target; - }; - - function _toConsumableArray(arr) { - if (Array.isArray(arr)) { - for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { - arr2[i] = arr[i]; - } - - return arr2; - } else { - return Array.from(arr); - } - } - - var DEFAULT_OPTIONS = { order: 2, precision: 2, period: null }; - - /** - * Determine the coefficient of determination (r^2) of a fit from the observations - * and predictions. - * - * @param {Array>} data - Pairs of observed x-y values - * @param {Array>} results - Pairs of observed predicted x-y values - * - * @return {number} - The r^2 value, or NaN if one cannot be calculated. - */ - function determinationCoefficient(data, results) { - var predictions = []; - var observations = []; - - data.forEach(function (d, i) { - if (d[1] !== null) { - observations.push(d); - predictions.push(results[i]); - } - }); - - var sum = observations.reduce(function (a, observation) { - return a + observation[1]; - }, 0); - var mean = sum / observations.length; - - var ssyy = observations.reduce(function (a, observation) { - var difference = observation[1] - mean; - return a + difference * difference; - }, 0); - - var sse = observations.reduce(function (accum, observation, index) { - var prediction = predictions[index]; - var residual = observation[1] - prediction[1]; - return accum + residual * residual; - }, 0); - - return 1 - sse / ssyy; - } - - /** - * Determine the solution of a system of linear equations A * x = b using - * Gaussian elimination. - * - * @param {Array>} input - A 2-d matrix of data in row-major form [ A | b ] - * @param {number} order - How many degrees to solve for - * - * @return {Array} - Vector of normalized solution coefficients matrix (x) - */ - function gaussianElimination(input, order) { - var matrix = input; - var n = input.length - 1; - var coefficients = [order]; - - for (var i = 0; i < n; i++) { - var maxrow = i; - for (var j = i + 1; j < n; j++) { - if (Math.abs(matrix[i][j]) > Math.abs(matrix[i][maxrow])) { - maxrow = j; - } - } - - for (var k = i; k < n + 1; k++) { - var tmp = matrix[k][i]; - matrix[k][i] = matrix[k][maxrow]; - matrix[k][maxrow] = tmp; - } - - for (var _j = i + 1; _j < n; _j++) { - for (var _k = n; _k >= i; _k--) { - matrix[_k][_j] -= (matrix[_k][i] * matrix[i][_j]) / matrix[i][i]; - } - } - } - - for (var _j2 = n - 1; _j2 >= 0; _j2--) { - var total = 0; - for (var _k2 = _j2 + 1; _k2 < n; _k2++) { - total += matrix[_k2][_j2] * coefficients[_k2]; - } - - coefficients[_j2] = (matrix[n][_j2] - total) / matrix[_j2][_j2]; - } - - return coefficients; - } - - /** - * Round a number to a precision, specificed in number of decimal places - * - * @param {number} number - The number to round - * @param {number} precision - The number of decimal places to round to: - * > 0 means decimals, < 0 means powers of 10 - * - * - * @return {numbr} - The number, rounded - */ - function round(number, precision) { - var factor = Math.pow(10, precision); - return Math.round(number * factor) / factor; - } - - /** - * The set of all fitting methods - * - * @namespace - */ - var methods = { - linear: function linear(data, options) { - var sum = [0, 0, 0, 0, 0]; - var len = 0; - - for (var n = 0; n < data.length; n++) { - if (data[n][1] !== null) { - len++; - sum[0] += data[n][0]; - sum[1] += data[n][1]; - sum[2] += data[n][0] * data[n][0]; - sum[3] += data[n][0] * data[n][1]; - sum[4] += data[n][1] * data[n][1]; - } - } - - var run = len * sum[2] - sum[0] * sum[0]; - var rise = len * sum[3] - sum[0] * sum[1]; - var gradient = run === 0 ? 0 : round(rise / run, options.precision); - var intercept = round( - sum[1] / len - (gradient * sum[0]) / len, - options.precision - ); - - var predict = function predict(x) { - return [ - round(x, options.precision), - round(gradient * x + intercept, options.precision) - ]; - }; - - var points = data.map(function (point) { - return predict(point[0]); - }); - - return { - points: points, - predict: predict, - equation: [gradient, intercept], - r2: round(determinationCoefficient(data, points), options.precision), - string: - intercept === 0 - ? 'y = ' + gradient + 'x' - : 'y = ' + gradient + 'x + ' + intercept - }; - }, - exponential: function exponential(data, options) { - var sum = [0, 0, 0, 0, 0, 0]; - - for (var n = 0; n < data.length; n++) { - if (data[n][1] !== null) { - sum[0] += data[n][0]; - sum[1] += data[n][1]; - sum[2] += data[n][0] * data[n][0] * data[n][1]; - sum[3] += data[n][1] * Math.log(data[n][1]); - sum[4] += data[n][0] * data[n][1] * Math.log(data[n][1]); - sum[5] += data[n][0] * data[n][1]; - } - } - - var denominator = sum[1] * sum[2] - sum[5] * sum[5]; - var a = Math.exp((sum[2] * sum[3] - sum[5] * sum[4]) / denominator); - var b = (sum[1] * sum[4] - sum[5] * sum[3]) / denominator; - var coeffA = round(a, options.precision); - var coeffB = round(b, options.precision); - var predict = function predict(x) { - return [ - round(x, options.precision), - round(coeffA * Math.exp(coeffB * x), options.precision) - ]; - }; - - var points = data.map(function (point) { - return predict(point[0]); - }); - - return { - points: points, - predict: predict, - equation: [coeffA, coeffB], - string: 'y = ' + coeffA + 'e^(' + coeffB + 'x)', - r2: round(determinationCoefficient(data, points), options.precision) - }; - }, - logarithmic: function logarithmic(data, options) { - var sum = [0, 0, 0, 0]; - var len = data.length; - - for (var n = 0; n < len; n++) { - if (data[n][1] !== null) { - sum[0] += Math.log(data[n][0]); - sum[1] += data[n][1] * Math.log(data[n][0]); - sum[2] += data[n][1]; - sum[3] += Math.pow(Math.log(data[n][0]), 2); - } - } - - var a = (len * sum[1] - sum[2] * sum[0]) / (len * sum[3] - sum[0] * sum[0]); - var coeffB = round(a, options.precision); - var coeffA = round((sum[2] - coeffB * sum[0]) / len, options.precision); - - var predict = function predict(x) { - return [ - round(x, options.precision), - round( - round(coeffA + coeffB * Math.log(x), options.precision), - options.precision - ) - ]; - }; - - var points = data.map(function (point) { - return predict(point[0]); - }); - - return { - points: points, - predict: predict, - equation: [coeffA, coeffB], - string: 'y = ' + coeffA + ' + ' + coeffB + ' ln(x)', - r2: round(determinationCoefficient(data, points), options.precision) - }; - }, - power: function power(data, options) { - var sum = [0, 0, 0, 0, 0]; - var len = data.length; - - for (var n = 0; n < len; n++) { - if (data[n][1] !== null) { - sum[0] += Math.log(data[n][0]); - sum[1] += Math.log(data[n][1]) * Math.log(data[n][0]); - sum[2] += Math.log(data[n][1]); - sum[3] += Math.pow(Math.log(data[n][0]), 2); - } - } - - var b = (len * sum[1] - sum[0] * sum[2]) / (len * sum[3] - Math.pow(sum[0], 2)); - var a = (sum[2] - b * sum[0]) / len; - var coeffA = round(Math.exp(a), options.precision); - var coeffB = round(b, options.precision); - - var predict = function predict(x) { - return [ - round(x, options.precision), - round( - round(coeffA * Math.pow(x, coeffB), options.precision), - options.precision - ) - ]; - }; - - var points = data.map(function (point) { - return predict(point[0]); - }); - - return { - points: points, - predict: predict, - equation: [coeffA, coeffB], - string: 'y = ' + coeffA + 'x^' + coeffB, - r2: round(determinationCoefficient(data, points), options.precision) - }; - }, - polynomial: function polynomial(data, options) { - var lhs = []; - var rhs = []; - var a = 0; - var b = 0; - var len = data.length; - var k = options.order + 1; - - for (var i = 0; i < k; i++) { - for (var l = 0; l < len; l++) { - if (data[l][1] !== null) { - a += Math.pow(data[l][0], i) * data[l][1]; - } - } - - lhs.push(a); - a = 0; - - var c = []; - for (var j = 0; j < k; j++) { - for (var _l = 0; _l < len; _l++) { - if (data[_l][1] !== null) { - b += Math.pow(data[_l][0], i + j); - } - } - c.push(b); - b = 0; - } - rhs.push(c); - } - rhs.push(lhs); - - var coefficients = gaussianElimination(rhs, k).map(function (v) { - return round(v, options.precision); - }); - - var predict = function predict(x) { - return [ - round(x, options.precision), - round( - coefficients.reduce(function (sum, coeff, power) { - return sum + coeff * Math.pow(x, power); - }, 0), - options.precision - ) - ]; - }; - - var points = data.map(function (point) { - return predict(point[0]); - }); - - var string = 'y = '; - for (var _i = coefficients.length - 1; _i >= 0; _i--) { - if (_i > 1) { - string += coefficients[_i] + 'x^' + _i + ' + '; - } else if (_i === 1) { - string += coefficients[_i] + 'x + '; - } else { - string += coefficients[_i]; - } - } - - return { - string: string, - points: points, - predict: predict, - equation: [].concat(_toConsumableArray(coefficients)).reverse(), - r2: round(determinationCoefficient(data, points), options.precision) - }; - } - }; - - function createWrapper() { - var reduce = function reduce(accumulator, name) { - return _extends( - { - _round: round - }, - accumulator, - _defineProperty({}, name, function (data, supplied) { - return methods[name](data, _extends({}, DEFAULT_OPTIONS, supplied)); - }) - ); - }; - - return Object.keys(methods).reduce(reduce, {}); - } - - module.exports = createWrapper(); - }); - }); - - function addRegressionLine() { - if (this.config.add_regression_line) { - var chart = this; - var config = this.config; - - // map chart data to array and calculate regression using regression-js - var arrayData = chart.filtered_data - .filter(function (f) { - return !isNaN(f.delta_x); - }) - .filter(function (f) { - return !isNaN(f.delta_y); - }) - .map(function (d) { - return [+d.delta_x, +d.delta_y]; - }); - - var result = regression.linear(arrayData); - - //calculate predicted values for min and max points on the chart - var min_x = chart.x_dom[0]; - var min_xy = result.predict(min_x); - var max_x = chart.x_dom[1]; - var max_xy = result.predict(max_x); - - //draw the regression line - var line = d3.svg - .line() - .x(function (d) { - return chart.x(d[0]); - }) - .y(function (d) { - return chart.y(d[1]); - }); - chart.svg.selectAll('.regressionLine').remove(); - chart.svg - .append('path') - .classed('regressionLine', true) - .datum([min_xy, max_xy]) - .attr('d', line) - .attr('stroke', 'black') - .attr('stroke-dasharray', '3,5'); - - //add footnote with R2 and exact calculation - chart.wrap.select('span.regression-note').remove(); - chart.wrap - .append('span') - .classed('regression-note', true) - .html( - 'The dashed line shows the result of a simple linear regression. Additional details are shown below.
Equation: ' + - result.string + - '
R2: ' + - d3.format('0.2f')(result.r2) - ); - } - } - - function onResize() { - addBoxPlots.call(this); - updateClipPath.call(this); - addPointClick.call(this); - addRegressionLine.call(this); - } - - function onDestroy() { } - - var callbacks = { - onInit: onInit, - onLayout: onLayout, - onPreprocess: onPreprocess, - onDatatransform: onDatatransform, - onDraw: onDraw, - onResize: onResize, - onDestroy: onDestroy - }; - - function layout(element) { - var container = d3.select(element); - container - .append('div') - .classed('sdd-component', true) - .attr('id', 'sdd-controls'); - container - .append('div') - .classed('sdd-component', true) - .attr('id', 'sdd-chart'); - container - .append('div') - .classed('sdd-component', true) - .attr('id', 'sdd-listing'); - } - - function styles() { - var styles = [ - '#safety-delta-delta {' + ' width: 100%;' + ' display: inline-block;' + '}', - '.sdd-component {' + - ' margin: 0;' + - ' border: none;' + - ' padding: 0;' + - ' display: inline-block;' + - '}', - - //controls - '#sdd-controls {' + ' width: 25%;' + ' float: left;' + '}', - '#sdd-controls .control-group {' + - ' width: 98%;' + - ' margin: 0 2% 5px 0;' + - ' padding: 0;' + - '}', - '#sdd-controls .control-group > * {' + ' display: inline-block;' + '}', - '#sdd-controls .changer {' + ' float: right;' + ' width: 50%;' + '}', - '#sdd-controls .wc-control-label {' + - ' text-align: right;' + - ' width: 48%;' + - '}', - '#sdd-controls .annote {' + ' width: 98%;' + ' text-align: right;' + '}', - - //chart - '#sdd-chart {' + ' width: 36%;' + ' margin: 0 2%;' + '}', - '.wc-data-mark {' + ' cursor: pointer;' + '}', - '.wc-data-mark:hover {' + ' stroke: black;' + ' stroke-width: 3;' + '}', - '.regression-note {' + - //' font-size: 0.8em;' + - ' color: #999;' + - '}', - - //listing - '#sdd-listing {' + ' width: 35%;' + ' float: right;' + '}', - '#sdd-listing .wc-table table {' + ' width: 100%;' + ' display: table;' + '}', - '#sdd-listing .wc-table th:not(:first-child),' + - '#sdd-listing .wc-table td:not(:first-child) {' + - ' text-align: right;' + - '}', - '.sdd-axisLabel{' + - 'font-size:75%;' + - 'border-radius:0.25em;' + - 'padding:.2em .6em .3em;' + - 'margin-right:0.4em;' + - 'background-color:#5bc0de;' + - 'color:white;' + - 'font-weight:700;' + - '}' - ]; - var style = document.createElement('style'); - style.type = 'text/css'; - style.innerHTML = styles.join('\n'); - document.getElementsByTagName('head')[0].appendChild(style); - } - - function safetyDeltaDelta() { - var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'body'; - var settings = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - - //layout and styles - layout(element); - styles(); - - //Define chart. - var mergedSettings = Object.assign( - {}, - JSON.parse(JSON.stringify(configuration.settings)), - settings - ); - var syncedSettings = configuration.syncSettings(mergedSettings); - var syncedControlInputs = configuration.syncControlInputs( - configuration.controlInputs(), - syncedSettings - ); - var controls = webcharts.createControls( - document.querySelector(element).querySelector('#sdd-controls'), - { - location: 'top', - inputs: syncedControlInputs - } - ); - var chart = webcharts.createChart( - document.querySelector(element).querySelector('#sdd-chart'), - syncedSettings, - controls - ); - - //Define chart callbacks. - for (var callback in callbacks) { - chart.on(callback.substring(2).toLowerCase(), callbacks[callback]); - } //listing - var listing = webcharts.createTable( - document.querySelector(element).querySelector('#sdd-listing'), - configuration.listingSettings() - ); - listing.wrap.style('display', 'none'); // empty table's popping up briefly - listing.init([]); - chart.listing = listing; - listing.chart = chart; - - return chart; - } - - return safetyDeltaDelta; -}); \ No newline at end of file diff --git a/inst/htmlwidgets/lib/safety-histogram-2.4.1/safetyHistogram.js b/inst/htmlwidgets/lib/safety-histogram-2.4.1/safetyHistogram.js deleted file mode 100644 index c9d6667b..00000000 --- a/inst/htmlwidgets/lib/safety-histogram-2.4.1/safetyHistogram.js +++ /dev/null @@ -1,2205 +0,0 @@ -(function(global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' - ? (module.exports = factory(require('d3'), require('webcharts'))) - : typeof define === 'function' && define.amd - ? define(['d3', 'webcharts'], factory) - : (global.safetyHistogram = factory(global.d3, global.webCharts)); -})(this, function(d3, webcharts) { - 'use strict'; - - if (typeof Object.assign != 'function') { - Object.defineProperty(Object, 'assign', { - value: function assign(target, varArgs) { - if (target == null) { - // TypeError if undefined or null - throw new TypeError('Cannot convert undefined or null to object'); - } - - var to = Object(target); - - for (var index = 1; index < arguments.length; index++) { - var nextSource = arguments[index]; - - if (nextSource != null) { - // Skip over if undefined or null - for (var nextKey in nextSource) { - // Avoid bugs when hasOwnProperty is shadowed - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - - return to; - }, - writable: true, - configurable: true - }); - } - - if (!Array.prototype.find) { - Object.defineProperty(Array.prototype, 'find', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, 'length')). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return kValue. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return kValue; - } - // e. Increase k by 1. - k++; - } - - // 7. Return undefined. - return undefined; - } - }); - } - - if (!Array.prototype.findIndex) { - Object.defineProperty(Array.prototype, 'findIndex', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, "length")). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return k. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return k; - } - // e. Increase k by 1. - k++; - } - - // 7. Return -1. - return -1; - } - }); - } - - Math.log10 = Math.log10 = - Math.log10 || - function(x) { - return Math.log(x) * Math.LOG10E; - }; - - // https://github.com/wbkd/d3-extended - d3.selection.prototype.moveToFront = function() { - return this.each(function() { - this.parentNode.appendChild(this); - }); - }; - - d3.selection.prototype.moveToBack = function() { - return this.each(function() { - var firstChild = this.parentNode.firstChild; - if (firstChild) { - this.parentNode.insertBefore(this, firstChild); - } - }); - }; - - function rendererSettings() { - return { - //required variables - measure_col: 'TEST', - value_col: 'STRESN', - - //optional variables - id_col: 'USUBJID', - unit_col: 'STRESU', - normal_col_low: 'STNRLO', - normal_col_high: 'STNRHI', - filters: null, - details: null, - - //miscellaneous settings - start_value: null, - normal_range: true, - displayNormalRange: false, - bin_algorithm: "Scott's normal reference rule", - annotate_bin_boundaries: false - }; - } - - function webchartsSettings() { - return { - x: { - type: 'linear', - column: null, // set in ./syncSettings - label: null, // set in ../callbacks/onPreprocess/setXaxisLabel - domain: [null, null], // set in ../callbacks/onPreprocess/setXdomain - format: null, // set in ../callbacks/onPreprocess/calculateXPrecision - bin: null // set in ../callbacks/onPreprocess/defineMeasureData - }, - y: { - type: 'linear', - column: null, - label: '# of Observations', - domain: [0, null], - format: '1d', - behavior: 'flex' - }, - marks: [ - { - per: [], // set in ./syncSettings - type: 'bar', - summarizeX: 'mean', - summarizeY: 'count', - attributes: { 'fill-opacity': 0.75 } - } - ], - aspect: 3 - }; - } - - function syncSettings(settings) { - settings.x.column = settings.value_col; - settings.x.bin_algorithm = settings.bin_algorithm; - settings.marks[0].per[0] = settings.value_col; - - //update normal range settings if normal_range is set to false - if (!settings.normal_range) { - settings.normal_col_low = null; - settings.normal_col_high = null; - settings.displayNormalRange = false; - } - - //handle a string argument to filters - if (!(settings.filters instanceof Array)) - settings.filters = typeof settings.filters === 'string' ? [settings.filters] : []; - - //handle a string argument to details - if (!(settings.details instanceof Array)) - settings.details = typeof settings.details === 'string' ? [settings.details] : []; - - //Define default details. - var defaultDetails = [{ value_col: settings.id_col, label: 'Participant ID' }]; - if (Array.isArray(settings.filters)) - settings.filters - .filter(function(filter) { - return filter.value_col !== settings.id_col; - }) - .forEach(function(filter) { - return defaultDetails.push({ - value_col: filter.value_col ? filter.value_col : filter, - label: filter.label - ? filter.label - : filter.value_col - ? filter.value_col - : filter - }); - }); - defaultDetails.push({ value_col: settings.value_col, label: 'Result' }); - if (settings.normal_col_low) - defaultDetails.push({ - value_col: settings.normal_col_low, - label: 'Lower Limit of Normal' - }); - if (settings.normal_col_high) - defaultDetails.push({ - value_col: settings.normal_col_high, - label: 'Upper Limit of Normal' - }); - - //If [settings.details] is not specified: - if (!settings.details) settings.details = defaultDetails; - else { - //If [settings.details] is specified: - //Allow user to specify an array of columns or an array of objects with a column property - //and optionally a column label. - settings.details.forEach(function(detail) { - if ( - defaultDetails - .map(function(d) { - return d.value_col; - }) - .indexOf(detail.value_col ? detail.value_col : detail) === -1 - ) - defaultDetails.push({ - value_col: detail.value_col ? detail.value_col : detail, - label: detail.label - ? detail.label - : detail.value_col - ? detail.value_col - : detail - }); - }); - settings.details = defaultDetails; - } - - return settings; - } - - function controlInputs() { - return [ - { - type: 'subsetter', - value_col: 'sh_measure', - label: 'Measure', - start: null // set in ../callbacks/onInit/checkControls/updateMeasureFilter - }, - { - type: 'number', - option: 'x.domain[0]', - label: 'Lower', - require: true - }, - { - type: 'number', - option: 'x.domain[1]', - label: 'Upper', - require: true - }, - { - type: 'dropdown', - option: 'x.bin_algorithm', - label: 'Algorithm', - values: [ - 'Square-root choice', - "Sturges' formula", - 'Rice Rule', - //'Doane\'s formula', - "Scott's normal reference rule", - "Freedman-Diaconis' choice", - "Shimazaki and Shinomoto's choice", - 'Custom' - ], - require: true - }, - { - type: 'number', - option: 'x.bin', - label: 'Quantity' - }, - { - type: 'number', - option: 'x.bin_width', - label: 'Width' - }, - { - type: 'checkbox', - option: 'displayNormalRange', - label: 'Normal Range' - }, - { - type: 'radio', - option: 'annotate_bin_boundaries', - label: 'X-axis Ticks', - values: [false, true], - relabels: ['linear', 'bin boundaries'] - } - ]; - } - - function syncControlInputs(controlInputs, settings) { - //Add filters to default controls. - if (Array.isArray(settings.filters) && settings.filters.length > 0) { - var position = controlInputs.findIndex(function(input) { - return input.label === 'Algorithm'; - }); - settings.filters.forEach(function(filter) { - var filterObj = { - type: 'subsetter', - value_col: filter.value_col || filter, - label: filter.label || filter.value_col || filter - }; - controlInputs.splice(position, 0, filterObj); - ++position; - }); - } - - //Remove normal range control. - if (!settings.normal_range) - controlInputs.splice( - controlInputs.findIndex(function(input) { - return input.label === 'Normal Range'; - }), - 1 - ); - - return controlInputs; - } - - var configuration = { - rendererSettings: rendererSettings, - webchartsSettings: webchartsSettings, - settings: Object.assign({}, rendererSettings(), webchartsSettings()), - syncSettings: syncSettings, - controlInputs: controlInputs, - syncControlInputs: syncControlInputs - }; - - var properties = { - measure_col: { - title: 'Medical Sign', - description: 'a variable that contains the names of each medical sign', - type: 'string', - default: 'TEST', - 'data-mapping': true, - 'data-type': 'character', - required: true - }, - value_col: { - title: 'Result', - description: - 'a variable that contains the results for each medical sign; non-numeric results are removed with a notification thrown to the log', - type: 'string', - default: 'STRESN', - 'data-mapping': true, - 'data-type': 'numeric', - required: true - }, - id_col: { - title: 'ID', - description: 'a variable that contains IDs for each participant', - type: 'string', - default: 'USUBJID', - 'data-mapping': true, - 'data-type': 'character', - required: false - }, - unit_col: { - title: 'Unit', - description: 'a variable that contains the units of each medical sign', - type: 'string', - default: 'STRESU', - 'data-mapping': true, - 'data-type': 'character', - required: false - }, - normal_col_low: { - title: 'Lower Limit of Normal', - description: 'a variable that contains the lower limit of normal of the medical sign', - type: 'string', - default: 'STNRLO', - 'data-mapping': true, - 'data-type': 'numeric', - required: false - }, - normal_col_high: { - title: 'Upper Limit of Normal', - description: 'a variable that contains the upper limit of normal of the medical sign', - type: 'string', - default: 'STNRHI', - 'data-mapping': true, - 'data-type': 'numeric', - required: false - }, - filters: { - title: 'Filter Variables', - description: - 'an array of variables and metadata that will appear in the controls as data filters', - type: 'array', - items: { - properties: { - label: { - description: 'a description of the variable', - title: 'Variable Label', - type: 'string' - }, - value_col: { - description: 'the name of the variable', - title: 'Variable Name', - type: 'string' - } - }, - type: 'object' - }, - 'data-mapping': true, - 'data-type': 'either', - required: false - }, - details: { - title: 'Listing Variables', - description: 'an array of variables and metadata that will appear in the data listing', - type: 'array', - items: { - properties: { - label: { - description: 'a description of the variable', - title: 'Variable Label', - type: 'string' - }, - value_col: { - description: 'the name of the variable', - title: 'Variable Name', - type: 'string' - } - }, - type: 'object' - }, - 'data-mapping': true, - 'data-type': 'either', - required: false - }, - start_value: { - title: 'Initial Medical Sign', - description: - 'the name of the initially displayed medical sign; defaults to the first measure in the data', - type: 'string' - }, - normal_range: { - title: 'Generate Normal Range Control?', - description: - 'a boolean that dictates whether the normal range control will be generated', - type: 'boolean', - default: true - }, - displayNormalRange: { - title: 'Display Normal Range?', - description: - 'a boolean that dictates whether the normal range will be displayed initially', - type: 'boolean', - default: false - } - }; - - function checkRequired() { - var _this = this; - - this.variables.required = this.variables.definitions.filter(function(definition) { - return definition.required === true; - }); - this.variables.required.forEach(function(definition) { - if (_this.variables.actual.indexOf(definition.setting) < 0) { - definition.missing = true; - - //Define error text. - var codeStyle = [ - 'padding: 1px 5px', - 'white-space: prewrap', - 'font-family: Consolas,Lucida Console,Courier New,monospace,sans-serif', - 'background-color: #eff0f1' - ]; - var errorText = - "The variable specified for " + - definition.property + - ', ' + - definition.setting + - ', does not exist in the data.'; - - //Print error to console. - console.error(errorText.replace(/<.+?>/g, '')); - - //Print error to containing element. - var div = d3.select(_this.div); - div.append('p') - .html(errorText) - .style('color', 'red'); - } - }); - - //Destroy chart. - if ( - this.variables.required.some(function(definition) { - return definition.missing; - }) - ) - this.destroy(); - } - - function checkOptional() { - var _this = this; - - this.variables.optional = this.variables.definitions.filter(function(definition) { - return definition.required === false; - }); - - this.variables.optional.forEach(function(definition) { - if (definition.type === 'string') { - if (_this.variables.actual.indexOf(definition.setting) < 0) { - definition.missing = true; - console.warn( - 'The variable specified for [ ' + - definition.property + - ' ], ' + - definition.setting + - ', does not exist in the data.' - ); - } - } // standard data mappings - else if ( - definition.type === 'array' && - Array.isArray(definition.setting) && - definition.setting.length - ) { - definition.setting.forEach(function(subDefinition, i) { - var variable = subDefinition.value_col || subDefinition; - if (_this.variables.actual.indexOf(variable) < 0) { - definition.missing = true; - console.warn( - 'The variable specified for [ ' + - definition.property + - '[' + - i + - '] ], ' + - variable + - ', does not exist in the data.' - ); - } - }); - } // optional variable arrays (filters, listing columns) - - //Remove participant ID column from listing if variable is missing. - if (definition.property === 'id_col' && definition.missing) { - var index = _this.listing.config.cols.findIndex(function(col) { - return col === definition.setting; - }); - _this.listing.config.cols.splice(index, 1); - _this.listing.config.headers.splice(index, 1); - } - }); - } - - function checkVariables() { - var _this = this; - - this.variables = { - actual: Object.keys(this.raw_data[0]), - definitions: Object.keys(properties) - .map(function(property) { - var definition = properties[property]; - definition.property = property; - definition.setting = _this.config[property]; - return definition; - }) - .filter(function(definition) { - return definition['data-mapping']; - }) - }; - checkRequired.call(this); - checkOptional.call(this); - } - - function countParticipants() { - var _this = this; - - this.participantCount = { - N: d3 - .set( - this.raw_data.map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values() - .filter(function(value) { - return !/^\s*$/.test(value); - }).length, - container: null, // set in ../onLayout/addParticipantCountContainer - n: null, // set in ../onDraw/updateParticipantCount - percentage: null // set in ../onDraw/updateParticipantCount - }; - } - - function removeMissingResults() { - var _this = this; - - //Split data into records with missing and nonmissing results. - var missingResults = []; - var nonMissingResults = []; - this.raw_data.forEach(function(d) { - if (/^\s*$/.test(d[_this.config.value_col])) missingResults.push(d); - else nonMissingResults.push(d); - }); - - //Nest missing and nonmissing results by participant. - var participantsWithMissingResults = d3 - .nest() - .key(function(d) { - return d[_this.config.id_col]; - }) - .rollup(function(d) { - return d.length; - }) - .entries(missingResults); - var participantsWithNonMissingResults = d3 - .nest() - .key(function(d) { - return d[_this.config.id_col]; - }) - .rollup(function(d) { - return d.length; - }) - .entries(nonMissingResults); - - //Identify placeholder records, i.e. participants with a single missing result. - this.removedRecords.placeholderRecords = participantsWithMissingResults - .filter(function(d) { - return ( - participantsWithNonMissingResults - .map(function(d) { - return d.key; - }) - .indexOf(d.key) < 0 && d.values === 1 - ); - }) - .map(function(d) { - return d.key; - }); - if (this.removedRecords.placeholderRecords.length) - console.log( - this.removedRecords.placeholderRecords.length + - ' participants without results have been detected.' - ); - - //Count the number of records with missing results. - this.removedRecords.missing = d3.sum( - participantsWithMissingResults.filter(function(d) { - return _this.removedRecords.placeholderRecords.indexOf(d.key) < 0; - }), - function(d) { - return d.values; - } - ); - if (this.removedRecords.missing > 0) - console.warn( - this.removedRecords.missing + - ' record' + - (this.removedRecords.missing > 1 - ? 's with a missing result have' - : ' with a missing result has') + - ' been removed.' - ); - - //Update data. - this.raw_data = nonMissingResults; - } - - function removeNonNumericResults() { - var _this = this; - - //Filter out non-numeric results. - var numericResults = this.raw_data.filter(function(d) { - return /^-?[0-9.]+$/.test(d[_this.config.value_col]); - }); - this.removedRecords.nonNumeric = this.raw_data.length - numericResults.length; - if (this.removedRecords.nonNumeric > 0) - console.warn( - this.removedRecords.nonNumeric + - ' record' + - (this.removedRecords.nonNumeric > 1 - ? 's with a non-numeric result have' - : ' with a non-numeric result has') + - ' been removed.' - ); - - //Update data. - this.raw_data = numericResults; - } - - function cleanData() { - this.removedRecords = { - placeholderParticipants: null, // defined in './cleanData/removeMissingResults - missing: null, // defined in './cleanData/removeMissingResults - nonNumeric: null, // defined in './cleanData/removeNonNumericResults - container: null // defined in ../onLayout/addRemovedRecordsContainer - }; - removeMissingResults.call(this); - removeNonNumericResults.call(this); - this.initial_data = this.raw_data; - } - - function addVariables() { - var _this = this; - - this.raw_data.forEach(function(d) { - //Concatenate unit to measure if provided. - d[_this.config.measure_col] = d[_this.config.measure_col].trim(); - d.sh_measure = d.hasOwnProperty(_this.config.unit_col) - ? d[_this.config.measure_col] + ' (' + d[_this.config.unit_col] + ')' - : d[_this.config.measure_col]; - }); - } - - function participant() { - var _this = this; - - this.participants = d3 - .set( - this.initial_data.map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values() - .sort(); - } - - function measure() { - var _this = this; - - this.measures = d3 - .set( - this.initial_data.map(function(d) { - return d[_this.config.measure_col]; - }) - ) - .values() - .sort(); - this.sh_measures = d3 - .set( - this.initial_data.map(function(d) { - return d.sh_measure; - }) - ) - .values() - .sort(); - } - - function defineSets() { - participant.call(this); - measure.call(this); - } - - function updateMeasureFilter() { - this.measure = {}; - var measureInput = this.controls.config.inputs.find(function(input) { - return input.label === 'Measure'; - }); - if ( - this.config.start_value && - this.sh_measures.indexOf(this.config.start_value) < 0 && - this.measures.indexOf(this.config.start_value) < 0 - ) { - measureInput.start = this.sh_measures[0]; - console.warn( - this.config.start_value + - ' is an invalid measure. Defaulting to ' + - measureInput.start + - '.' - ); - } else if ( - this.config.start_value && - this.sh_measures.indexOf(this.config.start_value) < 0 - ) { - measureInput.start = this.sh_measures[this.measures.indexOf(this.config.start_value)]; - console.warn( - this.config.start_value + - ' is missing the units value. Defaulting to ' + - measureInput.start + - '.' - ); - } else measureInput.start = this.config.start_value || this.sh_measures[0]; - } - - function removeFilters() { - var _this = this; - - this.controls.config.inputs = this.controls.config.inputs.filter(function(input) { - if (input.type !== 'subsetter' || input.value_col === 'sh_measure') { - return true; - } else if (!_this.raw_data[0].hasOwnProperty(input.value_col)) { - console.warn( - 'The [ ' + - input.label + - ' ] filter has been removed because the variable does not exist.' - ); - } else { - var levels = d3 - .set( - _this.raw_data.map(function(d) { - return d[input.value_col]; - }) - ) - .values(); - - if (levels.length === 1) - console.warn( - 'The [ ' + - input.label + - ' ] filter has been removed because the variable has only one level.' - ); - - return levels.length > 1; - } - }); - } - - function checkControls() { - updateMeasureFilter.call(this); - removeFilters.call(this); - } - - function onInit() { - // 0. Check variables. - checkVariables.call(this); - - // 1. Count total participants prior to data cleaning. - countParticipants.call(this); - - // 2. Drop missing values and remove measures with any non-numeric results. - cleanData.call(this); - - // 3. Define additional variables. - addVariables.call(this); - - // 4. Define sets. - defineSets.call(this); - - // 5. Check controls. - checkControls.call(this); - } - - function identifyControls() { - var context = this; - - var controlGroups = this.controls.wrap - .style('padding-bottom', '8px') - .selectAll('.control-group'); - - //Give each control a unique ID. - controlGroups - .attr('id', function(d) { - return d.label.toLowerCase().replace(/ /g, '-'); - }) - .each(function(d) { - var controlGroup = d3.select(this); - controlGroup.classed(d.type, true); - context.controls[d.label] = controlGroup; - }); - - //Give x-axis controls a common class name. - controlGroups - .filter(function(d) { - return ['x.domain[0]', 'x.domain[1]'].indexOf(d.option) > -1; - }) - .classed('x-axis', true); - - //Give binning controls a common class name. - controlGroups - .filter(function(d) { - return ['x.bin_algorithm', 'x.bin', 'x.bin_width'].indexOf(d.option) > -1; - }) - .classed('bin', true); - } - - function addXdomainResetButton() { - var _this = this; - - //Add x-domain reset button container. - this.controls.reset = { - container: this.controls.wrap - .insert('div', '#lower') - .classed('control-group x-axis', true) - .datum({ - type: 'button', - option: 'x.domain', - label: '' - }) - .style('vertical-align', 'bottom') - }; - - //Add label. - this.controls.reset.label = this.controls.reset.container - .append('span') - .attr('class', 'wc-control-label') - .text(''); - - //Add button. - this.controls.reset.button = this.controls.reset.container - .append('button') - .text(' Reset ') - .style('padding', '0px 5px') - .on('click', function() { - _this.config.x.domain = _this.measure.raw.domain; - - _this.controls.wrap - .selectAll('.control-group') - .filter(function(f) { - return f.option === 'x.domain[0]'; - }) - .select('input') - .property('value', _this.config.x.domain[0]); - - _this.controls.wrap - .selectAll('.control-group') - .filter(function(f) { - return f.option === 'x.domain[1]'; - }) - .select('input') - .property('value', _this.config.x.domain[1]); - - _this.draw(); - }); - } - - function insertGrouping(selector, label) { - var className = label.toLowerCase().replace(/ /g, '-') + '-grouping'; - var div = this.controls.wrap - .insert('div', selector) - .classed(className + '-div', true) - .style({ - display: 'inline-block', - 'margin-right': '5px' - }); - var fieldset = div - .append('fieldset') - .classed(className + '-fieldset', true) - .style('padding', '0px 2px'); - var legend = fieldset - .append('legend') - .classed(className + '-legend', true) - .text(label); - this.controls.wrap.selectAll(selector).each(function(d) { - this.style.marginTop = '0px'; - this.style.marginRight = '2px'; - this.style.marginBottom = '2px'; - this.style.marginLeft = '2px'; - fieldset.node().appendChild(this); - }); - } - - function groupControls() { - //Group x-axis controls. - insertGrouping.call(this, '.x-axis', 'X-axis Limits'); - - //Group filters. - if (this.filters.length > 1) - insertGrouping.call(this, '.subsetter:not(#measure)', 'Filters'); - - //Group bin controls. - insertGrouping.call(this, '.bin', 'Bins'); - } - - function addXdomainZoomButton() { - var _this = this; - - if ( - this.filters.find(function(filter) { - return filter.col !== 'sh_measure'; - }) - ) { - //Add x-domain zoom button container. - var resetContainer = this.controls.wrap - .select('.x-axis-limits-grouping-fieldset') - .append('div') - .classed('control-group x-axis', true) - .datum({ - type: 'button', - option: 'x.domain', - label: '' - }) - .attr('title', 'Zoom in on filtered histogram.') - .style({ - 'vertical-align': 'bottom', - 'margin-top': '0px', - 'margin-right': '2px', - 'margin-bottom': '2px', - 'margin-left': '2px' - }); - - //Add label. - resetContainer - .append('span') - .attr('class', 'wc-control-label') - .text(''); - - //Add button. - resetContainer - .append('button') - .text(' Zoom ') - .style('padding', '0px 5px') - .on('click', function() { - _this.config.x.domain = _this.measure.filtered.domain; - - _this.controls.wrap - .selectAll('.control-group') - .filter(function(f) { - return f.option === 'x.domain[0]'; - }) - .select('input') - .property('value', _this.config.x.domain[0]); - - _this.controls.wrap - .selectAll('.control-group') - .filter(function(f) { - return f.option === 'x.domain[1]'; - }) - .select('input') - .property('value', _this.config.x.domain[1]); - - _this.draw(); - }); - } - } - - function customizeBinsEventListener() { - var _this = this; - - var context = this; - - this.controls.Algorithm.selectAll('.wc-control-label') - .append('span') - .classed('algorithm-explanation', true) - .html(' ⓘ') - .style('cursor', 'pointer') - .on('click', function() { - if (_this.config.x.bin_algorithm !== 'Custom') - window.open( - 'https://en.wikipedia.org/wiki/Histogram#' + - _this.config.x.bin_algorithm - .replace(/ /g, '_') - .replace('Freedman-Diaconis', 'Freedman%E2%80%93Diaconis') - ); - }); - - this.controls.Quantity.selectAll('input') - .attr({ - min: 1, - step: 1 - }) - .on('change', function(d) { - if (this.value < 1) this.value = 1; - if (this.value % 1) this.value = Math.round(this.value); - context.config.x.bin = this.value; - context.config.x.bin_algorithm = 'Custom'; - context.controls.Algorithm.selectAll('option').property('selected', function(di) { - return di === 'Custom'; - }); - context.draw(); - }); - - this.controls.Width.selectAll('input').property('disabled', true); - } - - function addParticipantCountContainer() { - this.participantCount.container = this.controls.wrap - .style('position', 'relative') - .append('div') - .attr('id', 'participant-count') - .style({ - position: 'absolute', - 'font-style': 'italic', - bottom: '-10px', - left: 0, - display: this.variables.optional.find(function(definition) { - return definition.property === 'id_col'; - }).missing - ? 'none' - : 'block' - }); - } - - function addRemovedRecordsNote() { - var _this = this; - - if (this.removedRecords.missing > 0 || this.removedRecords.nonNumeric > 0) { - var message = - this.removedRecords.missing > 0 && this.removedRecords.nonNumeric > 0 - ? this.removedRecords.missing + - ' record' + - (this.removedRecords.missing > 1 ? 's' : '') + - ' with a missing result and ' + - this.removedRecords.nonNumeric + - ' record' + - (this.removedRecords.nonNumeric > 1 ? 's' : '') + - ' with a non-numeric result were removed.' - : this.removedRecords.missing > 0 - ? this.removedRecords.missing + - ' record' + - (this.removedRecords.missing > 1 ? 's' : '') + - ' with a missing result ' + - (this.removedRecords.missing > 1 ? 'were' : 'was') + - ' removed.' - : this.removedRecords.nonNumeric > 0 - ? this.removedRecords.nonNumeric + - ' record' + - (this.removedRecords.nonNumeric > 1 ? 's' : '') + - ' with a non-numeric result ' + - (this.removedRecords.nonNumeric > 1 ? 'were' : 'was') + - ' removed.' - : ''; - this.removedRecords.container = this.controls.wrap - .append('div') - .style({ - position: 'absolute', - 'font-style': 'italic', - bottom: '-10px', - right: 0 - }) - .text(message); - this.removedRecords.container - .append('span') - .style({ - color: 'blue', - 'text-decoration': 'underline', - 'font-style': 'normal', - 'font-weight': 'bold', - cursor: 'pointer', - 'font-size': '16px', - 'margin-left': '5px' - }) - .html('x') - .on('click', function() { - return _this.removedRecords.container.style('display', 'none'); - }); - } - } - - function addBorderAboveChart() { - this.wrap.style({ - 'border-top': '1px solid #ccc' - }); - } - - function addFootnoteContainer() { - this.footnotes = { - container: this.wrap - .insert('div', '.wc-chart') - .classed('footnotes', true) - .style({ - 'border-top': '1px solid #ccc', - 'padding-top': '10px' - }) - }; - this.footnotes.barClick = this.footnotes.container - .append('p') - .classed('footnote footnote--bar-click', true) - .text('Click a bar for details.'); - this.footnotes.barDetails = this.footnotes.container - .append('p') - .classed('footnote footnote--bar-details', true); - } - - function onLayout() { - identifyControls.call(this); - addXdomainResetButton.call(this); - groupControls.call(this); - addXdomainZoomButton.call(this); - customizeBinsEventListener.call(this); - addParticipantCountContainer.call(this); - addRemovedRecordsNote.call(this); - addBorderAboveChart.call(this); - addFootnoteContainer.call(this); - } - - function getCurrentMeasure() { - this.measure.previous = this.measure.current; - this.measure.current = this.controls.wrap.selectAll('#measure option:checked').text(); - this.config.x.label = this.measure.current; - if (this.measure.current !== this.measure.previous) this.config.x.custom_bin = false; - } - - function defineMeasureData() { - var _this = this; - - //Filter data on selected measure. - this.measure.raw = { - data: this.initial_data.filter(function(d) { - return d.sh_measure === _this.measure.current; - }) - }; - - //Apply other filters to measure data. - this.measure.filtered = { - data: this.measure.raw.data - }; - this.filters.forEach(function(filter) { - _this.measure.filtered.data = _this.measure.filtered.data.filter(function(d) { - return filter.val === 'All' - ? true - : Array.isArray(filter.val) - ? filter.val.includes(d[filter.col]) - : filter.val === d[filter.col]; - }); - }); - - //Filter results on current x-domain. - if (this.measure.current !== this.measure.previous) - this.config.x.domain = d3.extent( - this.measure.raw.data.map(function(d) { - return +d[_this.config.value_col]; - }) - ); - this.measure.custom = { - data: this.measure.raw.data.filter(function(d) { - return ( - _this.config.x.domain[0] <= +d[_this.config.value_col] && - +d[_this.config.value_col] <= _this.config.x.domain[1] - ); - }) - }; - - //Define arrays of results, unique results, and extent of results. - ['raw', 'custom', 'filtered'].forEach(function(property) { - var obj = _this.measure[property]; - - //Define array of all and unique results. - obj.results = obj.data - .map(function(d) { - return +d[_this.config.value_col]; - }) - .sort(function(a, b) { - return a - b; - }); - obj.uniqueResults = d3.set(obj.results).values(); - - //Calculate extent of data. - obj.domain = property !== 'custom' ? d3.extent(obj.results) : _this.config.x.domain; - }); - } - - function setXdomain() { - if (this.measure.current !== this.measure.previous) - this.config.x.domain = this.measure.raw.domain; - else if (this.config.x.domain[0] > this.config.x.domain[1]) this.config.x.domain.reverse(); - - //The x-domain can be in three states: - //- the extent of all results - //- user-defined, e.g. narrower to exclude outliers - // - //Bin width is calculated with two variables: - //- the interquartile range - //- the number of results - // - //1 When the x-domain is set to the extent of all results, the bin width should be calculated - // with the unfiltered set of results, regardless of the state of the current filters. - // - //2 Given a user-defined x-domain, the bin width should be calculated with the results that - // fall inside the current domain. - this.measure.domain_state = - (this.config.x.domain[0] === this.measure.raw.domain[0] && - this.config.x.domain[1] === this.measure.raw.domain[1]) || - this.measure.previous === undefined - ? 'raw' - : 'custom'; - - //Set chart data to measure data. - this.raw_data = this.measure[this.measure.domain_state].data.slice(); - } - - function calculateStatistics(obj) { - var _this = this; - - ['raw', 'custom'].forEach(function(property) { - var obj = _this.measure[property]; - - //Calculate statistics. - obj.stats = { - n: obj.results.length, - nUnique: obj.uniqueResults.length, - min: obj.domain[0], - q25: d3.quantile(obj.results, 0.25), - median: d3.quantile(obj.results, 0.5), - q75: d3.quantile(obj.results, 0.75), - max: obj.domain[1], - range: obj.domain[1] - obj.domain[0], - std: d3.deviation(obj.results) - }; - obj.stats.log10range = obj.stats.range > 0 ? Math.log10(obj.stats.range) : NaN; - obj.stats.iqr = obj.stats.q75 - obj.stats.q25; - }); - } - - function calculateSquareRootBinWidth(obj) { - //https://en.wikipedia.org/wiki/Histogram#Square-root_choice - var range = this.config.x.domain[1] - this.config.x.domain[0]; - obj.stats.SquareRootBins = Math.ceil(Math.sqrt(obj.stats.n)); - obj.stats.SquareRootBinWidth = range / obj.stats.SquareRootBins; - } - - function calculateSturgesBinWidth(obj) { - //https://en.wikipedia.org/wiki/Histogram#Sturges'_formula - var range = this.config.x.domain[1] - this.config.x.domain[0]; - obj.stats.SturgesBins = Math.ceil(Math.log2(obj.stats.n)) + 1; - obj.stats.SturgesBinWidth = range / obj.stats.SturgesBins; - } - - function calculateRiceBinWidth(obj) { - //https://en.wikipedia.org/wiki/Histogram#Rice_Rule - var range = this.config.x.domain[1] - this.config.x.domain[0]; - obj.stats.RiceBins = Math.ceil(2 * Math.pow(obj.stats.n, 1.0 / 3.0)); - obj.stats.RiceBinWidth = range / obj.stats.RiceBins; - } - - function calculateScottBinWidth(obj) { - //https://en.wikipedia.org/wiki/Histogram#Scott's_normal_reference_rule - var range = this.config.x.domain[1] - this.config.x.domain[0]; - obj.stats.ScottBinWidth = (3.5 * obj.stats.std) / Math.pow(obj.stats.n, 1.0 / 3.0); - obj.stats.ScottBins = - obj.stats.ScottBinWidth > 0 - ? Math.max(Math.ceil(range / obj.stats.ScottBinWidth), 5) - : NaN; - } - - function calculateFDBinWidth(obj) { - //https://en.wikipedia.org/wiki/Histogram#Freedman%E2%80%93Diaconis'_choice - var range = this.config.x.domain[1] - this.config.x.domain[0]; - obj.stats.FDBinWidth = (2 * obj.stats.iqr) / Math.pow(obj.stats.n, 1.0 / 3.0); - obj.stats.FDBins = - obj.stats.FDBinWidth > 0 ? Math.max(Math.ceil(range / obj.stats.FDBinWidth), 5) : NaN; - } - - var toConsumableArray = function(arr) { - if (Array.isArray(arr)) { - for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; - - return arr2; - } else { - return Array.from(arr); - } - }; - - function calculateSSBinWidth(obj) { - //https://en.wikipedia.org/wiki/Histogram#Shimazaki_and_Shinomoto's_choice - var nBins = d3.range(2, 100); // number of bins - var cost = d3.range(nBins.length); // cost function results - var binWidths = [].concat(toConsumableArray(cost)); // bin widths - var binBoundaries = [].concat(toConsumableArray(cost)); // bin boundaries - var bins = [].concat(toConsumableArray(cost)); // bins - var binSizes = [].concat(toConsumableArray(cost)); // bin lengths - var meanBinSizes = [].concat(toConsumableArray(cost)); // mean of bin lengths - var residuals = [].concat(toConsumableArray(cost)); // residuals - - var _loop = function _loop(i) { - binWidths[i] = obj.stats.range / nBins[i]; - binBoundaries[i] = d3.range(obj.stats.min, obj.stats.max, obj.stats.range / nBins[i]); - bins[i] = d3.layout.histogram().bins(nBins[i] - 1)( - /*.bins(binBoundaries[i])*/ obj.results - ); - binSizes[i] = bins[i].map(function(arr) { - return arr.length; - }); - meanBinSizes[i] = d3.mean(binSizes[i]); - residuals[i] = - d3.sum( - binSizes[i].map(function(binSize) { - return Math.pow(binSize - meanBinSizes[i], 2); - }) - ) / nBins[i]; - cost[i] = (2 * meanBinSizes[i] - residuals[i]) / Math.pow(binWidths[i], 2); - }; - - for (var i = 0; i < nBins.length; i++) { - _loop(i); - } - - //consoleLogVars( - // { - // nBins, - // binWidths, - // binBoundaries, - // //bins, - // binSizes, - // meanBinSizes, - // residuals, - // cost - // }, - // 5 - //); - - var minCost = d3.min(cost); - var idx = cost.findIndex(function(c) { - return c === minCost; - }); - - obj.stats.SSBinWidth = binWidths[idx]; - obj.stats.SSBins = nBins[idx]; - //const optBinBoundaries = range(obj.stats.min, obj.stats.max, obj.stats.range/optNBins); - } - - function calcualteBinWidth() { - var _this = this; - - ['raw', 'custom'].forEach(function(property) { - var obj = _this.measure[property]; - - //Calculate bin width with the selected algorithm. - switch (_this.config.x.bin_algorithm) { - case 'Square-root choice': - calculateSquareRootBinWidth.call(_this, obj); - obj.stats.nBins = - obj.stats.SquareRootBins < obj.stats.nUnique - ? obj.stats.SquareRootBins - : obj.stats.nUnique; - break; - case "Sturges' formula": - calculateSturgesBinWidth.call(_this, obj); - obj.stats.nBins = - obj.stats.SturgesBins < obj.stats.nUnique - ? obj.stats.SturgesBins - : obj.stats.nUnique; - break; - case 'Rice Rule': - calculateRiceBinWidth.call(_this, obj); - obj.stats.nBins = - obj.stats.RiceBins < obj.stats.nUnique - ? obj.stats.RiceBins - : obj.stats.nUnique; - break; - //case 'Doane\'s formula': - // console.log(4); - // calculateDoaneBinWidth.call(this, obj); - // obj.stats.nBins = - // obj.stats.DoaneBins < obj.stats.nUnique ? obj.stats.DoaneBins : obj.stats.nUnique; - // break; - case "Scott's normal reference rule": - calculateScottBinWidth.call(_this, obj); - obj.stats.nBins = - obj.stats.ScottBins < obj.stats.nUnique - ? obj.stats.ScottBins - : obj.stats.nUnique; - break; - case "Freedman-Diaconis' choice": - calculateFDBinWidth.call(_this, obj); - obj.stats.nBins = - obj.stats.FDBins < obj.stats.nUnique ? obj.stats.FDBins : obj.stats.nUnique; - break; - case "Shimazaki and Shinomoto's choice": - calculateSSBinWidth.call(_this, obj); - obj.stats.nBins = - obj.stats.SSBins < obj.stats.nUnique ? obj.stats.SSBins : obj.stats.nUnique; - break; - default: - //Handle custom number of bins. - obj.stats.nBins = _this.config.x.bin; - //obj.stats.binWidth = this.config.x.domain[1] - this.config.x.domain[0] / this.config.x.bin; - } - - //Calculate bin width. - obj.stats.binWidth = obj.stats.range / obj.stats.nBins; - obj.stats.binBoundaries = d3.range(obj.stats.nBins).concat(obj.domain[1]); - }); - - //Update chart config and set chart data to measure data. - this.config.x.bin = this.measure[this.measure.domain_state].stats.nBins; - this.config.x.bin_width = this.measure[this.measure.domain_state].stats.binWidth; - } - - function calculateXPrecision() { - //define the precision of the x-axis - this.config.x.precisionFactor = Math.round( - this.measure[this.measure.domain_state].stats.log10range - ); - this.config.x.precision = Math.pow(10, this.config.x.precisionFactor); - - //x-axis format - this.config.x.format = - this.config.x.precisionFactor > 0 - ? '.0f' - : '.' + (Math.abs(this.config.x.precisionFactor) + 1) + 'f'; - this.config.x.d3format = d3.format(this.config.x.format); - - //one more precision please: bin format - this.config.x.format1 = - this.config.x.precisionFactor > 0 - ? '.1f' - : '.' + (Math.abs(this.config.x.precisionFactor) + 2) + 'f'; - this.config.x.d3format1 = d3.format(this.config.x.format1); - - //define the size of the x-axis limit increments - var step = - this.measure[this.measure.domain_state].stats.range > 0 - ? Math.abs(this.measure[this.measure.domain_state].stats.range / 15) // non-zero range - : this.measure[this.measure.domain_state].results[0] !== 0 - ? Math.abs(this.measure[this.measure.domain_state].results[0] / 15) // zero range, non-zero result(s) - : 1; // zero range, zero result(s) - if (step < 1) { - var x10 = 0; - do { - step = step * 10; - ++x10; - } while (step < 1); - step = Math.round(step) / Math.pow(10, x10); - } else step = Math.round(step); - this.measure.step = step || 1; - } - - function updateXAxisResetButton() { - //Update tooltip of x-axis domain reset button. - if (this.measure.current !== this.measure.previous) { - this.controls.reset.container.attr( - 'title', - 'Initial Limits: [' + - this.config.x.d3format1(this.config.x.domain[0]) + - ' - ' + - this.config.x.d3format1(this.config.x.domain[1]) + - ']' - ); - } - } - - function updateXAxisLimits() { - this.controls.wrap - .selectAll('#lower input') - .attr('step', this.measure.step) // set in ./calculateXPrecision - .style('box-shadow', 'none') - .property('value', this.config.x.d3format1(this.config.x.domain[0])); - - this.controls.wrap - .selectAll('#upper input') - .attr('step', this.measure.step) // set in ./calculateXPrecision - .style('box-shadow', 'none') - .property('value', this.config.x.d3format1(this.config.x.domain[1])); - } - - function updateBinAlogrithm() { - this.controls.Algorithm.selectAll('.algorithm-explanation') - .style('display', this.config.x.bin_algorithm !== 'Custom' ? null : 'none') - .attr( - 'title', - this.config.x.bin_algorithm !== 'Custom' - ? 'View information on ' + this.config.x.bin_algorithm - : null - ); - } - - function updateBinWidth() { - this.controls.Width.selectAll('input').property( - 'value', - this.config.x.d3format1(this.config.x.bin_width) - ); - } - - function updateBinQuantity() { - this.controls.Quantity.selectAll('input').property('value', this.config.x.bin); - } - - function updateControls() { - updateXAxisResetButton.call(this); - updateXAxisLimits.call(this); - updateBinAlogrithm.call(this); - updateBinWidth.call(this); - updateBinQuantity.call(this); - } - - function defineBinBoundaries() { - var _this = this; - - var obj = this.measure[this.measure.domain_state]; - this.measure.binBoundaries = obj.stats.binBoundaries.map(function(d, i) { - var value = obj.domain[0] + obj.stats.binWidth * i; - return { - value: value, - value1: _this.config.x.d3format(value), - value2: _this.config.x.d3format1(value) - }; - }); - } - - function onPreprocess() { - // 1. Capture currently selected measure - needed in 2a. - getCurrentMeasure.call(this); - - // 2. Filter data on currently selected measure - needed in 3a and 3b. - defineMeasureData.call(this); - - // 3a Set x-domain given currently selected measure - needed in 4a and 4b. - setXdomain.call(this); - - // 3b Calculate statistics - needed in 4a and 4b. - calculateStatistics.call(this); - - // 4a Define precision of measure - needed in step 5a and 5b. - calculateXPrecision.call(this); - - // 4b Calculate bin width - needed in step 5c. - calcualteBinWidth.call(this); - - // 5a Update x-axis and bin controls after. - updateControls.call(this); - - // 5b Define bin boundaries given bin width and precision. - defineBinBoundaries.call(this); - } - - function onDatatransform() {} - - function updateParticipantCount() { - var _this = this; - - //count the number of unique ids in the current chart and calculate the percentage - this.participantCount.n = d3 - .set( - this.filtered_data.map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values().length; - this.participantCount.percentage = d3.format('0.1%')( - this.participantCount.n / this.participantCount.N - ); - - //clear the annotation - this.participantCount.container.selectAll('*').remove(); - - //update the annotation - this.participantCount.container.text( - '\n' + - this.participantCount.n + - ' of ' + - this.participantCount.N + - ' participant(s) shown (' + - this.participantCount.percentage + - ')' - ); - } - - function resetRenderer() { - delete this.highlightedBin; - delete this.highlighteD; - - //Remove bin boundaries. - this.svg.select('g.bin-boundaries').remove(); - - //Reset bar highlighting. - this.svg - .selectAll('.bar-group') - .classed('selected', false) - .selectAll('.bar') - .attr('fill-opacity', 0.75); - - //Reset footnotes. - this.footnotes.barClick - .style({ - 'text-decoration': 'none', - cursor: 'normal' - }) - .text('Click a bar for details.'); - this.footnotes.barDetails.text(''); - - //Reset listing. - this.listing.draw([]); - this.listing.wrap.style('display', 'none'); - } - - function increasePrecision() { - var _this = this; - - var ticks = this.x.ticks().map(function(d) { - return _this.config.x.d3format(d); - }); - if ( - d3 - .nest() - .key(function(d) { - return d; - }) - .rollup(function(d) { - return d.length; - }) - .entries(ticks) - .some(function(d) { - return d.values > 1; - }) - ) - this.config.x.format = this.config.x.format1; - } - - function onDraw() { - updateParticipantCount.call(this); - resetRenderer.call(this); - increasePrecision.call(this); - } - - function drawZeroRangeBar() { - var _this = this; - - if ( - this.current_data.length === 1 && - this.measure.filtered.domain[0] === this.measure.filtered.domain[1] - ) { - var width = this.plot_width / 25; - this.svg - .selectAll('g.bar-group rect') - .transition() - .delay(250) // wait for initial marks to transition - .attr({ - x: function x(d) { - return _this.x(d.values.x) - width / 2; - }, - width: width - }); - } - } - - function addHoverBars() { - var context = this; - - var bins = this.svg.selectAll('.bar-group').each(function(d) { - var g = d3.select(this); - g.selectAll('.hover-bar').remove(); - - //Drawing a path instead of a rect because Webcharts messes up the original rect on resize. - var x = context.x(d.rangeLow); - var y = 0; - var width = context.x(d.rangeHigh) - context.x(d.rangeLow); - var height = context.plot_height; - var hoverBar = g - .insert('path', ':first-child') - .classed('hover-bar', true) - .attr({ - d: 'M ' + x + ' ' + y + ' V ' + height + ' H ' + (x + width) + ' V ' + y, - fill: 'black', - 'fill-opacity': 0, - stroke: 'black', - 'stroke-opacity': 0 - }); - d.footnote = - "" + - d.values.raw.length + - ' records with ' + - (context.measure.current + " values ≥") + - (context.config.x.d3format1(d.rangeLow) + - ' and ' + - (d.rangeHigh < context.config.x.domain[1] ? '<' : '≤') + - "" + - context.config.x.d3format1(d.rangeHigh) + - ''); - }); - } - - function mouseout(element, d) { - //Update footnote. - this.footnotes.barDetails.html( - this.highlightedBin ? 'Table displays ' + this.highlighteD.footnote + '.' : '' - ); - - //Remove bar highlight. - var selection = d3.select(element); - selection.selectAll('.bar').attr('stroke', this.colorScale()); - } - - function mouseover(element, d) { - //Update bar details footnote. - this.footnotes.barDetails.html('Bar encompasses ' + d.footnote + '.'); - - //Highlight bar. - var selection = d3.select(element); - if (!/trident/i.test(navigator.userAgent)) selection.moveToFront(); - selection.selectAll('.bar').attr('stroke', 'black'); - } - - function select(element, d) { - var _this = this; - - //Reduce bin opacity and highlight selected bin. - this.svg - .selectAll('.bar-group') - .selectAll('.bar') - .attr('fill-opacity', 0.5); - d3.select(element) - .select('.bar') - .attr('fill-opacity', 1); - - //Update bar click footnote - this.footnotes.barClick - .style({ - cursor: 'pointer', - 'text-decoration': 'underline' - }) - .text('Click here to remove details and clear highlighting.') - .on('click', function() { - resetRenderer.call(_this); - }); - - //Update bar details footnote. - this.footnotes.barDetails.html('Table displays ' + d.footnote + '.'); - - //Draw listing. - this.listing.draw(d.values.raw); - this.listing.wrap.style('display', 'inline-block'); - } - - function deselect(element, d) { - delete this.highlightedBin; - delete this.highlighteD; - this.listing.draw([]); - this.listing.wrap.style('display', 'none'); - this.svg.selectAll('.bar').attr('fill-opacity', 0.75); - - this.footnotes.barClick - .style({ - cursor: 'normal', - 'text-decoration': 'none' - }) - .text('Click a bar for details.'); - this.footnotes.barDetails.text( - d.values.raw.length + - ' records with ' + - (this.measure.current + ' values from ') + - (this.config.x.d3format1(d.rangeLow) + - ' to ' + - this.config.x.d3format1(d.rangeHigh)) - ); - } - - function click(element, d) { - this.highlightedBin = d.key; - this.highlighteD = d; - var selection = d3.select(element); - var selected = selection.classed('selected'); - this.svg.selectAll('.bar-group').classed('selected', false); - selection.classed('selected', !selected); - - if (!selected) select.call(this, element, d); - else deselect.call(this, element, d); - } - - function addBinEventListeners() { - var context = this; - - var barGroups = this.svg.selectAll('.bar-group').style('cursor', 'pointer'); - - barGroups - .on('mouseover', function(d) { - mouseover.call(context, this, d); - }) - .on('mouseout', function(d) { - mouseout.call(context, this, d); - }) - .on('click', function(d) { - click.call(context, this, d); - }); - } - - function drawNormalRanges() { - var _this = this; - - this.controls.wrap.select('.normal-range-list').remove(); - this.svg.select('.normal-ranges').remove(); - - if (this.config.displayNormalRange && this.filtered_data.length > 0) { - //Capture distinct normal ranges in filtered data. - var normalRanges = d3 - .nest() - .key(function(d) { - return d[_this.config.normal_col_low] + ',' + d[_this.config.normal_col_high]; - }) // set key to comma-delimited normal range - .rollup(function(d) { - return d.length; - }) - .entries(this.filtered_data) - .map(function(d) { - d.keySplit = d.key.split(','); - - //lower - d.lower = +d.keySplit[0]; - d.x1 = d.lower >= _this.x_dom[0] ? _this.x(d.lower) : 0; - - //upper - d.upper = +d.keySplit[1]; - d.x2 = d.upper <= _this.x_dom[1] ? _this.x(d.upper) : _this.plot_width; - - //width - d.width = d.x2 - d.x1; - - //tooltip - d.rate = d.values / _this.filtered_data.length; - d.tooltip = - d.values < _this.filtered_data.length - ? d.lower + - ' - ' + - d.upper + - ' (' + - d3.format('%')(d.rate) + - ' of records)' - : d.lower + ' - ' + d.upper; - - //plot if: - // - at least one of the limits of normal fall within the current x-domain - // - the lower limit is less than the current x-domain and the upper limit is greater than current the x-domain - d.plot = - (_this.x_dom[0] <= d.lower && d.lower <= _this.x_dom[1]) || - (_this.x_dom[0] <= d.upper && d.upper <= _this.x_dom[1]) || - (_this.x_dom[0] >= d.lower && d.upper >= _this.x_dom[1]); - - return d; - }) - .sort(function(a, b) { - var diff_lower = a.lower - b.lower; - var diff_upper = a.upper - b.upper; - return diff_lower ? diff_lower : diff_upper ? diff_upper : 0; - }); // sort normal ranges so larger normal ranges plot beneath smaller normal ranges - - //Add tooltip to Normal Range control that lists normal ranges. - this.controls.wrap - .selectAll('#normal-range .wc-control-label') - .append('span') - .classed('normal-range-list', true) - .html(' ⓘ') - .attr( - 'title', - normalRanges.length > 1 - ? this.measure.current + - ' normal ranges:\n' + - normalRanges - .map(function(normalRange) { - return normalRange.tooltip; - }) - .join('\n') - : this.measure.current + ' normal range: ' + normalRanges[0].tooltip - ) - .style('cursor', 'default'); - - //Add groups in which to draw normal range rectangles and annotations. - var group = this.svg.insert('g', '.bar-supergroup').classed('normal-ranges', true); - var groups = group - .selectAll('g.normal-range') - .data( - normalRanges.filter(function(d) { - return d.plot; - }) - ) - .enter() - .append('g') - .classed('normal-range', true); - - //Draw normal range rectangles. - var rectangles = groups - .append('rect') - .classed('normal-range__rect', true) - .attr({ - x: function x(d) { - return d.x1; - }, - y: 0, - width: function width(d) { - return d.width; - }, - height: this.plot_height, - stroke: '#c26683', - fill: '#c26683', - 'stroke-opacity': function strokeOpacity(d) { - return (d.values / _this.filtered_data.length) * 0.5; - }, - 'fill-opacity': function fillOpacity(d) { - return (d.values / _this.filtered_data.length) * 0.25; - } - }); // opacity as a function of fraction of records with the given normal range - } - } - - function maintainBinHighlighting() { - var _this = this; - - this.svg.selectAll('.bar').attr('fill-opacity', function(d) { - return _this.highlightedBin - ? d.key !== _this.highlightedBin - ? 0.5 - : 1 - : _this.marks[0].attributes['fill-opacity']; - }); - } - - function removeXAxisTicks() { - if (this.config.annotate_bin_boundaries) this.svg.selectAll('.x.axis .tick').remove(); - } - - function annotateBinBoundaries() { - var _this = this; - - if (this.config.annotate_bin_boundaries) { - //Remove bin boundaries. - this.svg.select('g.bin-boundaries').remove(); - - //Check for repeats of values formatted with lower precision. - var repeats = d3 - .nest() - .key(function(d) { - return d.value1; - }) - .rollup(function(d) { - return d.length; - }) - .entries(this.measure.binBoundaries) - .some(function(d) { - return d.values > 1; - }); - - //Annotate bin boundaries. - var axis = this.svg.append('g').classed('bin-boundaries axis', true); - var ticks = axis - .selectAll('g.bin-boundary') - .data(this.measure.binBoundaries) - .enter() - .append('g') - .classed('bin-boundary tick', true); - var texts = ticks - .append('text') - .attr({ - x: function x(d) { - return _this.x(d.value); - }, - y: this.plot_height, - dy: '16px', - 'text-anchor': 'middle' - }) - .text(function(d) { - return repeats ? d.value2 : d.value1; - }); - - //Thin ticks. - var textDimensions = []; - texts.each(function(d) { - var text = d3.select(this); - var bbox = this.getBBox(); - if ( - textDimensions.some(function(textDimension) { - return textDimension.x + textDimension.width > bbox.x - 5; - }) - ) - text.remove(); - else - textDimensions.push({ - x: bbox.x, - width: bbox.width, - y: bbox.y, - height: bbox.height - }); - }); - } - } - - function onResize() { - //Draw custom bin for single observation subsets. - drawZeroRangeBar.call(this); - - //Add invisible bars for improved hovering. - addHoverBars.call(this); - - //Display data listing on bin click. - addBinEventListeners.call(this); - - //Visualize normal ranges. - drawNormalRanges.call(this); - - //Keep highlighted bin highlighted on resize. - maintainBinHighlighting.call(this); - - //Remove x-axis ticks. - removeXAxisTicks.call(this); - - //Annotate bin boundaries. - annotateBinBoundaries.call(this); - } - - function onDestroy() { - this.listing.destroy(); - d3.select(this.div) - .selectAll('.loader') - .remove(); - } - - var callbacks = { - onInit: onInit, - onLayout: onLayout, - onPreprocess: onPreprocess, - onDatatransform: onDatatransform, - onDraw: onDraw, - onResize: onResize, - onDestroy: onDestroy - }; - - function safetyHistogram() { - var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'body'; - var settings = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - - //Define chart. - var mergedSettings = Object.assign( - {}, - JSON.parse(JSON.stringify(configuration.settings)), - settings - ); - var syncedSettings = configuration.syncSettings(mergedSettings); - var syncedControlInputs = configuration.syncControlInputs( - configuration.controlInputs(), - syncedSettings - ); - var controls = webcharts.createControls(element, { - location: 'top', - inputs: syncedControlInputs - }); - var chart = webcharts.createChart(element, syncedSettings, controls); - - //Define chart callbacks. - for (var callback in callbacks) { - chart.on(callback.substring(2).toLowerCase(), callbacks[callback]); - } //Define listing - var listingSettings = Object.assign( - {}, - { - cols: syncedSettings.details.map(function(detail) { - return detail.value_col; - }), - headers: syncedSettings.details.map(function(detail) { - return detail.label; - }) - }, - syncedSettings - ); - var listing = webcharts.createTable(element, listingSettings); - listing.on('layout', function() { - //Style table - this.wrap.style('display', 'none'); - this.wrap.selectAll('.table-top,table,.table-bottom').style({ - float: 'left', - clear: 'left', - width: '100%' - }); - this.table.style('white-space', 'nowrap'); - }); - //Attach listing to chart. - chart.listing = listing; - listing.chart = chart; - - //Initialize listing and hide initially. - - chart.listing.init([]); - - return chart; - } - - return safetyHistogram; -}); diff --git a/inst/htmlwidgets/lib/safety-outlier-explorer-2.6.0/safetyOutlierExplorer.js b/inst/htmlwidgets/lib/safety-outlier-explorer-2.6.0/safetyOutlierExplorer.js deleted file mode 100644 index 0d17de1c..00000000 --- a/inst/htmlwidgets/lib/safety-outlier-explorer-2.6.0/safetyOutlierExplorer.js +++ /dev/null @@ -1,2478 +0,0 @@ -(function(global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' - ? (module.exports = factory(require('d3'), require('webcharts'))) - : typeof define === 'function' && define.amd - ? define(['d3', 'webcharts'], factory) - : ((global = global || self), - (global.safetyOutlierExplorer = factory(global.d3, global.webCharts))); -})(this, function(d3$1, webcharts) { - 'use strict'; - - if (typeof Object.assign != 'function') { - Object.defineProperty(Object, 'assign', { - value: function assign(target, varArgs) { - if (target == null) { - // TypeError if undefined or null - throw new TypeError('Cannot convert undefined or null to object'); - } - - var to = Object(target); - - for (var index = 1; index < arguments.length; index++) { - var nextSource = arguments[index]; - - if (nextSource != null) { - // Skip over if undefined or null - for (var nextKey in nextSource) { - // Avoid bugs when hasOwnProperty is shadowed - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - - return to; - }, - writable: true, - configurable: true - }); - } - - if (!Array.prototype.find) { - Object.defineProperty(Array.prototype, 'find', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, 'length')). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return kValue. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return kValue; - } - // e. Increase k by 1. - k++; - } - - // 7. Return undefined. - return undefined; - } - }); - } - - if (!Array.prototype.findIndex) { - Object.defineProperty(Array.prototype, 'findIndex', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, "length")). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return k. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return k; - } - // e. Increase k by 1. - k++; - } - - // 7. Return -1. - return -1; - } - }); - } - - Math.log10 = - Math.log10 || - function(x) { - 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$1.selection.prototype.moveToFront = function() { - return this.each(function() { - this.parentNode.appendChild(this); - }); - }; - - d3$1.selection.prototype.moveToBack = function() { - return this.each(function() { - var firstChild = this.parentNode.firstChild; - if (firstChild) { - this.parentNode.insertBefore(this, firstChild); - } - }); - }; - - 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 - } - }; - } - - 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 - }; - } - - function syncSettings(settings) { - var time_col = settings.time_cols[0]; - - //handle a string arguments to array settings - var 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 - var lines = settings.marks.find(function(mark) { - return 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 - var points = settings.marks.find(function(mark) { - return 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(function(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 !== '' - ) { - var 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; - } - - 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' - } - ]; - } - - function syncControlInputs(controlInputs, settings) { - var xAxisControl = controlInputs.find(function(d) { - return d.label === 'X-axis'; - }); - xAxisControl.values = settings.time_cols.map(function(d) { - return d.value_col; - }); - - if (settings.filters) { - settings.filters.forEach(function(d, i) { - var 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(function(f) { - return f.type == 'subsetter'; - }) - .map(function(m) { - return 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(function(controlInput) { - return controlInput.label; - }) - .indexOf('Unscheduled Visits'), - 1 - ); - - return controlInputs; - } - - var configuration = { - rendererSettings: rendererSettings, - webchartsSettings: webchartsSettings, - settings: Object.assign({}, rendererSettings(), webchartsSettings()), - syncSettings: syncSettings, - controlInputs: controlInputs, - syncControlInputs: syncControlInputs - }; - - function countParticipants() { - var _this = this; - - this.participantCount = { - N: d3$1 - .set( - this.raw_data.map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values() - .filter(function(value) { - return !/^\s*$/.test(value); - }).length, - container: null, // set in ../onLayout/addParticipantCountContainer - n: null, // set in ../onDraw/updateParticipantCount - percentage: null // set in ../onDraw/updateParticipantCount - }; - } - - function removeMissingResults() { - var _this = this; - - //Split data into records with missing and nonmissing results. - var missingResults = []; - var nonMissingResults = []; - this.raw_data.forEach(function(d) { - if (/^\s*$/.test(d[_this.config.value_col])) missingResults.push(d); - else nonMissingResults.push(d); - }); - - //Nest missing and nonmissing results by participant. - var participantsWithMissingResults = d3$1 - .nest() - .key(function(d) { - return d[_this.config.id_col]; - }) - .rollup(function(d) { - return d.length; - }) - .entries(missingResults); - var participantsWithNonMissingResults = d3$1 - .nest() - .key(function(d) { - return d[_this.config.id_col]; - }) - .rollup(function(d) { - return d.length; - }) - .entries(nonMissingResults); - - //Identify placeholder records, i.e. participants with a single missing result. - this.removedRecords.placeholderRecords = participantsWithMissingResults - .filter(function(d) { - return ( - participantsWithNonMissingResults - .map(function(d) { - return d.key; - }) - .indexOf(d.key) < 0 && d.values === 1 - ); - }) - .map(function(d) { - return d.key; - }); - if (this.removedRecords.placeholderRecords.length) - console.log( - this.removedRecords.placeholderRecords.length + - ' participants without results have been detected.' - ); - - //Count the number of records with missing results. - this.removedRecords.missing = d3$1.sum( - participantsWithMissingResults.filter(function(d) { - return _this.removedRecords.placeholderRecords.indexOf(d.key) < 0; - }), - function(d) { - return d.values; - } - ); - if (this.removedRecords.missing > 0) - console.warn( - this.removedRecords.missing + - ' record' + - (this.removedRecords.missing > 1 - ? 's with a missing result have' - : ' with a missing result has') + - ' been removed.' - ); - - //Update data. - this.raw_data = nonMissingResults; - } - - function removeNonNumericResults() { - var _this = this; - - //Filter out non-numeric results. - var numericResults = this.raw_data.filter(function(d) { - return /^-?[0-9.]+$/.test(d[_this.config.value_col]); - }); - this.removedRecords.nonNumeric = this.raw_data.length - numericResults.length; - if (this.removedRecords.nonNumeric > 0) - console.warn( - this.removedRecords.nonNumeric + - ' record' + - (this.removedRecords.nonNumeric > 1 - ? 's with a non-numeric result have' - : ' with a non-numeric result has') + - ' been removed.' - ); - - //Update data. - this.raw_data = numericResults; - } - - function cleanData() { - this.removedRecords = { - placeholderParticipants: null, // defined in './cleanData/removeMissingResults - missing: null, // defined in './cleanData/removeMissingResults - nonNumeric: null, // defined in './cleanData/removeNonNumericResults - container: null // defined in ../onLayout/addRemovedRecordsContainer - }; - removeMissingResults.call(this); - removeNonNumericResults.call(this); - this.initial_data = this.raw_data; - } - - function addVariables() { - var _this = this; - - var ordinalTimeSettings = this.config.time_cols.find(function(time_col) { - return time_col.type === 'ordinal'; - }); - - this.raw_data.forEach(function(d) { - //Concatenate unit to measure if provided. - d.soe_measure = d.hasOwnProperty(_this.config.unit_col) - ? d[_this.config.measure_col] + ' (' + d[_this.config.unit_col] + ')' - : d[_this.config.measure_col]; - - //Identify unscheduled visits. - d.unscheduled = false; - if (ordinalTimeSettings) { - if (_this.config.unscheduled_visit_values) - d.unscheduled = - _this.config.unscheduled_visit_values.indexOf( - d[ordinalTimeSettings.value_col] - ) > -1; - else if (_this.config.unscheduled_visit_regex) - d.unscheduled = _this.config.unscheduled_visit_regex.test( - d[ordinalTimeSettings.value_col] - ); - } - }); - } - - function participant() { - var _this = this; - - this.IDOrder = d3$1 - .set( - this.raw_data.map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values() - .sort() - .map(function(ID, i) { - return { - ID: ID, - order: i - }; - }); - } - - function visit() { - var _this = this; - - //ordinal - this.config.time_cols - .filter(function(time_col) { - return time_col.type === 'ordinal'; - }) - .forEach(function(time_settings) { - var visits = void 0, - visitOrder = void 0; - - //Given an ordering variable sort a unique set of visits by the ordering variable. - if ( - time_settings.order_col && - _this.raw_data[0].hasOwnProperty(time_settings.order_col) - ) { - //Define a unique set of visits with visit order concatenated. - visits = d3$1 - .set( - _this.raw_data.map(function(d) { - return ( - d[time_settings.order_col] + '|' + d[time_settings.value_col] - ); - }) - ) - .values(); - - //Sort visits. - visitOrder = visits - .sort(function(a, b) { - var aOrder = a.split('|')[0], - bOrder = b.split('|')[0], - diff = +aOrder - +bOrder; - return diff ? diff : d3$1.ascending(a, b); - }) - .map(function(visit) { - return visit.split('|')[1]; - }); - } else { - //Otherwise sort a unique set of visits alphanumerically. - //Define a unique set of visits. - visits = d3$1 - .set( - _this.raw_data.map(function(d) { - return d[time_settings.value_col]; - }) - ) - .values(); - - //Sort visits; - visitOrder = visits.sort(); - } - - //Set x-axis domain. - if (time_settings.order) { - //If a visit order is specified, use it and concatenate any unspecified visits at the end. - time_settings.order = time_settings.order.concat( - visitOrder.filter(function(visit) { - return time_settings.order.indexOf(visit) < 0; - }) - ); - } - //Otherwise use data-driven visit order. - else time_settings.order = visitOrder; - - //Define domain. - time_settings.domain = time_settings.order; - }); - } - - function measure() { - var _this = this; - - this.measures = d3$1 - .set( - this.initial_data.map(function(d) { - return d[_this.config.measure_col]; - }) - ) - .values() - .sort(); - this.soe_measures = d3$1 - .set( - this.initial_data.map(function(d) { - return d.soe_measure; - }) - ) - .values() - .sort(); - } - - function defineSets() { - participant.call(this); - visit.call(this); - measure.call(this); - } - - function updateMeasureFilter() { - this.measure = {}; - var measureInput = this.controls.config.inputs.find(function(input) { - return input.label === 'Measure'; - }); - if ( - this.config.start_value && - this.soe_measures.indexOf(this.config.start_value) < 0 && - this.measures.indexOf(this.config.start_value) < 0 - ) { - measureInput.start = this.soe_measures[0]; - console.warn( - this.config.start_value + - ' is an invalid measure. Defaulting to ' + - measureInput.start + - '.' - ); - } else if ( - this.config.start_value && - this.soe_measures.indexOf(this.config.start_value) < 0 - ) { - measureInput.start = this.soe_measures[this.measures.indexOf(this.config.start_value)]; - console.warn( - this.config.start_value + - ' is missing the units value. Defaulting to ' + - measureInput.start + - '.' - ); - } else measureInput.start = this.config.start_value || this.soe_measures[0]; - } - - function removeFilters() { - var _this = this; - - this.controls.config.inputs = this.controls.config.inputs.filter(function(input) { - if (input.type !== 'subsetter' || input.value_col === 'soe_measure') { - return true; - } else if (!_this.raw_data[0].hasOwnProperty(input.value_col)) { - console.warn( - 'The [ ' + - input.label + - ' ] filter has been removed because the variable does not exist.' - ); - } else { - var levels = d3$1 - .set( - _this.raw_data.map(function(d) { - return d[input.value_col]; - }) - ) - .values(); - - if (levels.length === 1) - console.warn( - 'The [ ' + - input.label + - ' ] filter has been removed because the variable has only one level.' - ); - - return levels.length > 1; - } - }); - } - - function updateNormalRangeControl() { - //If data do not have normal range variables update normal range method setting and options. - if ( - Object.keys(this.raw_data[0]).indexOf(this.config.normal_col_low) < 0 || - Object.keys(this.raw_data[0]).indexOf(this.config.normal_col_high) < 0 - ) { - if (this.config.normal_range_method === 'LLN-ULN') - this.config.normal_range_method = 'Standard Deviation'; - this.controls.config.inputs - .find(function(input) { - return input.option === 'normal_range_method'; - }) - .values.splice(1, 1); - } - } - - function checkControls() { - updateMeasureFilter.call(this); - removeFilters.call(this); - updateNormalRangeControl.call(this); - } - - function initCustomEvents() { - var chart = this; - chart.participantsSelected = []; - chart.events.participantsSelected = new CustomEvent('participantsSelected'); - } - - function onInit() { - // 1. Count number of unique participant IDs in data prior to data cleaning. - countParticipants.call(this); - - // 2. Remove missing and non-numeric results. - cleanData.call(this); - - // 3. Define additional variables. - addVariables.call(this); - - // 4. Define participant, visit, and measure sets. - defineSets.call(this); - - // 5. Check controls. - checkControls.call(this); - - // 6. Initialize custom events - initCustomEvents.call(this); - } - - function identifyControls() { - var controlGroups = this.controls.wrap - .style('padding-bottom', '8px') - .selectAll('.control-group'); - - //Give each control a unique ID. - controlGroups - .attr('id', function(d) { - return d.label.toLowerCase().replace(' ', '-'); - }) - .each(function(d) { - d3$1.select(this).classed(d.type, true); - }); - - //Give y-axis controls a common class name. - controlGroups - .filter(function(d) { - return ['y.domain[0]', 'y.domain[1]'].indexOf(d.option) > -1; - }) - .classed('y-axis', true); - - //Give normal range controls a common class name. - controlGroups - .filter(function(d) { - return ( - [ - 'normal_range_method', - 'normal_range_sd', - 'normal_range_quantile_low', - 'normal_range_quantile_high' - ].indexOf(d.option) > -1 - ); - }) - .classed('normal-range', true); - - //Give visit range controls a common class name. - controlGroups - .filter(function(d) { - return ['visits_without_data', 'unscheduled_visits'].indexOf(d.option) > -1; - }) - .classed('visits', true); - } - - function labelXaxisOptions() { - var _this = this; - - this.controls.wrap - .selectAll('.control-group') - .filter(function(d) { - return d.option === 'x.column'; - }) - .selectAll('option') - .property('label', function(d) { - return _this.config.time_cols.find(function(time_col) { - return time_col.value_col === d; - }).label; - }); - } - - function addYdomainResetButton() { - var _this = this; - - var resetContainer = this.controls.wrap - .insert('div', '#lower') - .classed('control-group y-axis', true) - .datum({ - type: 'button', - option: 'y.domain', - label: '' - }) - .style('vertical-align', 'bottom'); - var resetLabel = resetContainer - .append('span') - .attr('class', 'wc-control-label') - .text('Limits'); - var resetButton = resetContainer - .append('button') - .text(' Reset ') - .style('padding', '0px 5px') - .on('click', function() { - _this.config.y.domain = _this.measure.domain; //reset axis to full range - _this.draw(); - }); - } - - function insertGrouping(selector, label) { - var grouping = this.controls.wrap - .insert('div', selector) - .style({ - display: 'inline-block', - 'margin-right': '5px' - }) - .append('fieldset') - .style('padding', '0px 2px'); - grouping.append('legend').text(label); - this.controls.wrap.selectAll(selector).each(function(d) { - this.style.marginTop = '0px'; - this.style.marginRight = '2px'; - this.style.marginBottom = '2px'; - this.style.marginLeft = '2px'; - grouping.node().appendChild(this); - }); - } - - function groupControls() { - //Group y-axis controls. - insertGrouping.call(this, '.y-axis', 'Y-axis'); - - //Group filters. - if (this.filters.length > 1) - insertGrouping.call(this, '.subsetter:not(#measure)', 'Filters'); - - //Group normal controls. - insertGrouping.call(this, '.normal-range', 'Normal Range'); - - //Group visit controls. - insertGrouping.call(this, '.visits', 'Visits'); - } - - function hideNormalRangeInputs() { - var _this = this; - var controls = this.controls.wrap.selectAll('.control-group'); - - //Normal range method control - var normalRangeMethodControl = controls.filter(function(d) { - return d.label === 'Method'; - }); - - //Normal range inputs - var normalRangeInputs = controls - .filter(function(d) { - return ( - [ - 'normal_range_sd', - 'normal_range_quantile_low', - 'normal_range_quantile_high' - ].indexOf(d.option) > -1 - ); - }) - .style('display', function(d) { - return (_this.config.normal_range_method !== 'Standard Deviation' && - d.option === 'normal_range_sd') || - (_this.config.normal_range_method !== 'Quantiles' && - ['normal_range_quantile_low', 'normal_range_quantile_high'].indexOf( - d.option - ) > -1) - ? 'none' - : 'inline-table'; - }); - - //Set significant digits to .01. - normalRangeInputs.select('input').attr('step', 0.01); - - normalRangeMethodControl.on('change', function() { - var normal_range_method = d3$1 - .select(this) - .select('option:checked') - .text(); - - normalRangeInputs.style('display', function(d) { - return (normal_range_method !== 'Standard Deviation' && - d.option === 'normal_range_sd') || - (normal_range_method !== 'Quantiles' && - ['normal_range_quantile_low', 'normal_range_quantile_high'].indexOf( - d.option - ) > -1) - ? 'none' - : 'inline-table'; - }); - }); - } - - function addParticipantCountContainer() { - this.participantCount.container = this.controls.wrap - .style('position', 'relative') - .append('div') - .attr('id', 'participant-count') - .style({ - position: 'absolute', - 'font-style': 'italic', - bottom: '-10px', - left: 0 - }); - } - - function addRemovedRecordsNote() { - var _this = this; - - if (this.removedRecords.missing > 0 || this.removedRecords.nonNumeric > 0) { - var message = - this.removedRecords.missing > 0 && this.removedRecords.nonNumeric > 0 - ? this.removedRecords.missing + - ' record' + - (this.removedRecords.missing > 1 ? 's' : '') + - ' with a missing result and ' + - this.removedRecords.nonNumeric + - ' record' + - (this.removedRecords.nonNumeric > 1 ? 's' : '') + - ' with a non-numeric result were removed.' - : this.removedRecords.missing > 0 - ? this.removedRecords.missing + - ' record' + - (this.removedRecords.missing > 1 ? 's' : '') + - ' with a missing result ' + - (this.removedRecords.missing > 1 ? 'were' : 'was') + - ' removed.' - : this.removedRecords.nonNumeric > 0 - ? this.removedRecords.nonNumeric + - ' record' + - (this.removedRecords.nonNumeric > 1 ? 's' : '') + - ' with a non-numeric result ' + - (this.removedRecords.nonNumeric > 1 ? 'were' : 'was') + - ' removed.' - : ''; - this.removedRecords.container = this.controls.wrap - .append('div') - .style({ - position: 'absolute', - 'font-style': 'italic', - bottom: '-10px', - right: 0 - }) - .text(message); - this.removedRecords.container - .append('span') - .style({ - color: 'blue', - 'text-decoration': 'underline', - 'font-style': 'normal', - 'font-weight': 'bold', - cursor: 'pointer', - 'font-size': '16px', - 'margin-left': '5px' - }) - .html('x') - .on('click', function() { - return _this.removedRecords.container.style('display', 'none'); - }); - } - } - - function addBorderAboveChart() { - this.wrap.style({ - 'border-top': '1px solid #ccc' - }); - } - - function addSmallMultiplesContainer() { - this.multiples = { - container: this.wrap - .append('div') - .classed('multiples', true) - .style({ - 'padding-top': '10px' - }), - id: null - }; - } - - function onLayout() { - identifyControls.call(this); // Distinguish controls to insert y-axis reset button in the correct position. - labelXaxisOptions.call(this); - addYdomainResetButton.call(this); - groupControls.call(this); // Group related controls visually. - hideNormalRangeInputs.call(this); // Hide normal range input controls depending on the normal range method. - addParticipantCountContainer.call(this); - addRemovedRecordsNote.call(this); - addBorderAboveChart.call(this); - addSmallMultiplesContainer.call(this); - } - - function getCurrentMeasure() { - this.measure.previous = this.measure.current; - this.measure.current = this.controls.wrap - .selectAll('.control-group') - .filter(function(d) { - return d.value_col && d.value_col === 'soe_measure'; - }) - .select('option:checked') - .text(); - } - - function defineMeasureData() { - var _this = this; - - this.measure.data = this.initial_data.filter(function(d) { - return d.soe_measure === _this.measure.current; - }); - this.measure.unit = - this.config.unit_col && this.measure.data[0].hasOwnProperty(this.config.unit_col) - ? this.measure.data[0][this.config.unit_col] - : null; - this.measure.results = this.measure.data - .map(function(d) { - return +d[_this.config.value_col]; - }) - .sort(function(a, b) { - return a - b; - }); - 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) { - return _this.config.unscheduled_visits || !d.unscheduled; - }); - } - - function removeVisitsWithoutData() { - var _this = this; - - if (!this.config.visits_without_data) - this.config.x.domain = this.config.x.domain.filter(function(visit) { - return ( - d3$1 - .set( - _this.raw_data.map(function(d) { - return d[_this.config.time_settings.value_col]; - }) - ) - .values() - .indexOf(visit) > -1 - ); - }); - } - - function removeUnscheduledVisits() { - var _this = this; - - if (!this.config.unscheduled_visits) { - if (this.config.unscheduled_visit_values) - this.config.x.domain = this.config.x.domain.filter(function(visit) { - return _this.config.unscheduled_visit_values.indexOf(visit) < 0; - }); - else if (this.config.unscheduled_visit_regex) - this.config.x.domain = this.config.x.domain.filter(function(visit) { - return !_this.config.unscheduled_visit_regex.test(visit); - }); - } - } - - function setXdomain() { - var _this = this; - - //Attach the time settings object to the x-axis settings object. - this.config.time_settings = this.config.time_cols.find(function(time_col) { - return time_col.value_col === _this.config.x.column; - }); - Object.assign(this.config.x, this.config.time_settings); - - //When the domain is not specified, it's calculated on data transform. - if (this.config.x.type === 'linear') { - delete this.config.x.domain; - delete this.config.x.order; - } - - //Remove unscheduled visits from x-domain if x-type is ordinal. - if (this.config.x.type === 'ordinal') { - removeVisitsWithoutData.call(this); - removeUnscheduledVisits.call(this); - } - } - - function setYdomain() { - if (this.measure.current !== this.measure.previous) - this.config.y.domain = this.measure.domain; - else if (this.config.y.domain[0] > this.config.y.domain[1]) - // reset y-domain - this.config.y.domain.reverse(); // reverse y-domain - } - - function calculateYPrecision() { - //define the precision of the y-axis - this.config.y.precisionFactor = Math.round(this.measure.log10range); - this.config.y.precision = Math.pow(10, this.config.y.precisionFactor); - this.config.y.format = - this.config.y.precisionFactor > 0 - ? '.0f' - : '.' + (Math.abs(this.config.y.precisionFactor) + 1) + 'f'; - - //define the size of the y-axis limit increments - var step = - this.measure.range > 0 - ? Math.abs(this.measure.range / 15) // non-zero range - : this.measure.results[0] !== 0 - ? Math.abs(this.measure.results[0] / 15) // zero range, non-zero result(s) - : 1; // zero range, zero result(s) - if (step < 1) { - var x10 = 0; - do { - step = step * 10; - ++x10; - } while (step < 1); - step = Math.round(step) / Math.pow(10, x10); - } else step = Math.round(step); - this.measure.step = step || 1; - } - - function updateYaxisLimitControls() { - this.controls.wrap - .selectAll('.control-group') - .filter(function(f) { - return f.option === 'y.domain[0]'; - }) - .select('input') - .attr('step', this.measure.step) // set in ./calculateYPrecision - .style('box-shadow', 'none') - .property('value', this.config.y.domain[0]); - - this.controls.wrap - .selectAll('.control-group') - .filter(function(f) { - return f.option === 'y.domain[1]'; - }) - .select('input') - .attr('step', this.measure.step) // set in ./calculateYPrecision - .style('box-shadow', 'none') - .property('value', this.config.y.domain[1]); - } - - function setYaxisLabel() { - this.config.y.label = this.measure.current; - } - - function updateYaxisResetButton() { - //Update tooltip of y-axis domain reset button. - if (this.currentMeasure !== this.previousMeasure) - this.controls.wrap - .selectAll('.y-axis') - .property( - 'title', - 'Initial Limits: [' + - this.config.y.domain[0] + - ' - ' + - this.config.y.domain[1] + - ']' - ); - } - - function deriveStatistics() { - var _this = this; - - if (this.config.normal_range_method === 'LLN-ULN') { - this.lln = function(d) { - return d instanceof Object - ? +d[_this.config.normal_col_low] - : 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$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$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; - }; - this.uln = function() { - return _this.mean + _this.config.normal_range_sd * _this.sd; - }; - } else if (this.config.normal_range_method === 'Quantiles') { - this.lln = function() { - return d3$1.quantile(_this.measure.results, _this.config.normal_range_quantile_low); - }; - this.uln = function() { - return d3$1.quantile( - _this.measure.results, - _this.config.normal_range_quantile_high - ); - }; - } else { - this.lln = function(d) { - return d instanceof Object - ? d[_this.config.value_col] + 1 - : _this.measure.results[0]; - }; - this.uln = function(d) { - return d instanceof Object - ? d[_this.config.value_col] - 1 - : _this.measure.results[_this.measure.results.length - 1]; - }; - } - } - - function onPreprocess() { - // 1. Capture currently selected measure. - getCurrentMeasure.call(this); - - // 2. Filter data on currently selected measure. - defineMeasureData.call(this); - - // 3a Set x-domain given current visit settings. - setXdomain.call(this); - - // 3b Set y-domain given currently selected measure. - setYdomain.call(this); - - // 3c Calculate precision of y-domain. - calculateYPrecision.call(this); - - // 3c Set y-axis label to current measure. - setYaxisLabel.call(this); - - // 4a Update y-axis reset button when measure changes. - updateYaxisResetButton.call(this); - - // 4b Update y-axis limit controls to match y-axis domain. - updateYaxisLimitControls.call(this); - - // 4c Define normal range statistics. - deriveStatistics.call(this); - } - - function onDatatransform() {} - - function updateParticipantCount() { - var _this = this; - - //count the number of unique ids in the current chart and calculate the percentage - this.participantCount.n = d3$1 - .set( - this.filtered_data.map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values().length; - this.participantCount.percentage = d3$1.format('0.1%')( - this.participantCount.n / this.participantCount.N - ); - - //clear the annotation - this.participantCount.container.selectAll('*').remove(); - - //update the annotation - this.participantCount.container.text( - '\n' + - this.participantCount.n + - ' of ' + - this.participantCount.N + - ' participant(s) shown (' + - this.participantCount.percentage + - ')' - ); - } - - 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) - // this.multiples.chart.destroy(); - } - - function extendYDomain() { - if ( - this.config.y.domain[0] === this.measure.domain[0] && - this.config.y.domain[1] === this.measure.domain[1] && - this.config.y.domain[0] < this.measure.domain[1] - ) - this.y_dom = [ - this.config.y.domain[0] - this.measure.range * 0.01, - this.config.y.domain[1] + this.measure.range * 0.01 - ]; - } - - function updateBottomMargin() { - this.config.margin.bottom = this.config.x.vertical_space; - } - - function onDraw() { - //Annotate participant count. - updateParticipantCount.call(this); - - //Clear current multiples. - resetChart.call(this); - - //Extend y-domain to avoid obscuring minimum and maximum points. - extendYDomain.call(this); - - //Update bottom margin for tick label rotation. - updateBottomMargin.call(this); - } - - function attachMarks() { - this.marks.forEach(function(mark) { - mark.groups.each(function(group) { - group.attributes = mark.attributes; - if (mark.type === 'circle') group.radius = mark.radius; - }); - }); - this.lines = this.svg.selectAll('.line'); - this.points = this.svg.selectAll('.point'); - } - - function highlightSelected() { - var _this = this; - - //Add _selected_ class to participant's marks. - this.marks.forEach(function(mark) { - mark.groups.classed('selected', function(d) { - return mark.type === 'line' - ? d.values[0].values.raw[0][_this.config.id_col] === _this.selected_id - : d.values.raw[0][_this.config.id_col] === _this.selected_id; - }); - }); - - //Update attributes of selected line. - this.lines - .filter(function(d) { - return d.values[0].values.raw[0][_this.config.id_col] === _this.selected_id; - }) - .select('path') - .attr('stroke-width', function(d) { - return d.attributes['stroke-width'] * 8; - }); - - //Update attributes of selected points. - this.points - .filter(function(d) { - return d.values.raw[0][_this.config.id_col] === _this.selected_id; - }) - .select('circle') - .attr({ - r: function r(d) { - return d.radius; - }, - stroke: 'black', - 'stroke-width': function strokeWidth(d) { - return d.attributes['stroke-width'] * 8; - } - }); - } - - function maintainHighlight() { - if (this.selected_id) highlightSelected.call(this); - } - - function drawNormalRange() { - if (this.normalRange) this.normalRange.remove(); - - if (this.config.normal_range_method) { - this.normalRange = this.svg - .insert('g', '.line-supergroup') - .classed('normal-range', true); - this.normalRange - .append('rect') - .attr({ - x: 0, - y: this.y(this.uln()), - width: this.plot_width, - height: this.y(this.lln()) - this.y(this.uln()), - 'clip-path': 'url(#' + this.id + ')' - }) - .style({ - fill: 'blue', - 'fill-opacity': 0.1 - }); - this.normalRange.append('title').text('Normal range: ' + this.lln() + '-' + this.uln()); - } - } - - function clearHovered() { - this.lines - .filter(function() { - return !d3$1.select(this).classed('selected'); - }) - .select('path') - .each(function(d) { - d3$1.select(this).attr(d.attributes); - }); - this.points - .filter(function() { - return !d3$1.select(this).classed('selected'); - }) - .select('circle') - .each(function(d) { - d3$1.select(this).attr(d.attributes); - d3$1.select(this).attr('r', d.radius); - }); - delete this.hovered_id; - } - - function clearSelected() { - this.marks.forEach(function(mark) { - var element = mark.type === 'line' ? 'path' : mark.type; - mark.groups - .classed('selected', false) - .select(element) - .attr(mark.attributes); - }); - if (this.multiples.chart) this.multiples.chart.destroy(); - delete this.selected_id; - - //Trigger participantsSelected event - this.participantsSelected = []; - this.events.participantsSelected.data = this.participantsSelected; - this.wrap.node().dispatchEvent(this.events.participantsSelected); - } - - function addOverlayEventListener() { - var _this = this; - - var context = this; - this.overlay - .on('mouseover', function() { - clearHovered.call(_this); - }) - .on('click', function() { - clearHovered.call(_this); - clearSelected.call(_this); - context.wrap.select('div.overlapNote').remove(); - }); - } - - function addOverlayEventListener$1() { - var _this = this; - - this.normalRange - .on('mouseover', function() { - clearHovered.call(_this); - }) - .on('click', function() { - clearHovered.call(_this); - clearSelected.call(_this); - }); - } - - function highlightHovered() { - var _this = this; - - //Update attributes of hovered line. - this.lines - .filter(function(d) { - return d.values[0].values.raw[0][_this.config.id_col] === _this.hovered_id; - }) - .select('path') - .attr('stroke-width', function(d) { - return d.attributes['stroke-width'] * 4; - }); - - //Update attributes of hovered points. - this.points - .filter(function(d) { - return d.values.raw[0][_this.config.id_col] === _this.hovered_id; - }) - .select('circle') - .attr({ - r: function r(d) { - return d.radius; - }, - stroke: 'black', - 'stroke-width': function strokeWidth(d) { - return d.attributes['stroke-width'] * 4; - } - }); - } - - var _typeof = - typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' - ? function(obj) { - return typeof obj; - } - : function(obj) { - return obj && - typeof Symbol === 'function' && - obj.constructor === Symbol && - obj !== Symbol.prototype - ? 'symbol' - : typeof obj; - }; - - /*------------------------------------------------------------------------------------------------\ - Clone a variable (http://stackoverflow.com/a/728694). - \------------------------------------------------------------------------------------------------*/ - - function clone(obj) { - var copy; - - //Handle the 3 simple types, and null or undefined - if (null == obj || 'object' != (typeof obj === 'undefined' ? 'undefined' : _typeof(obj))) - return obj; - - //Handle Date - if (obj instanceof Date) { - copy = new Date(); - copy.setTime(obj.getTime()); - return copy; - } - - //Handle Array - if (obj instanceof Array) { - copy = []; - for (var i = 0, len = obj.length; i < len; i++) { - copy[i] = clone(obj[i]); - } - return copy; - } - - //Handle 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 isn't supported."); - } - - function defineSmallMultiples() { - //Define small multiples settings. - this.multiples.settings = Object.assign( - {}, - clone(this.config), - clone(Object.getPrototypeOf(this.config)) - ); - this.multiples.settings.x.domain = null; - this.multiples.settings.y.domain = null; - this.multiples.settings.resizable = false; - this.multiples.settings.scale_text = false; - - if (this.multiples.settings.multiples_sizing.width) - this.multiples.settings.width = this.multiples.settings.multiples_sizing.width; - if (this.multiples.settings.multiples_sizing.height) - this.multiples.settings.height = - this.multiples.settings.multiples_sizing.height + - (this.multiples.settings.margin.bottom ? this.multiples.settings.margin.bottom : 0); - - this.multiples.settings.margin = { bottom: this.multiples.settings.margin.bottom || 20 }; - - //Add participant dropdown. - this.multiples.settings.selected_id = this.selected_id; - this.multiples.controls = webcharts.createControls(this.multiples.container.node(), { - inputs: [ - { - type: 'dropdown', - label: 'All Measures for', - option: 'selected_id', - values: this.IDOrder.map(function(d) { - return d.ID; - }), - require: true - } - ] - }); - - //Initialize small multiples. - this.multiples.chart = webcharts.createChart( - this.multiples.container.node(), - this.multiples.settings, - this.multiples.controls - ); - this.multiples.chart.safetyOutlierExplorer = this; - } - - function participantCharacteristics() { - var _this = this; - - this.multiples.detail_table = this.multiples.chart.wrap - .insert('table', '.legend') - .append('tbody') - .classed('detail-listing', true); - this.multiples.detail_table - .append('thead') - .selectAll('th') - .data(['', '']) - .enter() - .append('th'); - this.multiples.detail_table.append('tbody'); - - //Insert a line for each item in [ settings.detail_cols ]. - if (Array.isArray(this.config.details) && this.config.details.length) { - var participantDatum = this.multiples.data[0]; - this.config.details.forEach(function(detail) { - var value_col = detail.value_col ? detail.value_col : detail; - var label = detail.label - ? detail.label - : detail.value_col - ? detail.value_col - : detail; - var tuple = [label, participantDatum[value_col]]; - - if (tuple[1] !== undefined) - _this.multiples.detail_table - .select('tbody') - .append('tr') - .selectAll('td') - .data(tuple) - .enter() - .append('td') - .style('text-align', function(d, i) { - return i === 0 ? 'right' : 'left'; - }) - .text(function(d, i) { - return i === 0 ? d + ':' : d; - }); - }); - } - } - - function onLayout$1() { - this.multiples.chart.on('layout', function() { - //Define multiple styling. - this.wrap.style('display', 'block'); - this.wrap - .selectAll('.wc-chart-title') - .style('display', 'block') - .style('border-top', '1px solid #eee'); - this.wrap.selectAll('.wc-chart').style('padding-bottom', '2px'); - - //Set y-label to measure unit. - this.config.y.label = ''; - - //Outline currently selected measure. - //if (this.filters[0].val === this.parent.safetyOutlierExplorer.measure.current) - // this.wrap - // .select('.wc-chart-title') - // .append('span') - // .html(' ⓘ') - // .style({ - // 'font-weight': 'bold', - // 'cursor': 'default', - // }) - // .attr('title', 'Currently selected measure'); - }); - } - - function onPreprocess$1() { - this.multiples.chart.on('preprocess', function() { - var _this = this; - - //Define y-domain as minimum of lower limit of normal and minimum result and maximum of - //upper limit of normal and maximum result. - var filtered_data = this.raw_data.filter(function(f) { - return f[_this.filters[0].col] === _this.filters[0].val; - }); - - //Calculate range of normal range. - var normlo = Math.min.apply( - null, - filtered_data - .map(function(m) { - return +m[_this.config.normal_col_low]; - }) - .filter(function(f) { - return +f || +f === 0; - }) - ); - var normhi = Math.max.apply( - null, - filtered_data - .map(function(m) { - return +m[_this.config.normal_col_high]; - }) - .filter(function(f) { - return +f || +f === 0; - }) - ); - - //Calculate range of data. - var ylo = d3$1.min( - filtered_data - .map(function(m) { - return +m[_this.config.y.column]; - }) - .filter(function(f) { - return +f || +f === 0; - }) - ); - var yhi = d3$1.max( - filtered_data - .map(function(m) { - return +m[_this.config.y.column]; - }) - .filter(function(f) { - return +f || +f === 0; - }) - ); - - //Set y-domain. - this.config.y_dom = [Math.min(normlo, ylo), Math.max(normhi, yhi)]; - }); - } - - function adjustTicks() { - if (this.config.x.rotate_tick_labels) - this.svg - .selectAll('.x.axis .tick text') - .attr({ - transform: 'rotate(-45)', - dx: -10, - dy: 10 - }) - .style('text-anchor', 'end'); - } - - function rangePolygon() { - var _this = this; - - var area = d3$1.svg - .area() - .x(function(d) { - return ( - _this.x(d['TIME']) + - (_this.config.x.type === 'ordinal' ? _this.x.rangeBand() / 2 : 0) - ); - }) - .y0(function(d) { - return /^-?[0-9.]+$/.test(d[_this.config.normal_col_low]) - ? _this.y(d[_this.config.normal_col_low]) - : 0; - }) - .y1(function(d) { - return /^-?[0-9.]+$/.test(d[_this.config.normal_col_high]) - ? _this.y(d[_this.config.normal_col_high]) - : 0; - }); - - var dRow = this.filtered_data[0]; - - var myRows = this.x_dom.slice().map(function(m) { - return { - STNRLO: dRow[_this.config.normal_col_low], - STNRHI: dRow[_this.config.normal_col_high], - TIME: m - }; - }); - - //remove what is there now - this.svg.select('.norms').remove(); - - //add new - var normalRange = this.svg - .append('g') - .datum(myRows) - .attr('class', 'norms'); - normalRange - .append('path') - .attr('fill', 'blue') - .attr('fill-opacity', 0.1) - .attr('d', area); - normalRange.append('title').text(function(d) { - return 'Normal range: ' + d[0].STNRLO + '-' + d[0].STNRHI; - }); - } - - function onResize() { - this.multiples.chart.on('resize', function() { - //Resize text manually. - this.wrap.select('.wc-chart-title').style('font-size', '12px'); - this.svg.selectAll('.axis .tick text').style('font-size', '10px'); - - //Draw normal range. - if (this.filtered_data.length) rangePolygon.call(this); - - //Axis tweaks - this.svg - .select('.x.axis') - .select('.axis-title') - .remove(); - - //Delete legend. - this.legend.remove(); - - //Rotate ticks. - adjustTicks.call(this); - }); - } - - function updateParticipantDropdown() { - var context = this; // chart - - var participantDropdown = this.multiples.controls.wrap - .style('margin', 0) - .selectAll('.control-group') - .filter(function(d) { - return d.option === 'selected_id'; - }) - .style('margin', 0) - .style('display', 'block'); // firefox is being weird about inline-table - participantDropdown.selectAll('*').style('display', 'inline-block'); - participantDropdown.selectAll('.wc-control-label').style('font-weight', 'bold'); - participantDropdown - .selectAll('select') - .style('margin-left', '3px') - .style('width', null) - .style('max-width', '10%') - .on('change', function(d) { - context.multiples.id = d3$1 - .select(this) - .selectAll('option:checked') - .text(); - clearSelected.call(context); - context.selected_id = context.multiples.id; - highlightSelected.call(context); - smallMultiples.call(context); - context.wrap.select('div.overlapNote').remove(); - - //Trigger participantsSelected event - context.participantsSelected = [context.selected_id]; - context.events.participantsSelected.data = context.participantsSelected; - context.wrap.node().dispatchEvent(context.events.participantsSelected); - }); - } - - function smallMultiples() { - var _this = this; - - //Define participant data. - this.multiples.data = this.initial_data.filter(function(d) { - return d[_this.config.id_col] === _this.selected_id; - }); - - //Define small multiples. - defineSmallMultiples.call(this); - - //Insert participant characteristics table. - participantCharacteristics.call(this); - - //Add callbacks to small multiples. - onLayout$1.call(this); - onPreprocess$1.call(this); - onResize.call(this); - - //Initialize small multiples. - webcharts.multiply(this.multiples.chart, this.multiples.data, 'soe_measure', this.measures); - - //Update participant dropdown. - updateParticipantDropdown.call(this); - } - - function addLineEventListeners() { - var _this = this; - - this.lines - .on('mouseover', function(d) { - clearHovered.call(_this); - _this.hovered_id = d.values[0].values.raw[0][_this.config.id_col]; - if (_this.hovered_id !== _this.selected_id) highlightHovered.call(_this); - }) - .on('mouseout', function(d) { - clearHovered.call(_this); - }) - .on('click', function(d) { - clearHovered.call(_this); - clearSelected.call(_this); - _this.selected_id = d.values[0].values.raw[0][_this.config.id_col]; - highlightSelected.call(_this); - smallMultiples.call(_this); - - //Trigger participantsSelected event - _this.participantsSelected = [_this.selected_id]; - _this.events.participantsSelected.data = _this.participantsSelected; - _this.wrap.node().dispatchEvent(_this.events.participantsSelected); - }); - } - - function checkPointOverlap(d, chart) { - // Get the position of the clicked point - var click_x = d3 - .select(this) - .select('circle') - .attr('cx'); - var click_y = d3 - .select(this) - .select('circle') - .attr('cy'); - var click_r = d3 - .select(this) - .select('circle') - .attr('r'); - var click_id = d.values.raw[0][chart.config.id_col]; - - // See if any other points overlap - var overlap_ids = chart.points - .filter(function(f) { - var point_id = f.values.raw[0][chart.config.id_col]; - var point_x = d3 - .select(this) - .select('circle') - .attr('cx'); - var point_y = d3 - .select(this) - .select('circle') - .attr('cy'); - var distance_x2 = Math.pow(click_x - point_x, 2); - var distance_y2 = Math.pow(click_y - point_y, 2); - var distance = Math.sqrt(distance_x2 + distance_y2); - - var max_distance = click_r * 2; - var overlap = distance <= max_distance; - var diff_id = point_id != click_id; - return diff_id & overlap; - }) - .data() - .map(function(d) { - return d.values.raw[0][chart.config.id_col]; - }); - - return overlap_ids; - } - - function addOverlapNote(d, chart) { - function showID(d) { - //click an overlapping ID to see details for that participant - var participantDropdown = chart.multiples.controls.wrap - .style('margin', 0) - .selectAll('.control-group') - .filter(function(d) { - return d.option === 'selected_id'; - }) - .select('select') - .property('value', d); - - //participantDropdown.on("change")() // Can't quite get this to work, so copy/pasting for now ... - - var context = chart; - chart.multiples.id = d; - clearSelected.call(context); - context.selected_id = context.multiples.id; - highlightSelected.call(context); - smallMultiples.call(context); - - //Trigger participantsSelected event - context.participantsSelected = [context.selected_id]; - context.events.participantsSelected.data = context.participantsSelected; - context.wrap.node().dispatchEvent(context.events.participantsSelected); - } - - chart.wrap.select('div.overlapNote').remove(); - - // check for overlapping points - chart.overlap_ids = checkPointOverlap.call(this, d, chart); - - // If there are overlapping points, add a note in the details section. - - if (chart.overlap_ids.length) { - var click_id = d.values.raw[0][chart.config.id_col]; - var overlap_div = chart.wrap - .insert('div', 'div.multiples') - .attr('class', 'overlapNote') - .style('background-color', '#eee') - .style('border', '1px solid #999') - .style('padding', '0.5em') - .style('border-radius', '0.2em') - .style('margin', '0 0.1em'); - - overlap_div - .append('span') - .html( - 'Note: ' + - chart.overlap_ids.length + - (' point' + - (chart.overlap_ids.length === 1 ? '' : 's') + - ' overlap' + - (chart.overlap_ids.length === 1 ? 's' : '') + - ' the clicked point for ') + - click_id + - '. Click an ID for details: ' - ); - overlap_div - .select('span.idLink') - .datum(click_id) - .style('color', 'blue') - .style('text-decoration', 'underline') - .style('cursor', 'pointer') - .on('click', showID) - .on('mouseover', function(d) { - clearHovered.call(chart); - chart.hovered_id = d; - if (chart.hovered_id !== chart.selected_id) highlightHovered.call(chart); - }) - .on('mouseout', function(d) { - clearHovered.call(chart); - }); - var overlap_ul = overlap_div - .append('ul') - .style('list-style', 'none') - .style('padding', '0') - .style('display', 'inline-block'); - - overlap_ul - .selectAll('li') - .data(chart.overlap_ids) - .enter() - .append('li') - .style('display', 'inline-block') - .style('padding-right', '.5em') - .attr('class', 'idLink') - .style('color', 'blue') - .style('text-decoration', 'underline') - .style('cursor', 'pointer') - .text(function(d) { - return d; - }) - .on('click', showID) - .on('mouseover', function(d) { - clearHovered.call(chart); - chart.hovered_id = d; - if (chart.hovered_id !== chart.selected_id) highlightHovered.call(chart); - }) - .on('mouseout', function(d) { - clearHovered.call(chart); - }); - } - } - - function addOverlapTitle(d, chart) { - // check for overlapping points - var overlap = checkPointOverlap.call(this, d, chart); - - // If there are overlapping points, add a note in the details section. - - if (overlap.length > 0) { - var titleEl = d3.select(this).select('title'); - var currentTitle = titleEl.text(); - var hasOverlapNote = currentTitle.search('overlapping'); //minor hack ... - if (hasOverlapNote == -1) { - var newTitle = - currentTitle + '\nNumber of overlapping point(s) = ' + overlap.length; - titleEl.text(newTitle); - } - } - } - - function addPointEventListeners() { - var _this = this; - - var chart = this; - this.points - .on('mouseover', function(d) { - addOverlapTitle.call(this, d, chart); - clearHovered.call(chart); - chart.hovered_id = d.values.raw[0][chart.config.id_col]; - if (chart.hovered_id !== chart.selected_id) highlightHovered.call(chart); - }) - .on('mouseout', function(d) { - clearHovered.call(_this); - }) - .on('click', function(d) { - clearHovered.call(chart); - clearSelected.call(chart); - chart.selected_id = d.values.raw[0][chart.config.id_col]; - highlightSelected.call(chart); - smallMultiples.call(chart); - - //Trigger participantsSelected event - chart.participantsSelected = [chart.selected_id]; - chart.events.participantsSelected.data = chart.participantsSelected; - chart.wrap.node().dispatchEvent(chart.events.participantsSelected); - - //check for overlapping points - addOverlapNote.call(this, d, chart); - }); - } - - function addEventListeners() { - addOverlayEventListener.call(this); - addOverlayEventListener$1.call(this); - addLineEventListeners.call(this); - addPointEventListeners.call(this); - } - - function addBoxPlot() { - //Clear box plot. - this.svg.select('g.boxplot').remove(); - - //Customize box plot. - var svg = this.svg; - var results = this.current_data - .map(function(d) { - return +d.values.y; - }) - .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$1.format('.3r'); - - //set up scales - var x = d3$1.scale.linear().range([0, width]); - var y = d3$1.scale.linear().range([height, 0]); - - { - y.domain(domain); - } - - var probs = [0.05, 0.25, 0.5, 0.75, 0.95]; - for (var i = 0; i < probs.length; i++) { - probs[i] = d3$1.quantile(results, probs[i]); - } - - var boxplot = this.svg - .append('g') - .attr('class', 'boxplot') - .datum({ - values: results, - probs: probs - }) - .attr( - 'transform', - 'translate(' + (this.plot_width + this.config.margin.right / 2) + ',0)' - ); - - //draw rectangle from q1 to q3 - var box_x = x(0.5 - boxPlotWidth / 2); - var box_width = x(0.5 + boxPlotWidth / 2) - x(0.5 - boxPlotWidth / 2); - var box_y = y(probs[3]); - var box_height = -y(probs[3]) + y(probs[1]); - - boxplot - .append('rect') - .attr('class', 'boxplot fill') - .attr('x', box_x) - .attr('width', box_width) - .attr('y', box_y) - .attr('height', box_height) - .style('fill', boxColor); - - //draw dividing lines at median, 95% and 5% - var iS = [0, 2, 4]; - var iSclass = ['', 'median', '']; - var iSColor = [boxColor, boxInsideColor, boxColor]; - for (var i = 0; i < iS.length; i++) { - boxplot - .append('line') - .attr('class', 'boxplot ' + iSclass[i]) - .attr('x1', x(0.5 - boxPlotWidth / 2)) - .attr('x2', x(0.5 + boxPlotWidth / 2)) - .attr('y1', y(probs[iS[i]])) - .attr('y2', y(probs[iS[i]])) - .style('fill', iSColor[i]) - .style('stroke', iSColor[i]); - } - - //draw lines from 5% to 25% and from 75% to 95% - var iS = [[0, 1], [3, 4]]; - for (var i = 0; i < iS.length; i++) { - boxplot - .append('line') - .attr('class', 'boxplot') - .attr('x1', x(0.5)) - .attr('x2', x(0.5)) - .attr('y1', y(probs[iS[i][0]])) - .attr('y2', y(probs[iS[i][1]])) - .style('stroke', boxColor); - } - - boxplot - .append('circle') - .attr('class', 'boxplot mean') - .attr('cx', x(0.5)) - .attr('cy', y(d3$1.mean(results))) - .attr('r', x(boxPlotWidth / 3)) - .style('fill', boxInsideColor) - .style('stroke', boxColor); - - boxplot - .append('circle') - .attr('class', 'boxplot mean') - .attr('cx', x(0.5)) - .attr('cy', y(d3$1.mean(results))) - .attr('r', x(boxPlotWidth / 6)) - .style('fill', boxColor) - .style('stroke', 'None'); - - boxplot.append('title').text(function(d) { - var tooltip = - 'N = ' + - d.values.length + - '\n' + - 'Min = ' + - d3$1.min(d.values) + - '\n' + - '5th % = ' + - fmt(d3$1.quantile(d.values, 0.05)).replace(/^ */, '') + - '\n' + - 'Q1 = ' + - fmt(d3$1.quantile(d.values, 0.25)).replace(/^ */, '') + - '\n' + - 'Median = ' + - fmt(d3$1.median(d.values)).replace(/^ */, '') + - '\n' + - 'Q3 = ' + - fmt(d3$1.quantile(d.values, 0.75)).replace(/^ */, '') + - '\n' + - '95th % = ' + - fmt(d3$1.quantile(d.values, 0.95)).replace(/^ */, '') + - '\n' + - 'Max = ' + - d3$1.max(d.values) + - '\n' + - 'Mean = ' + - fmt(d3$1.mean(d.values)).replace(/^ */, '') + - '\n' + - 'StDev = ' + - fmt(d3$1.deviation(d.values)).replace(/^ */, ''); - return tooltip; - }); - } - - function onResize$1() { - //Attach mark groups to central chart object. - attachMarks.call(this); - - //Maintain mark highlighting. - maintainHighlight.call(this); - - //Draw normal range. - drawNormalRange.call(this); - - //Add event listeners to lines, points, and overlay. - addEventListeners.call(this); - - //Draw a marginal box plot. - addBoxPlot.call(this); - - //Rotate tick marks to prevent text overlap. - adjustTicks.call(this); - } - - function onDestroy() {} - - var callbacks = { - onInit: onInit, - onLayout: onLayout, - onPreprocess: onPreprocess, - onDatatransform: onDatatransform, - onDraw: onDraw, - onResize: onResize$1, - onDestroy: onDestroy - }; - - function safetyOutlierExplorer(element, settings) { - //Merge user settings with default settings. - var mergedSettings = Object.assign({}, configuration.settings, settings); - - //Sync options within settings object, e.g. data mappings. - var syncedSettings = configuration.syncSettings(mergedSettings); - - //Sync control inputs with with settings object. - var syncedControlInputs = configuration.syncControlInputs( - configuration.controlInputs(), - syncedSettings - ); - - //Define controls. - var controls = webcharts.createControls(element, { - location: 'top', - inputs: syncedControlInputs - }); - - //Define chart. - var chart = webcharts.createChart(element, syncedSettings, controls); - chart.config.marks.forEach(function(mark) { - mark.attributes = mark.attributes || {}; - mark.attributes['clip-path'] = 'url(#' + chart.id + ')'; - }); - - //Attach callbacks to chart. - for (var callback in callbacks) { - chart.on(callback.substring(2).toLowerCase(), callbacks[callback]); - } - return chart; - } - - return safetyOutlierExplorer; -}); diff --git a/inst/htmlwidgets/lib/safety-results-over-time-2.3.3/safetyResultsOverTime.js b/inst/htmlwidgets/lib/safety-results-over-time-2.3.3/safetyResultsOverTime.js deleted file mode 100644 index 0a21b6ac..00000000 --- a/inst/htmlwidgets/lib/safety-results-over-time-2.3.3/safetyResultsOverTime.js +++ /dev/null @@ -1,1866 +0,0 @@ -(function(global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' - ? (module.exports = factory(require('d3'), require('webcharts'))) - : typeof define === 'function' && define.amd - ? define(['d3', 'webcharts'], factory) - : ((global = global || self), - (global.safetyResultsOverTime = factory(global.d3, global.webCharts))); -})(this, function(d3, webcharts) { - 'use strict'; - - if (typeof Object.assign != 'function') { - Object.defineProperty(Object, 'assign', { - value: function assign(target, varArgs) { - if (target == null) { - // TypeError if undefined or null - throw new TypeError('Cannot convert undefined or null to object'); - } - - var to = Object(target); - - for (var index = 1; index < arguments.length; index++) { - var nextSource = arguments[index]; - - if (nextSource != null) { - // Skip over if undefined or null - for (var nextKey in nextSource) { - // Avoid bugs when hasOwnProperty is shadowed - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - - return to; - }, - writable: true, - configurable: true - }); - } - - if (!Array.prototype.find) { - Object.defineProperty(Array.prototype, 'find', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, 'length')). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return kValue. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return kValue; - } - // e. Increase k by 1. - k++; - } - - // 7. Return undefined. - return undefined; - } - }); - } - - if (!Array.prototype.findIndex) { - Object.defineProperty(Array.prototype, 'findIndex', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, "length")). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return k. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return k; - } - // e. Increase k by 1. - k++; - } - - // 7. Return -1. - return -1; - } - }); - } - - Math.log10 = - Math.log10 || - function(x) { - return Math.log(x) * Math.LOG10E; - }; - - var _typeof = - typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' - ? function(obj) { - return typeof obj; - } - : function(obj) { - return obj && - typeof Symbol === 'function' && - obj.constructor === Symbol && - obj !== Symbol.prototype - ? 'symbol' - : typeof obj; - }; - - var hasOwnProperty = Object.prototype.hasOwnProperty; - var propIsEnumerable = Object.prototype.propertyIsEnumerable; - - function toObject(val) { - if (val === null || val === undefined) { - throw new TypeError('Cannot convert undefined or null to object'); - } - - return Object(val); - } - - function isObj(x) { - var type = typeof x === 'undefined' ? 'undefined' : _typeof(x); - return x !== null && (type === 'object' || type === 'function'); - } - - function assignKey(to, from, key) { - var val = from[key]; - - if (val === undefined) { - return; - } - - if (hasOwnProperty.call(to, key)) { - if (to[key] === undefined) { - throw new TypeError('Cannot convert undefined or null to object (' + key + ')'); - } - } - - if (!hasOwnProperty.call(to, key) || !isObj(val)) to[key] = val; - else if (val instanceof Array) to[key] = from[key]; - // figure out how to merge arrays without converting them into objects - else to[key] = assign(Object(to[key]), from[key]); - } - - function assign(to, from) { - if (to === from) { - return to; - } - - from = Object(from); - - for (var key in from) { - if (hasOwnProperty.call(from, key)) { - assignKey(to, from, key); - } - } - - if (Object.getOwnPropertySymbols) { - var symbols = Object.getOwnPropertySymbols(from); - - for (var i = 0; i < symbols.length; i++) { - if (propIsEnumerable.call(from, symbols[i])) { - assignKey(to, from, symbols[i]); - } - } - } - - return to; - } - - function merge(target) { - target = toObject(target); - - for (var s = 1; s < arguments.length; s++) { - assign(target, arguments[s]); - } - - return target; - } - - function rendererSettings() { - return { - id_col: 'USUBJID', - time_settings: { - value_col: 'VISIT', - label: 'Visit', - order_col: 'VISITNUM', - order: null, - rotate_tick_labels: true, - vertical_space: 100 - }, - measure_col: 'TEST', - value_col: 'STRESN', - unit_col: 'STRESU', - normal_col_low: 'STNRLO', - normal_col_high: 'STNRHI', - start_value: null, - filters: null, - groups: null, - color_by: null, - boxplots: true, - outliers: true, - violins: false, - missingValues: ['', 'NA', 'N/A'], - visits_without_data: false, - unscheduled_visits: false, - unscheduled_visit_pattern: '/unscheduled|early termination/i', - unscheduled_visit_values: null // takes precedence over unscheduled_visit_pattern - }; - } - - function webchartsSettings() { - return { - x: { - column: null, // set in syncSettings() - type: 'ordinal', - label: null, - behavior: 'flex', - sort: 'alphabetical-ascending', - tickAttr: null - }, - y: { - column: null, // set in syncSettings() - type: 'linear', - label: null, - behavior: 'flex', - stat: 'mean', - format: null // set in ./onPreprocess/setYprecision() - }, - marks: [ - { - type: 'line', - per: null, // set in syncSettings() - attributes: { - 'stroke-width': 2, - 'stroke-opacity': 1, - display: 'none' - } - }, - { - type: 'circle', - per: null, // set in syncSettings() - attributes: { - stroke: 'black', - 'stroke-opacity': 0, - 'fill-opacity': 0 - }, - values: { - srot_outlier: [true] - }, - radius: null, // set in syncSettings() - tooltip: null, // set in syncSettings() - hidden: true - }, - { - type: 'circle', - per: null, // set in syncSettings() - attributes: { - stroke: 'black', - 'stroke-opacity': 1, - 'fill-opacity': 1 - }, - values: { - srot_outlier: [true] - }, - radius: 1.75, - tooltip: null, // set in syncSettings() - hidden: false - } - ], - legend: { - mark: 'square' - }, - color_by: null, // set in syncSettings() - resizable: true, - gridlines: 'y', - aspect: 3 - }; - } - - function syncSettings(settings) { - //x-axis - settings.x.column = settings.time_settings.value_col; - settings.x.label = settings.time_settings.label; - settings.x.behavior = settings.visits_without_data ? 'raw' : 'flex'; - - //y-axis - settings.y.column = settings.value_col; - - //handle a string arguments to array settings - var array_settings = ['filters', 'groups', 'missingValues']; - array_settings.forEach(function(s) { - if (!(settings[s] instanceof Array)) - settings[s] = typeof settings[s] === 'string' ? [settings[s]] : []; - }); - - //stratification - var defaultGroup = { value_col: 'srot_none', label: 'None' }; - if (!(settings.groups instanceof Array && settings.groups.length)) - settings.groups = [defaultGroup]; - else - settings.groups = [defaultGroup].concat( - settings.groups.map(function(group) { - return { - value_col: group.value_col || group, - label: group.label || group.value_col || group - }; - }) - ); - - //Remove duplicate values. - settings.groups = d3 - .set( - settings.groups.map(function(group) { - return group.value_col; - }) - ) - .values() - .map(function(value) { - return { - value_col: value, - label: settings.groups.find(function(group) { - return group.value_col === value; - }).label - }; - }); - - //Set initial group-by variable. - settings.color_by = settings.color_by - ? settings.color_by - : settings.groups.length > 1 - ? settings.groups[1].value_col - : defaultGroup.value_col; - - //Set initial group-by label. - settings.legend.label = settings.groups.find(function(group) { - return group.value_col === settings.color_by; - }).label; - - //marks - var lines = settings.marks.find(function(mark) { - return mark.type === 'line'; - }); - var hiddenOutliers = settings.marks.find(function(mark) { - return mark.type === 'circle' && mark.hidden; - }); - var visibleOutliers = settings.marks.find(function(mark) { - return mark.type === 'circle' && !mark.hidden; - }); - lines.per = [settings.color_by]; - hiddenOutliers.radius = visibleOutliers.radius * 4; - settings.marks - .filter(function(mark) { - return mark.type === 'circle'; - }) - .forEach(function(mark) { - mark.per = [settings.id_col, settings.time_settings.value_col, settings.value_col]; - mark.tooltip = - '[' + - settings.id_col + - '] at [' + - settings.x.column + - ']: [' + - settings.value_col + - ']'; - }); - - //miscellany - settings.margin = settings.margin || { bottom: settings.time_settings.vertical_space }; - - //Convert unscheduled_visit_pattern from string to regular expression. - if ( - typeof settings.unscheduled_visit_pattern === 'string' && - settings.unscheduled_visit_pattern !== '' - ) { - var 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; - } - - function controlInputs() { - return [ - { - type: 'subsetter', - label: 'Measure', - value_col: 'srot_measure', // set in syncControlInputs() - start: null // set in ../callbacks/onInit/setInitialMeasure.js - }, - { - type: 'dropdown', - label: 'Group by', - options: ['marks.0.per.0', 'color_by'], - start: null, // set in ./syncControlInputs.js - values: null, // set in ./syncControlInputs.js - require: true - }, - { - type: 'number', - label: 'Lower', - grouping: 'y-axis', - option: 'y.domain[0]', - require: true - }, - { - type: 'number', - label: 'Upper', - grouping: 'y-axis', - option: 'y.domain[1]', - require: true - }, - { - type: 'radio', - option: 'y.type', - grouping: 'y-axis', - values: ['linear', 'log'], - label: 'Scale' - }, - { - type: 'checkbox', - inline: true, - option: 'visits_without_data', - label: 'Visits without data' - }, - { - type: 'checkbox', - inline: true, - option: 'unscheduled_visits', - label: 'Unscheduled visits' - }, - { type: 'checkbox', inline: true, option: 'boxplots', label: 'Box plots' }, - { type: 'checkbox', inline: true, option: 'violins', label: 'Violin plots' }, - { type: 'checkbox', inline: true, option: 'outliers', label: 'Outliers' } - ]; - } - - function syncControlInputs(controlInputs, settings) { - //Sync group control. - var groupControl = controlInputs.find(function(controlInput) { - return controlInput.label === 'Group by'; - }); - groupControl.start = settings.groups.find(function(group) { - return group.value_col === settings.color_by; - }).label; - groupControl.values = settings.groups.map(function(group) { - return group.label; - }); - - //Add custom filters to control inputs. - if (settings.filters) { - settings.filters.reverse().forEach(function(filter) { - var thisFilter = { - type: 'subsetter', - value_col: filter.value_col ? filter.value_col : filter, - label: filter.label - ? filter.label - : filter.value_col - ? filter.value_col - : filter, - description: 'filter' - }; - - //add the filter to the control inputs (as long as it's not already there) - var current_value_cols = controlInputs - .filter(function(f) { - return f.type == 'subsetter'; - }) - .map(function(m) { - return m.value_col; - }); - if (current_value_cols.indexOf(thisFilter.value_col) == -1) - controlInputs.splice(1, 0, thisFilter); - }); - } - - //Remove unscheduled visit control if unscheduled visit pattern is unscpecified. - if (!settings.unscheduled_visit_regex) - controlInputs.splice( - controlInputs - .map(function(controlInput) { - return controlInput.label; - }) - .indexOf('Unscheduled visits'), - 1 - ); - - return controlInputs; - } - - var configuration = { - rendererSettings: rendererSettings, - webchartsSettings: webchartsSettings, - defaultSettings: Object.assign({}, rendererSettings(), webchartsSettings()), - syncSettings: syncSettings, - controlInputs: controlInputs, - syncControlInputs: syncControlInputs - }; - - function countParticipants() { - var _this = this; - - this.populationCount = d3 - .set( - this.raw_data.map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values().length; - } - - function cleanData() { - var _this = this; - - //Remove missing and non-numeric data. - var preclean = this.raw_data, - clean = this.raw_data.filter(function(d) { - return /^-?[0-9.]+$/.test(d[_this.config.value_col]); - }), - nPreclean = preclean.length, - nClean = clean.length, - nRemoved = nPreclean - nClean; - - //Warn user of removed records. - if (nRemoved > 0) - console.warn( - nRemoved + - ' missing or non-numeric result' + - (nRemoved > 1 ? 's have' : ' has') + - ' been removed.' - ); - this.initial_data = clean; - this.raw_data = clean; - } - - function addVariables() { - var _this = this; - - this.raw_data.forEach(function(d) { - //Convert results to numeric - d[_this.config.y.column] = parseFloat(d[_this.config.y.column]); - - //Concatenate unit to measure if provided. - d.srot_measure = d.hasOwnProperty(_this.config.unit_col) - ? d[_this.config.measure_col] + ' (' + d[_this.config.unit_col] + ')' - : d[_this.config.measure_col]; - - //Add placeholder variable for non-grouped comparisons. - d.srot_none = 'All Participants'; - - //Add placeholder variable for outliers. - d.srot_outlier = null; - }); - this.variables = Object.keys(this.raw_data[0]); - } - - function defineVisitOrder() { - var _this = this; - - var visits = void 0, - visitOrder = void 0; - - //Given an ordering variable sort a unique set of visits by the ordering variable. - if ( - this.config.time_settings.order_col && - this.raw_data[0].hasOwnProperty(this.config.time_settings.order_col) - ) { - //Define a unique set of visits with visit order concatenated. - visits = d3 - .set( - this.raw_data.map(function(d) { - return ( - d[_this.config.time_settings.order_col] + - '|' + - d[_this.config.time_settings.value_col] - ); - }) - ) - .values(); - - //Sort visits. - visitOrder = visits - .sort(function(a, b) { - var aOrder = a.split('|')[0], - bOrder = b.split('|')[0], - diff = +aOrder - +bOrder; - return diff ? diff : d3.ascending(a, b); - }) - .map(function(visit) { - return visit.split('|')[1]; - }); - } else { - //Otherwise sort a unique set of visits alphanumerically. - //Define a unique set of visits. - visits = d3 - .set( - this.raw_data.map(function(d) { - return d[_this.config.time_settings.value_col]; - }) - ) - .values(); - - //Sort visits; - visitOrder = visits.sort(); - } - - //Set x-axis domain. - if (this.config.time_settings.order) { - //If a visit order is specified, use it and concatenate any unspecified visits at the end. - this.config.x.order = this.config.time_settings.order.concat( - visitOrder.filter(function(visit) { - return _this.config.time_settings.order.indexOf(visit) < 0; - }) - ); - } - //Otherwise use data-driven visit order. - else this.config.x.order = visitOrder; - } - - function checkFilters() { - var _this = this; - - this.controls.config.inputs = this.controls.config.inputs.filter(function(input) { - if (input.type != 'subsetter') { - return true; - } else if (!_this.raw_data[0].hasOwnProperty(input.value_col)) { - console.warn( - 'The [ ' + - input.label + - ' ] filter has been removed because the variable does not exist.' - ); - } else { - var levels = d3 - .set( - _this.raw_data.map(function(d) { - return d[input.value_col]; - }) - ) - .values(); - - if (levels.length === 1) - console.warn( - 'The [ ' + - input.label + - ' ] filter has been removed because the variable has only one level.' - ); - - return levels.length > 1; - } - }); - } - - function checkGroupByVariables() { - var _this = this; - - var groupByInput = this.controls.config.inputs.find(function(input) { - return input.label === 'Group by'; - }); - this.config.groups = this.config.groups.filter(function(group) { - var groupByExists = _this.variables.indexOf(group.value_col) > -1; - if (!groupByExists) - console.warn( - 'The [ ' + - group.label + - ' ] group-by option has been removed because the variable does not exist.' - ); - return groupByExists; - }); - groupByInput.values = this.config.groups.map(function(group) { - return group.label; - }); - } - - function defineMeasureSet() { - var _this = this; - - this.measures = d3 - .set( - this.initial_data.map(function(d) { - return d[_this.config.measure_col]; - }) - ) - .values() - .sort(); - this.srot_measures = d3 - .set( - this.initial_data.map(function(d) { - return d.srot_measure; - }) - ) - .values() - .sort(); - } - - function setInitialMeasure() { - var measureInput = this.controls.config.inputs.find(function(input) { - return input.label === 'Measure'; - }); - if ( - this.config.start_value && - this.srot_measures.indexOf(this.config.start_value) < 0 && - this.measures.indexOf(this.config.start_value) < 0 - ) { - measureInput.start = this.srot_measures[0]; - console.warn( - this.config.start_value + - ' is an invalid measure. Defaulting to ' + - measureInput.start + - '.' - ); - } else if ( - this.config.start_value && - this.srot_measures.indexOf(this.config.start_value) < 0 - ) { - measureInput.start = this.srot_measures[this.measures.indexOf(this.config.start_value)]; - console.warn( - this.config.start_value + - ' is missing the units value. Defaulting to ' + - measureInput.start + - '.' - ); - } else measureInput.start = this.config.start_value || this.srot_measures[0]; - } - - function onInit() { - // 1. Count total participants prior to data cleaning. - countParticipants.call(this); - - // 2. Drop missing values and remove measures with any non-numeric results. - cleanData.call(this); - - // 3a Define additional variables. - addVariables.call(this); - - // 3b Define ordered x-axis domain with visit order variable. - defineVisitOrder.call(this); - - // 3c Remove filters for nonexistent or single-level variables. - checkFilters.call(this); - - // 3d Remove group-by options for nonexistent variables. - checkGroupByVariables.call(this); - - // 4. Define set of measures. - defineMeasureSet.call(this); - - // 5. Set the start value of the Measure filter. - setInitialMeasure.call(this); - } - - function classControlGroups() { - var checkboxOffset = 0; - this.controls.wrap - .style('position', 'relative') - .selectAll('.control-group') - .each(function(d, i) { - var controlGroup = d3.select(this); - controlGroup.classed( - d.type.toLowerCase().replace(' ', '-') + - ' ' + - d.label.toLowerCase().replace(' ', '-'), - true - ); - - //Add y-axis class to group y-axis controls. - if (d.grouping) controlGroup.classed(d.grouping, true); - - //Float all checkboxes right. - if (d.type === 'checkbox') { - controlGroup.style({ - position: 'absolute', - top: checkboxOffset + 'px', - right: 0, - margin: '0' - }); - checkboxOffset += controlGroup.node().offsetHeight; - } - }); - } - - function customizeGroupByControl() { - var _this = this; - - var context = this; - - var groupControl = this.controls.wrap.selectAll('.control-group.dropdown.group-by'); - if (groupControl.datum().values.length === 1) groupControl.style('display', 'none'); - else - groupControl - .selectAll('select') - .on('change', function(d) { - var label = d3 - .select(this) - .selectAll('option:checked') - .text(); - var value_col = context.config.groups.find(function(group) { - return group.label === label; - }).value_col; - context.config.marks[0].per[0] = value_col; - context.config.color_by = value_col; - context.config.legend.label = label; - context.draw(); - }) - .selectAll('option') - .property('selected', function(d) { - return d === _this.config.legend.label; - }); - } - - function addYDomainResetButton() { - var context = this, - resetContainer = this.controls.wrap - .insert('div', '.lower') - .classed('control-group y-axis', true) - .datum({ - type: 'button', - option: 'y.domain', - label: 'Limits' - }), - resetLabel = resetContainer - .append('span') - .attr('class', 'wc-control-label') - .text('Limits'), - resetButton = resetContainer - .append('button') - .style('padding', '0px 5px') - .text('Reset') - .on('click', function() { - var measure_data = context.raw_data.filter(function(d) { - return d.srot_measure === context.currentMeasure; - }); - context.config.y.domain = d3.extent(measure_data, function(d) { - return +d[context.config.value_col]; - }); //reset axis to full range - - context.controls.wrap - .selectAll('.control-group') - .filter(function(f) { - return f.option === 'y.domain[0]'; - }) - .select('input') - .property('value', context.config.y.domain[0]); - - context.controls.wrap - .selectAll('.control-group') - .filter(function(f) { - return f.option === 'y.domain[1]'; - }) - .select('input') - .property('value', context.config.y.domain[1]); - - context.draw(); - }); - } - - function groupYAxisControls() { - //Define a container in which to place y-axis controls. - var grouping = this.controls.wrap - .insert('div', '.y-axis') - .style({ - display: 'inline-block', - 'margin-right': '5px' - }) - .append('fieldset') - .style('padding', '0px 2px'); - grouping.append('legend').text('Y-axis'); - - //Move each y-axis control into container. - this.controls.wrap.selectAll('.y-axis').each(function(d) { - this.style.marginTop = '0px'; - this.style.marginRight = '2px'; - this.style.marginBottom = '2px'; - this.style.marginLeft = '2px'; - grouping.node().appendChild(this); - - //Radio buttons sit too low. - if (d.option === 'y.type') - d3.select(this) - .selectAll('input[type=radio]') - .style({ - top: '-.1em' - }); - }); - } - - function addPopulationCountContainer() { - this.populationCountContainer = this.controls.wrap - .append('div') - .classed('population-count', true) - .style('font-style', 'italic'); - } - - function addBorderAboveChart() { - this.wrap.style('border-top', '1px solid #ccc'); - } - - function onLayout() { - classControlGroups.call(this); - customizeGroupByControl.call(this); - addYDomainResetButton.call(this); - groupYAxisControls.call(this); - addPopulationCountContainer.call(this); - addBorderAboveChart.call(this); - } - - function getCurrentMeasure() { - this.previousMeasure = this.currentMeasure; - this.currentMeasure = this.controls.wrap - .selectAll('.control-group') - .filter(function(d) { - return d.value_col && d.value_col === 'srot_measure'; - }) - .selectAll('option:checked') - .text(); - this.config.y.label = this.currentMeasure; - this.previousYAxis = this.currentYAxis; - this.currentYAxis = this.config.y.type; - } - - function defineMeasureData() { - var _this = this; - - //Filter raw data on selected measure. - this.measure_data = this.initial_data.filter(function(d) { - return d.srot_measure === _this.currentMeasure; - }); - - //Remove nonpositive results given log y-axis. - this.controls.wrap.select('.non-positive-results').remove(); - if (this.config.y.type === 'log') { - var nResults = this.measure_data.length; - this.measure_data = this.measure_data.filter(function(d) { - return +d[_this.config.value_col] > 0; - }); - var nonPositiveResults = nResults - this.measure_data.length; - if (nonPositiveResults > 0) - this.controls.wrap - .selectAll('.axis-type .radio') - .filter(function() { - return ( - d3 - .select(this) - .select('input') - .attr('value') === 'log' - ); - }) - .append('small') - .classed('non-positive-results', true) - .text( - nonPositiveResults + - ' nonpositive result' + - (nonPositiveResults > 1 ? 's' : '') + - ' removed.' - ); - } - this.raw_data = this.measure_data; - - //Apply filter to measure data. - this.filtered_measure_data = this.measure_data; - this.filters.forEach(function(filter) { - _this.filtered_measure_data = _this.filtered_measure_data.filter(function(d) { - return Array.isArray(filter.val) - ? filter.val.indexOf(d[filter.col]) > -1 - : filter.val === d[filter.col] || filter.val === 'All'; - }); - }); - - //Nest data and calculate summary statistics for each visit-group combination. - this.nested_measure_data = d3 - .nest() - .key(function(d) { - return d[_this.config.x.column]; - }) - .key(function(d) { - return d[_this.config.color_by]; - }) - .rollup(function(d) { - var results = { - values: d - .map(function(m) { - return +m[_this.config.y.column]; - }) - .sort(d3.ascending), - n: d.length - }; - - //Calculate summary statistics. - [ - 'min', - ['quantile', 0.05], - ['quantile', 0.25], - 'median', - ['quantile', 0.75], - ['quantile', 0.95], - 'max', - 'mean', - 'deviation' - ].forEach(function(item) { - var fx = Array.isArray(item) ? item[0] : item; - var stat = Array.isArray(item) ? '' + fx.substring(0, 1) + item[1] * 100 : fx; - results[stat] = Array.isArray(item) - ? d3[fx](results.values, item[1]) - : d3[fx](results.values); - }); - - return results; - }) - .entries(this.filtered_measure_data); - } - - function flagOutliers() { - var _this = this; - - this.quantileMap = new Map(); - this.nested_measure_data.forEach(function(visit) { - visit.values.forEach(function(group) { - _this.quantileMap.set( - visit.key + '|' + group.key, // key - [group.values.q5, group.values.q95] // value - ); - }); - }); - this.filtered_measure_data.forEach(function(d) { - var quantiles = _this.quantileMap.get( - d[_this.config.x.column] + '|' + d[_this.config.color_by] - ); - d.srot_outlier = _this.config.outliers - ? d[_this.config.y.column] < quantiles[0] || quantiles[1] < d[_this.config.y.column] - : false; - }); - } - - function removeVisitsWithoutData() { - var _this = this; - - if (!this.config.visits_without_data) - this.config.x.domain = this.config.x.domain.filter(function(visit) { - return ( - d3 - .set( - _this.filtered_measure_data.map(function(d) { - return d[_this.config.time_settings.value_col]; - }) - ) - .values() - .indexOf(visit) > -1 - ); - }); - } - - function removeUnscheduledVisits() { - var _this = this; - - if (!this.config.unscheduled_visits) { - if (this.config.unscheduled_visit_values) - this.config.x.domain = this.config.x.domain.filter(function(visit) { - return _this.config.unscheduled_visit_values.indexOf(visit) < 0; - }); - else if (this.config.unscheduled_visit_regex) - this.config.x.domain = this.config.x.domain.filter(function(visit) { - return !_this.config.unscheduled_visit_regex.test(visit); - }); - - //Remove unscheduled visits from raw data. - this.raw_data = this.raw_data.filter(function(d) { - return _this.config.x.domain.indexOf(d[_this.config.time_settings.value_col]) > -1; - }); - } - } - - function setXdomain() { - this.config.x.domain = this.config.x.order; - removeVisitsWithoutData.call(this); - removeUnscheduledVisits.call(this); - } - - function setYdomain() { - var _this = this; - - //Define y-domain. - if ( - this.currentMeasure !== this.previousMeasure || - this.currentYAxis !== this.previousYAxis - ) - this.config.y.domain = d3.extent( - this.measure_data.map(function(d) { - return +d[_this.config.y.column]; - }) - ); - else if (this.config.y.domain[0] > this.config.y.domain[1]) - // new measure - this.config.y.domain.reverse(); - else if (this.config.y.domain[0] === this.config.y.domain[1]) - // invalid domain - this.config.y.domain = this.config.y.domain.map(function(d, i) { - return i === 0 ? d - d * 0.01 : d + d * 0.01; - }); // domain with zero range - } - - function setYprecision() { - var _this = this; - - //Calculate range of current measure and the log10 of the range to choose an appropriate precision. - this.config.y.range = this.config.y.domain[1] - this.config.y.domain[0]; - this.config.y.log10range = Math.log10(this.config.y.range); - this.config.y.roundedLog10range = Math.round(this.config.y.log10range); - this.config.y.precision1 = -1 * (this.config.y.roundedLog10range - 1); - this.config.y.precision2 = -1 * (this.config.y.roundedLog10range - 2); - - //Define the format of the y-axis tick labels and y-domain controls. - this.config.y.precision = this.config.y.log10range > 0.5 ? 0 : this.config.y.precision1; - this.config.y.format = - this.config.y.log10range > 0.5 ? '1f' : '.' + this.config.y.precision1 + 'f'; - this.config.y.d3_format = d3.format(this.config.y.format); - this.config.y.formatted_domain = this.config.y.domain.map(function(d) { - return _this.config.y.d3_format(d); - }); - - //Define the bin format: one less than the y-axis format. - this.config.y.format1 = - this.config.y.log10range > 5 ? '1f' : '.' + this.config.y.precision2 + 'f'; - this.config.y.d3_format1 = d3.format(this.config.y.format1); - } - - function updateYaxisResetButton() { - //Update tooltip of y-axis domain reset button. - if (this.currentMeasure !== this.previousMeasure) - this.controls.wrap - .selectAll('.y-axis') - .property( - 'title', - 'Initial Limits: [' + - this.config.y.domain[0] + - ' - ' + - this.config.y.domain[1] + - ']' - ); - } - - function updateYaxisLimitControls() { - var _this = this; - - //Update y-axis limit controls. - var step = Math.pow(10, -this.config.y.precision); - var yDomain = this.config.y.domain.map(function(limit) { - return _this.config.y.d3_format(limit); - }); - this.controls.wrap - .selectAll('.control-group') - .filter(function(f) { - return f.option === 'y.domain[0]'; - }) - .select('input') - .attr('step', step) - .property('value', yDomain[0]); - this.controls.wrap - .selectAll('.control-group') - .filter(function(f) { - return f.option === 'y.domain[1]'; - }) - .select('input') - .attr('step', step) - .property('value', yDomain[1]); - } - - function onPreprocess() { - // 1. Capture currently selected measure. - getCurrentMeasure.call(this); - - // 2. Filter data on currently selected measure. - defineMeasureData.call(this); - - // 3a Flag outliers with quantiles calculated in defineMeasureData(). - flagOutliers.call(this); - - // 3a Set x-domain given current visit settings. - setXdomain.call(this); - - // 3b Set y-domain given currently selected measure. - setYdomain.call(this); - - // 4a Define precision of measure. - setYprecision.call(this); - - // 4b Update y-axis reset button when measure changes. - updateYaxisResetButton.call(this); - - // 4c Update y-axis limit controls to match y-axis domain. - updateYaxisLimitControls.call(this); - } - - function onDatatransform() {} - - function updateParticipantCount() { - var _this = this; - - this.populationCountContainer.selectAll('*').remove(); - var subpopulationCount = d3 - .set( - this.filtered_data.map(function(d) { - return d[_this.config.id_col]; - }) - ) - .values().length; - var percentage = d3.format('0.1%')(subpopulationCount / this.populationCount); - this.populationCountContainer.html( - '\n' + - subpopulationCount + - ' of ' + - this.populationCount + - ' participants shown (' + - percentage + - ')' - ); - } - - function removeUnscheduledVisits$1() { - var _this = this; - - if (!this.config.unscheduled_visits) - this.marks.forEach(function(mark) { - if (mark.type === 'line') - mark.data.forEach(function(d) { - d.values = d.values.filter(function(di) { - return _this.config.x.domain.indexOf(di.key) > -1; - }); - }); - else if (mark.type === 'circle') - mark.data = mark.data.filter(function(d) { - return _this.config.x.domain.indexOf(d.values.x) > -1; - }); - }); - } - - function clearCanvas() { - this.svg.selectAll('.y.axis .tick').remove(); - this.svg.selectAll('.point').remove(); // mark data doesn't necessarily get updated (?) - this.svg.selectAll('.boxplot-wrap').remove(); - } - - function updateMarkData() { - var _this = this; - - this.marks.forEach(function(mark, i) { - mark.hidden = _this.config.marks[i].hidden; - }); - this.marks - .filter(function(mark) { - return mark.type === 'circle'; - }) - .forEach(function(mark) { - mark.data.forEach(function(d, i) { - d.id = 'outlier-' + i; - d.hidden = mark.hidden; - d.visit = d.values.x; - d.group = d.values.raw[0][_this.config.color_by]; - }); - }); - } - - function onDraw() { - updateParticipantCount.call(this); - clearCanvas.call(this); - removeUnscheduledVisits$1.call(this); - updateMarkData.call(this); - } - - function editXAxisTicks() { - //Rotate x-axis tick labels. - if (this.config.time_settings.rotate_tick_labels) - this.svg - .selectAll('.x.axis .tick text') - .attr({ - transform: 'rotate(-45)', - dx: -10, - dy: 10 - }) - .style('text-anchor', 'end'); - } - - function drawLogAxis() { - //Draw custom y-axis given a log scale. - if (this.config.y.type === 'log') { - var logYAxis = d3.svg - .axis() - .scale(this.y) - .orient('left') - .ticks(8, ',' + this.config.y.format) - .tickSize(6, 0); - this.svg.select('g.y.axis').call(logYAxis); - } - } - - function handleEmptyAxis() { - var _this = this; - - //Manually draw y-axis ticks when none exist. - if (this.svg.selectAll('.y .tick').size() < 2) { - //Define quantiles of current measure results. - var probs = [ - { probability: 0.1 }, - { probability: 0.3 }, - { probability: 0.5 }, - { probability: 0.7 }, - { probability: 0.9 } - ]; - - for (var i = 0; i < probs.length; i++) { - probs[i].quantile = d3.quantile( - this.measure_data - .map(function(d) { - return +d[_this.config.y.column]; - }) - .sort(function(a, b) { - return a - b; - }), - probs[i].probability - ); - } - - var ticks = probs.map(function(prob) { - return prob.quantile; - }); - - //Manually define y-axis tick values. - this.yAxis.tickValues(ticks); - - //Transition the y-axis to draw the ticks. - this.svg - .select('g.y.axis') - .transition() - .call(this.yAxis); - - //Draw the gridlines. - this.drawGridlines(); - } - } - - function removeDuplicateTickLabels() { - //Manually remove excess y-axis ticks. - var tickLabels = []; - this.svg.selectAll('.y.axis .tick').each(function(d) { - var tick = d3.select(this); - var label = tick.select('text'); - - if (label.size()) { - var tickLabel = label.text(); - - //Check if tick value already exists on axis and if so, remove. - if (tickLabels.indexOf(tickLabel) < 0) tickLabels.push(tickLabel); - else label.remove(); - } - }); - } - - function fixFloatingPointIssues() { - this.svg - .selectAll('.y.axis .tick text') - .filter(function(d) { - return /^\d*\.0*[1-9]0{5,}[1-9]$/.test(d); - }) // floating point issues, e.g. .2 + .1 !== .3 - .remove(); - } - - function editYAxisTicks() { - drawLogAxis.call(this); - handleEmptyAxis.call(this); - removeDuplicateTickLabels.call(this); - fixFloatingPointIssues.call(this); - } - - function clearCanvas$1() { - this.svg.selectAll('.boxplot-wrap').remove(); - } - - function defineScales(subgroup) { - subgroup.boxplot.x = d3.scale.linear().range([0, this.x.rangeBand()]); - subgroup.boxplot.left = subgroup.boxplot.x(0.5 - subgroup.boxplot.boxPlotWidth / 2); - subgroup.boxplot.right = subgroup.boxplot.x(0.5 + subgroup.boxplot.boxPlotWidth / 2); - subgroup.boxplot.y = - this.config.y.type === 'linear' - ? d3.scale - .linear() - .range([this.plot_height, 0]) - .domain(this.y.domain()) - : d3.scale - .log() - .range([this.plot_height, 0]) - .domain(this.y.domain()); - } - - function addContainer(subgroup) { - subgroup.boxplot.container = subgroup.svg - .append('g') - .attr('class', 'boxplot') - .datum({ - values: subgroup.results.values, - probs: subgroup.boxplot.probs - }) - .attr('clip-path', 'url(#' + this.id + ')'); - } - - function drawBox(subgroup) { - subgroup.boxplot.container - .append('rect') - .attr({ - class: 'boxplot fill', - x: subgroup.boxplot.left, - width: subgroup.boxplot.right - subgroup.boxplot.left, - y: subgroup.boxplot.y(subgroup.boxplot.probs[3]), - height: - subgroup.boxplot.y(subgroup.boxplot.probs[1]) - - subgroup.boxplot.y(subgroup.boxplot.probs[3]) - }) - .style('fill', subgroup.boxplot.boxColor); - } - - function drawHorizontalLines(subgroup) { - var iS = [0, 2, 4]; - var iSclass = ['', 'median', '']; - var iSColor = [ - subgroup.boxplot.boxColor, - subgroup.boxplot.boxInsideColor, - subgroup.boxplot.boxColor - ]; - for (var i = 0; i < iS.length; i++) { - subgroup.boxplot.container - .append('line') - .attr({ - class: 'boxplot ' + iSclass[i], - x1: subgroup.boxplot.left, - x2: subgroup.boxplot.right, - y1: subgroup.boxplot.y(subgroup.boxplot.probs[iS[i]]), - y2: subgroup.boxplot.y(subgroup.boxplot.probs[iS[i]]) - }) - .style({ - fill: iSColor[i], - stroke: iSColor[i] - }); - } - } - - function drawVerticalLines(subgroup) { - var iS = [[0, 1], [3, 4]]; - for (var i = 0; i < iS.length; i++) { - subgroup.boxplot.container - .append('line') - .attr({ - class: 'boxplot', - x1: subgroup.boxplot.x(0.5), - x2: subgroup.boxplot.x(0.5), - y1: subgroup.boxplot.y(subgroup.boxplot.probs[iS[i][0]]), - y2: subgroup.boxplot.y(subgroup.boxplot.probs[iS[i][1]]) - }) - .style('stroke', subgroup.boxplot.boxColor); - } - } - - function drawOuterCircle(subgroup) { - subgroup.boxplot.container - .append('circle') - .attr({ - class: 'boxplot mean', - cx: subgroup.boxplot.x(0.5), - cy: subgroup.boxplot.y(subgroup.results.mean), - r: Math.min(subgroup.boxplot.x(subgroup.boxplot.boxPlotWidth / 3), 10) - }) - .style({ - fill: subgroup.boxplot.boxInsideColor, - stroke: subgroup.boxplot.boxColor - }); - } - - function drawInnerCircle(subgroup) { - subgroup.boxplot.container - .append('circle') - .attr({ - class: 'boxplot mean', - cx: subgroup.boxplot.x(0.5), - cy: subgroup.boxplot.y(subgroup.results.mean), - r: Math.min(subgroup.boxplot.x(subgroup.boxplot.boxPlotWidth / 6), 5) - }) - .style({ - fill: subgroup.boxplot.boxColor, - stroke: 'none' - }); - } - - function addBoxPlot(subgroup) { - //Attach needed stuff to subgroup object. - subgroup.boxplot = { - boxPlotWidth: 0.75 / this.colorScale.domain().length, - boxColor: this.colorScale(subgroup.key), - boxInsideColor: '#eee', - probs: ['q5', 'q25', 'median', 'q75', 'q95'].map(function(prob) { - return subgroup.results[prob]; - }) - }; - - //Draw box plot. - defineScales.call(this, subgroup); - addContainer.call(this, subgroup); - drawBox.call(this, subgroup); - drawHorizontalLines.call(this, subgroup); - drawVerticalLines.call(this, subgroup); - drawOuterCircle.call(this, subgroup); - drawInnerCircle.call(this, subgroup); - } - - function defineData(subgroup) { - //Define histogram data. - subgroup.violinPlot = { - histogram: d3.layout - .histogram() - .bins(10) - .frequency(0) - }; - (subgroup.violinPlot.data = subgroup.violinPlot.histogram(subgroup.results.values)), - subgroup.violinPlot.data.unshift({ - x: subgroup.results.min, - dx: 0, - y: subgroup.violinPlot.data[0].y - }); - subgroup.violinPlot.data.push({ - x: subgroup.results.max, - dx: 0, - y: subgroup.violinPlot.data[subgroup.violinPlot.data.length - 1].y - }); - } - - function defineScales$1(subgroup) { - subgroup.violinPlot.width = this.x.rangeBand(); - subgroup.violinPlot.x = - this.config.y.type === 'linear' - ? d3.scale - .linear() - .domain(this.y.domain()) - .range([this.plot_height, 0]) - : d3.scale - .log() - .domain(this.y.domain()) - .range([this.plot_height, 0]); - subgroup.violinPlot.y = d3.scale - .linear() - .domain([ - 0, - Math.max( - 1 - 1 / subgroup.group.x.nGroups, - d3.max(subgroup.violinPlot.data, function(d) { - return d.y; - }) - ) - ]) - .range([subgroup.violinPlot.width / 2, 0]); - } - - function addContainer$1(subgroup) { - //Define violin shapes. - subgroup.violinPlot.area = d3.svg - .area() - .interpolate('basis') - .x(function(d) { - return subgroup.violinPlot.x(d.x + d.dx / 2); - }) - .y0(subgroup.violinPlot.width / 2) - .y1(function(d) { - return subgroup.violinPlot.y(d.y); - }); - subgroup.violinPlot.line = d3.svg - .line() - .interpolate('basis') - .x(function(d) { - return subgroup.violinPlot.x(d.x + d.dx / 2); - }) - .y(function(d) { - return subgroup.violinPlot.y(d.y); - }); - subgroup.violinPlot.container = subgroup.svg - .append('g') - .attr('class', 'violinplot') - .attr('clip-path', 'url(#' + this.id + ')'); - } - - function drawLeftSide(subgroup) { - subgroup.violinPlot.gMinus = subgroup.violinPlot.container - .append('g') - .attr('transform', 'rotate(90,0,0) scale(1,-1)'); - subgroup.violinPlot.gMinus - .append('path') - .datum(subgroup.violinPlot.data) - .attr({ - class: 'area', - d: subgroup.violinPlot.area, - fill: this.colorScale(subgroup.key), - 'fill-opacity': 0.75 - }); - subgroup.violinPlot.gMinus - .append('path') - .datum(subgroup.violinPlot.data) - .attr({ - class: 'violin', - d: subgroup.violinPlot.line, - stroke: this.colorScale(subgroup.key), - fill: 'none' - }); - } - - function drawRightSide(subgroup) { - subgroup.violinPlot.gPlus = subgroup.violinPlot.container - .append('g') - .attr('transform', 'rotate(90,0,0) translate(0,-' + subgroup.violinPlot.width + ')'); - subgroup.violinPlot.gPlus - .append('path') - .datum(subgroup.violinPlot.data) - .attr({ - class: 'area', - d: subgroup.violinPlot.area, - fill: this.colorScale(subgroup.key), - 'fill-opacity': 0.75 - }); - subgroup.violinPlot.gPlus - .append('path') - .datum(subgroup.violinPlot.data) - .attr({ - class: 'violin', - d: subgroup.violinPlot.line, - stroke: this.colorScale(subgroup.key), - fill: 'none' - }); - } - - function addViolinPlot(subgroup) { - defineData.call(this, subgroup); - defineScales$1.call(this, subgroup); - addContainer$1.call(this, subgroup); - drawLeftSide.call(this, subgroup); - drawRightSide.call(this, subgroup); - } - - function addSummaryStatistics(subgroup) { - var format0 = d3.format('.' + (this.config.y.precision + 0) + 'f'); - var format1 = d3.format('.' + (this.config.y.precision + 1) + 'f'); - var format2 = d3.format('.' + (this.config.y.precision + 2) + 'f'); - subgroup.svg - .selectAll('g') - .append('title') - .html(function(d) { - return ( - subgroup.key + - ' at ' + - subgroup.group.x.key + - ':\n    N = ' + - subgroup.results.n + - '\n    Min = ' + - format0(subgroup.results.min) + - '\n    5th % = ' + - format1(subgroup.results.q5) + - '\n    Q1 = ' + - format1(subgroup.results.q25) + - '\n    Median = ' + - format1(subgroup.results.median) + - '\n    Q3 = ' + - format1(subgroup.results.q75) + - '\n    95th % = ' + - format1(subgroup.results.q95) + - '\n    Max = ' + - format0(subgroup.results.max) + - '\n    Mean = ' + - format1(subgroup.results.mean) + - '\n    StDev = ' + - format2(subgroup.results.deviation) - ); - }); - } - - function drawPlots() { - var _this = this; - - this.nested_measure_data - .filter(function(visit) { - return _this.x_dom.indexOf(visit.key) > -1; - }) - .forEach(function(visit) { - // iterate over groups - //Sort [ config.color_by ] groups. - visit.values = visit.values.sort(function(a, b) { - return _this.colorScale.domain().indexOf(a.key) < - _this.colorScale.domain().indexOf(b.key) - ? -1 - : 1; - }); - - //Define group object. - var groupObject = { - x: { - key: visit.key, // x-axis value - nGroups: _this.colorScale.domain().length, // number of groups at x-axis value - width: _this.x.rangeBand() // width of x-axis value - }, - subgroups: [] - }; - groupObject.x.start = -(groupObject.x.nGroups / 2) + 0.5; - groupObject.distance = groupObject.x.width / groupObject.x.nGroups; - - visit.values.forEach(function(group, i) { - //Iterate over visits. - var subgroup = { - group: groupObject, - key: group.key, - offset: (groupObject.x.start + i) * groupObject.distance, - results: group.values - }; - subgroup.svg = _this.svg - .insert('g', '.point-supergroup') - .attr({ - class: 'boxplot-wrap overlay-item', - transform: - 'translate(' + - (_this.x(groupObject.x.key) + subgroup.offset) + - ',0)' - }) - .datum({ values: subgroup.results }); - groupObject.subgroups.push(subgroup); - - if (_this.config.boxplots) addBoxPlot.call(_this, subgroup); - if (_this.config.violins) addViolinPlot.call(_this, subgroup); - addSummaryStatistics.call(_this, subgroup); - - //Offset outliers. - _this.marks - .filter(function(mark) { - return mark.type === 'circle'; - }) - .forEach(function(mark) { - mark.groups - .filter(function(d) { - return d.visit === visit.key && d.group === group.key; - }) - .attr('transform', 'translate(' + subgroup.offset + ',0)'); - }); - }); - }); - } - - function addMouseoverToOutliers() { - var _this = this; - - this.marks - .filter(function(mark) { - return mark.type === 'circle'; - }) - .forEach(function(mark) { - mark.groups - .each(function(d, i) { - d3.select(this).classed('hidden-' + d.hidden + ' ' + d.id, true); - }) - .on('mouseover', function(d) { - _this.svg.select('.hidden-true.' + d.id + ' circle').attr({ - 'fill-opacity': 1, - 'stroke-opacity': 1 - }); - }) - .on('mouseout', function(d) { - _this.svg.select('.hidden-true.' + d.id + ' circle').attr({ - 'fill-opacity': 0, - 'stroke-opacity': 0 - }); - }); - }); - } - - function removeLegend() { - if (this.config.color_by === 'srot_none') this.wrap.select('.legend').remove(); - } - - function onResize() { - editXAxisTicks.call(this); - editYAxisTicks.call(this); - clearCanvas$1.call(this); - drawPlots.call(this); - addMouseoverToOutliers.call(this); - removeLegend.call(this); - } - - function onDestroy() {} - - var callbacks = { - onInit: onInit, - onLayout: onLayout, - onPreprocess: onPreprocess, - onDatatransform: onDatatransform, - onDraw: onDraw, - onResize: onResize, - onDestroy: onDestroy - }; - - function safetyResultsOverTime(element, settings) { - var mergedSettings = merge(configuration.defaultSettings, settings); //Merge user settings onto default settings. - var syncedSettings = configuration.syncSettings(mergedSettings); //Sync properties within merged settings, e.g. data mappings. - var syncedControlInputs = configuration.syncControlInputs( - configuration.controlInputs(), - syncedSettings - ); //Sync merged settings with controls. - - //Define controls. - var controls = webcharts.createControls(element, { - location: 'top', - inputs: syncedControlInputs - }); - - //Define chart. - var chart = webcharts.createChart(element, syncedSettings, controls); - - //Attach callbacks to chart. - for (var callback in callbacks) { - chart.on(callback.substring(2).toLowerCase(), callbacks[callback]); - } - return chart; - } - - return safetyResultsOverTime; -}); diff --git a/inst/htmlwidgets/lib/safety-shift-plot-2.1.3/safetyShiftPlot.js b/inst/htmlwidgets/lib/safety-shift-plot-2.1.3/safetyShiftPlot.js deleted file mode 100644 index 637660ac..00000000 --- a/inst/htmlwidgets/lib/safety-shift-plot-2.1.3/safetyShiftPlot.js +++ /dev/null @@ -1,1399 +0,0 @@ -(function(global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' - ? (module.exports = factory(require('d3'), require('webcharts'))) - : typeof define === 'function' && define.amd - ? define(['d3', 'webcharts'], factory) - : (global.safetyShiftPlot = factory(global.d3, global.webCharts)); -})(this, function(d3, webcharts) { - 'use strict'; - - if (typeof Object.assign != 'function') { - // Must be writable: true, enumerable: false, configurable: true - Object.defineProperty(Object, 'assign', { - value: function assign(target, varArgs) { - if (target == null) { - // TypeError if undefined or null - throw new TypeError('Cannot convert undefined or null to object'); - } - - var to = Object(target); - - for (var index = 1; index < arguments.length; index++) { - var nextSource = arguments[index]; - - if (nextSource != null) { - // Skip over if undefined or null - for (var nextKey in nextSource) { - // Avoid bugs when hasOwnProperty is shadowed - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - - return to; - }, - writable: true, - configurable: true - }); - } - - if (!Array.prototype.find) { - Object.defineProperty(Array.prototype, 'find', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, 'length')). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return kValue. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return kValue; - } - // e. Increase k by 1. - k++; - } - - // 7. Return undefined. - return undefined; - } - }); - } - - if (!Array.prototype.findIndex) { - Object.defineProperty(Array.prototype, 'findIndex', { - value: function value(predicate) { - // 1. Let O be ? ToObject(this value). - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let len be ? ToLength(? Get(O, "length")). - var len = o.length >>> 0; - - // 3. If IsCallable(predicate) is false, throw a TypeError exception. - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - - // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. - var thisArg = arguments[1]; - - // 5. Let k be 0. - var k = 0; - - // 6. Repeat, while k < len - while (k < len) { - // a. Let Pk be ! ToString(k). - // b. Let kValue be ? Get(O, Pk). - // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). - // d. If testResult is true, return k. - var kValue = o[k]; - if (predicate.call(thisArg, kValue, k, o)) { - return k; - } - // e. Increase k by 1. - k++; - } - - // 7. Return -1. - return -1; - } - }); - } - - var rendererSpecificSettings = { - id_col: 'USUBJID', - time_col: 'VISITN', - visit_col: 'VISIT', - visit_order_col: 'VISITNUM', - measure_col: 'TEST', - value_col: 'STRESN', - start_value: null, - x_params: { visits: null, stat: 'mean' }, - y_params: { visits: null, stat: 'mean' }, - filters: null - }; - - var webchartsSettings = { - x: { - column: 'shiftx', - type: 'linear', - label: 'Baseline Value', - format: '0.2f' - }, - y: { - column: 'shifty', - type: 'linear', - label: 'Comparison Value', - behavior: 'flex', - format: '0.2f' - }, - marks: [ - { - type: 'circle', - per: ['key'], - radius: 4, - attributes: { - 'stroke-width': 0.5, - 'fill-opacity': 0.8 - }, - tooltip: - 'Subject ID: [key]\nBaseline: [shiftx]\nComparison: [shifty]\nChange: [chg]\nPercent Change: [pchg]' - } - ], - gridlines: 'xy', - resizable: false, - margin: { right: 25, top: 25 }, - aspect: 1 - }; - - var defaultSettings = Object.assign({}, rendererSpecificSettings, webchartsSettings); - - // Replicate settings in multiple places in the settings object - function syncSettings(settings) { - if (!(settings.filters instanceof Array)) - settings.filters = typeof settings.filters === 'string' ? [settings.filters] : []; - - settings.measure = settings.start_value; - return settings; - } - - // Default Control objects - var controlInputs = [ - { type: 'dropdown', values: [], label: 'Measure', option: 'measure', require: true }, - { - type: 'dropdown', - values: [], - label: 'Baseline visit(s)', - option: 'x_params_visits', - require: true, - multiple: true - }, - { - type: 'dropdown', - values: [], - label: 'Comparison visit(s)', - option: 'y_params_visits', - require: true, - multiple: true - } - ]; - - // Map values from settings to control inputs - function syncControlInputs(controlInputs, settings) { - //Define filter objects. - if (Array.isArray(settings.filters) && settings.filters.length) - settings.filters = settings.filters.map(function(filter) { - var filterObject = { - value_col: filter.value_col || filter - }; - filterObject.label = filter.label || filterObject.value_col; - filterObject.type = 'subsetter'; - - if (filter instanceof Object) Object.assign(filterObject, filter); - - return filterObject; - }); - else delete settings.filters; - - return controlInputs; - } - - var listingSettings = { - cols: ['key', 'shiftx', 'shifty', 'chg', 'pchg'], - headers: ['Participant ID', 'Baseline', 'Comparison', 'Change', 'Percent Change'], - searchable: false, - sortable: true, - pagination: false, - exportable: true - }; - - var _typeof = - typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' - ? function(obj) { - return typeof obj; - } - : function(obj) { - return obj && - typeof Symbol === 'function' && - obj.constructor === Symbol && - obj !== Symbol.prototype - ? 'symbol' - : typeof obj; - }; - - function clone(obj) { - var copy; - - //boolean, number, string, null, undefined - if ('object' != (typeof obj === 'undefined' ? 'undefined' : _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.'); - } - - var isMergeableObject = function isMergeableObject(value) { - return isNonNullObject(value) && !isSpecial(value); - }; - - function isNonNullObject(value) { - return ( - !!value && (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' - ); - } - - function isSpecial(value) { - var stringValue = Object.prototype.toString.call(value); - - return ( - stringValue === '[object RegExp]' || - stringValue === '[object Date]' || - isReactElement(value) - ); - } - - // see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25 - var canUseSymbol = typeof Symbol === 'function' && Symbol.for; - var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7; - - function isReactElement(value) { - return value.$$typeof === REACT_ELEMENT_TYPE; - } - - function emptyTarget(val) { - return Array.isArray(val) ? [] : {}; - } - - function cloneUnlessOtherwiseSpecified(value, options) { - return options.clone !== false && options.isMergeableObject(value) - ? deepmerge(emptyTarget(value), value, options) - : value; - } - - function defaultArrayMerge(target, source, options) { - return target.concat(source).map(function(element) { - return cloneUnlessOtherwiseSpecified(element, options); - }); - } - - function mergeObject(target, source, options) { - var destination = {}; - - if (options.isMergeableObject(target)) { - Object.keys(target).forEach(function(key) { - destination[key] = cloneUnlessOtherwiseSpecified(target[key], options); - }); - } - - Object.keys(source).forEach(function(key) { - if (!options.isMergeableObject(source[key]) || !target[key]) { - destination[key] = cloneUnlessOtherwiseSpecified(source[key], options); - } else { - destination[key] = deepmerge(target[key], source[key], options); - } - }); - - return destination; - } - - function deepmerge(target, source, options) { - options = options || {}; - - options.arrayMerge = options.arrayMerge || defaultArrayMerge; - - options.isMergeableObject = options.isMergeableObject || isMergeableObject; - - var sourceIsArray = Array.isArray(source); - - var targetIsArray = Array.isArray(target); - - var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray; - - if (!sourceAndTargetTypesMatch) { - return cloneUnlessOtherwiseSpecified(source, options); - } else if (sourceIsArray) { - return options.arrayMerge(target, source, options); - } else { - return mergeObject(target, source, options); - } - } - - deepmerge.all = function deepmergeAll(array, options) { - if (!Array.isArray(array)) { - throw new Error('first argument should be an array'); - } - - return array.reduce(function(prev, next) { - return deepmerge(prev, next, options); - }, {}); - }; - - var deepmerge_1 = deepmerge; - - function defineLayout(element) { - var container = d3.select(element); - container - .append('div') - .classed('ssp-component', true) - .attr('id', 'ssp-controls'); - container - .append('div') - .classed('ssp-component', true) - .attr('id', 'ssp-chart'); - container - .append('div') - .classed('ssp-component', true) - .attr('id', 'ssp-listing'); - } - - function defineStyles() { - var styles = [ - '#safety-shift-plot {' + ' width: 100%;' + ' display: inline-block;' + '}', - '.ssp-component {' + - ' margin: 0;' + - ' border: none;' + - ' padding: 0;' + - ' display: inline-block;' + - '}', - - //controls - '#ssp-controls {' + ' width: 25%;' + ' float: left;' + '}', - '#ssp-controls .control-group {' + - ' width: 98%;' + - ' margin: 0 2% 5px 0;' + - ' padding: 0;' + - '}', - '#ssp-controls .control-group > * {' + ' display: inline-block;' + '}', - '#ssp-controls .changer {' + ' float: right;' + ' width: 50%;' + '}', - '#ssp-controls .wc-control-label {' + - ' text-align: right;' + - ' width: 48%;' + - '}', - '#ssp-controls .annote {' + ' width: 98%;' + ' text-align: right;' + '}', - - //chart - '#ssp-chart {' + ' width: 36%;' + ' margin: 0 2%;' + '}', - - //listing - '#ssp-listing {' + ' width: 35%;' + ' float: right;' + '}', - '#ssp-listing .wc-table table {' + ' width: 100%;' + ' display: table;' + '}', - '#ssp-listing .wc-table th:not(:first-child),' + - '#ssp-listing .wc-table td:not(:first-child) {' + - ' text-align: right;' + - '}' - ]; - var style = document.createElement('style'); - style.type = 'text/css'; - style.innerHTML = styles.join('\n'); - document.getElementsByTagName('head')[0].appendChild(style); - } - - function cleanData() { - var _this = this; - - //Remove missing and non-numeric data. - var preclean = this.raw_data; - var clean = this.raw_data.filter(function(d) { - return /^-?[0-9.]+$/.test(d[_this.config.value_col]); - }); - var nPreclean = preclean.length; - var nClean = clean.length; - var nRemoved = nPreclean - nClean; - - //Warn user of removed records. - if (nRemoved > 0) - console.warn( - nRemoved + - ' missing or non-numeric result' + - (nRemoved > 1 ? 's have' : ' has') + - ' been removed.' - ); - - //Preserve cleaned data. - this.initial_data = clean; - } - - function addVariables() { - var _this = this; - - this.initial_data.forEach(function(d) { - d[_this.config.measure_col] = d[_this.config.measure_col].trim(); - }); - } - - function checkFilters() { - var _this = this; - - if (this.config.filters) - this.config.filters = this.config.filters.filter(function(filter) { - var variableExists = _this.raw_data[0].hasOwnProperty(filter.value_col); - var nLevels = d3 - .set( - _this.raw_data.map(function(d) { - return d[filter.value_col]; - }) - ) - .values().length; - - if (!variableExists) - console.warn( - ' The [ ' + - filter.label + - ' ] filter has been removed because the variable does not exist.' - ); - else if (nLevels < 2) - console.warn( - 'The [ ' + - filter.label + - ' ] filter has been removed because the variable has only one level.' - ); - - return variableExists && nLevels > 1; - }); - } - - function getMeasures() { - var _this = this; - - this.measures = d3 - .set( - this.initial_data.map(function(d) { - return d[_this.config.measure_col]; - }) - ) - .values() - .sort(); - } - - function getVisits() { - var _this = this; - - if ( - this.config.visit_order_col && - this.initial_data[0].hasOwnProperty(this.config.visit_order_col) - ) - this.visits = d3 - .set( - this.initial_data.map(function(d) { - return d[_this.config.visit_col] + '||' + d[_this.config.visit_order_col]; - }) - ) - .values() - .sort(function(a, b) { - var aSplit = a.split('||'); - var aVisit = aSplit[0]; - var aOrder = aSplit[1]; - var bSplit = b.split('||'); - var bVisit = bSplit[0]; - var bOrder = bSplit[1]; - var diff = aOrder - bOrder; - return diff - ? diff - : aOrder < bOrder - ? -1 - : aOrder > bOrder - ? 1 - : aVisit < bVisit - ? -1 - : 1; - }) - .map(function(visit) { - return visit.split('||')[0]; - }); - else - this.visits = d3 - .set( - this.initial_data.map(function(d) { - return d[_this.config.visit_col]; - }) - ) - .values() - .sort(); - } - - function updateControlInputs() { - this.controls.config.inputs.find(function(input) { - return input.option === 'measure'; - }).values = this.measures; - this.controls.config.inputs.find(function(input) { - return input.option === 'x_params_visits'; - }).values = this.visits; - this.controls.config.inputs.find(function(input) { - return input.option === 'y_params_visits'; - }).values = this.visits; - } - - function preprocessData(rawData) { - var config = this.config; - - var nested = d3 - .nest() - .key(function(d) { - return d[config.id_col]; - }) - .key(function(d) { - return d[config.visit_col]; - }) - .key(function(d) { - return d[config.measure_col]; - }) - .rollup(function(r) { - var value = r[0][config.value_col]; - return { value: value, raw: r[0] }; - }) - .entries(rawData); - - function getMean(arr) { - return d3.sum(arr) / arr.length; - } - - function setVal(e, params) { - var visits = e.values.filter(function(f) { - return params.visits.indexOf(f.key) !== -1; - }); - var measures = visits.length - ? d3.merge( - visits.map(function(m) { - return m.values - .filter(function(f) { - return f.key === config.measure; - }) - .map(function(p) { - return +p.values.value; - }); - }) - ) - : []; - - var meas = null; - var stat = measures && measures.length > 1 ? params.stat : 'def'; - var something = { - mean: getMean(measures), - max: d3.max(measures), - min: d3.min(measures), - def: measures[0] - }; - meas = something[stat]; - return meas; - } - - function getXY(e) { - e.shiftx = +setVal(e, config.x_params); - e.shifty = +setVal(e, config.y_params); - e.chg = e.shifty - e.shiftx; - e.pchg = d3.format('%')(e.chg / e.shiftx); - } - - function getChange(e) { - e.shifty -= +e.shiftx; - } - - //flatten out other columns specified for details - function getOther(e) { - config.details.forEach(function(g) { - e[g.col] = e.values[0].values[0].values.raw[g.col]; - }); - } - - config.details = config.details && config.details.length ? config.details : []; - - if (config.color_by) { - var match = config.details.filter(function(f) { - return f.col === config.color_by; - }); - if (!match[0]) config.details.push({ col: config.color_by, label: config.color_by }); - } - - var test_data = nested; - test_data.forEach(getXY); - if (config.change) test_data.forEach(getChange); - if (config.details.length) test_data.forEach(getOther); - - return test_data; - } - - function initCustomEvents() { - var chart = this; - chart.participantsSelected = []; - chart.events.participantsSelected = new CustomEvent('participantsSelected'); - } - - function onInit() { - var _this = this; - - // 1. Remove invalid data. - cleanData.call(this); - - // 2. Add/edit variables. - addVariables.call(this); - - // 3a Check filters against data. - checkFilters.call(this); - - // 3b Get list of measures. - getMeasures.call(this); - - // 3c Get list of visits. - getVisits.call(this); - - // 4. Update control inputs. - updateControlInputs.call(this); - - //Set initial measure. - this.config.measure = this.config.measure || this.measures[0]; - - //Set baseline and comparison visits. - this.config.x_params.visits = this.config.x_params.visits || [this.visits[0]]; - this.config.y_params.visits = this.config.y_params.visits || this.visits.slice(1); - - //Filter raw data on initial measure and derive baseline/comparison data. - this.measureData = this.initial_data.filter(function(d) { - return d[_this.config.measure_col] === _this.config.measure; - }); - this.filteredData = this.measureData; // filtered data placeholder - this.raw_data = preprocessData.call(this, this.measureData); // preprocessed measure data - - //Define initial domains. - this.config.x.domain = d3.extent( - this.raw_data.map(function(d) { - return d.shiftx; - }) - ); - this.config.y.domain = d3.extent( - this.raw_data.map(function(d) { - return d.shifty; - }) - ); - - //initialize custom events - initCustomEvents.call(this); - } - - function custmoizeMeasureControl() { - var _this = this; - - var measureSelect = this.controls.wrap - .selectAll('.control-group') - .filter(function(f) { - return f.option === 'measure'; - }) - .select('select'); - measureSelect.on('change', function() { - _this.config.measure = measureSelect.select('option:checked').property('text'); - - //Redefine raw and preprocessed measure data, x-domain, and y-domain. - _this.measureData = _this.initial_data.filter(function(d) { - return d[_this.config.measure_col] === _this.config.measure; - }); - _this.raw_data = preprocessData.call(_this, _this.measureData); - _this.config.x.domain = d3.extent( - _this.raw_data.map(function(d) { - return d.shiftx; - }) - ); - _this.config.y.domain = d3.extent( - _this.raw_data.map(function(d) { - return d.shifty; - }) - ); - - //Redefine and preprocess filtered data and redraw chart. - if (_this.config.filters) { - _this.filteredData = _this.measureData.filter(function(d) { - var filtered = false; - _this.config.filters.forEach(function(filter) { - return (filtered = - filtered === false && filter.value !== 'All' - ? d[filter.value_col] !== filter.value - : filtered); - }); - return !filtered; - }); - var filteredPreprocessedData = preprocessData.call(_this, _this.filteredData); - _this.draw(filteredPreprocessedData); - } else { - _this.filteredData = _this.measureData; - _this.draw(_this.raw_data); - } - }); - } - - function customizeBaselineControl() { - var _this = this; - - var baselineSelect = this.controls.wrap - .selectAll('.control-group') - .filter(function(f) { - return f.option === 'x_params_visits'; - }) - .select('select'); - baselineSelect - .selectAll('option') - .filter(function(f) { - return _this.config.x_params.visits.indexOf(f) > -1; - }) - .attr('selected', 'selected'); - baselineSelect.on('change', function() { - _this.config.x_params.visits = baselineSelect.selectAll('option:checked').data(); - - //Redefine preprocessed measure data and x-domain. - _this.raw_data = preprocessData.call(_this, _this.measureData); - _this.config.x.domain = d3.extent( - _this.raw_data.map(function(d) { - return d.shiftx; - }) - ); - - //Preprocess filtered data and redraw chart. - if (_this.config.filters) { - var filteredPreprocessedData = preprocessData.call(_this, _this.filteredData); - _this.draw(filteredPreprocessedData); - } else _this.draw(_this.raw_data); - }); - } - - function customizeComparisonControl() { - var _this = this; - - var comparisonSelect = this.controls.wrap - .selectAll('.control-group') - .filter(function(f) { - return f.option === 'y_params_visits'; - }) - .select('select'); - comparisonSelect - .selectAll('option') - .filter(function(f) { - return _this.config.y_params.visits.indexOf(f) > -1; - }) - .attr('selected', 'selected'); - comparisonSelect.on('change', function() { - _this.config.y_params.visits = comparisonSelect.selectAll('option:checked').data(); - - //Redefine preprocessed measure data and y-domain. - _this.raw_data = preprocessData.call(_this, _this.measureData); - _this.config.y.domain = d3.extent( - _this.raw_data.map(function(d) { - return d.shifty; - }) - ); - - //Preprocess filtered data and redraw chart. - if (_this.config.filters) { - var filteredPreprocessedData = preprocessData.call(_this, _this.filteredData); - _this.draw(filteredPreprocessedData); - } else _this.draw(_this.raw_data); - }); - } - - function addFilters(chart) { - chart.config.filters.forEach(function(filter) { - //Capture distinct [filter.value_col] values. - filter.values = d3 - .set( - chart.initial_data.map(function(d) { - return d[filter.value_col]; - }) - ) - .values(); - filter.value = 'All'; - - //Attach filter to the DOM. - var controlGroup = chart.controls.wrap - .append('div') - .classed('control-group', true) - .datum(filter); - controlGroup - .append('span') - .classed('wc-control-label', true) - .text(filter.label); - var changer = controlGroup.append('select').classed('changer', true); - - //Attach distinct [filter.value_col] values as select options. - changer - .selectAll('option') - .data(['All'].concat(filter.values)) - .enter() - .append('option') - .text(function(d) { - return d; - }); - - //Define dropdown event listener. - changer.on('change', function(d) { - //Set [filter.value] to dropdown selection. - filter.value = changer.select('option:checked').property('text'); - - //Filter raw measure data on all filter selections. - chart.filteredData = chart.measureData.filter(function(di) { - var filtered = false; - chart.config.filters.forEach(function(dii) { - return (filtered = - filtered === false && dii.value !== 'All' - ? di[dii.value_col] !== dii.value - : filtered); - }); - return !filtered; - }); - - //Preprocess filtered data and redraw chart. - var preprocessedFilteredData = preprocessData.call(chart, chart.filteredData); - chart.draw(preprocessedFilteredData); - }); - }); - } - - function onLayout() { - //Add footnote element. - this.wrap - .insert('p', ':first-child') - .attr('class', 'record-note') - .style('text-align', 'center') - .style('font-weight', 'bold') - .text('Click and drag to select points.'); - - //Add header element in which to list visits at which measure is captured. - this.wrap.append('p', 'svg').attr('class', 'possible-visits'); - - //Designate chart container for brushing. - this.wrap.classed('brushable', true); - - //Customize measure, baseline, and comparison controls. - custmoizeMeasureControl.call(this); - customizeBaselineControl.call(this); - customizeComparisonControl.call(this); - - //Create custom filters. - if (this.config.filters) addFilters(this); - - //Add element for participant counts. - this.controls.wrap - .append('em') - .classed('annote', true) - .style('display', 'block'); - } - - function onPreprocess() {} - - function onDataTransform() {} - - /*------------------------------------------------------------------------------------------------\ - Annotate number of participants based on current filters, number of participants in all, and - the corresponding percentage. - - Inputs: - - chart - a webcharts chart object - id_unit - a text string to label the units in the annotation (default = 'participants') - selector - css selector for the annotation - \------------------------------------------------------------------------------------------------*/ - - function updateParticipantCount(chart, selector, id_unit) { - //count the number of unique ids in the data set - var totalObs = d3 - .set( - chart.initial_data.map(function(d) { - return d[chart.config.id_col]; - }) - ) - .values().length; - - //count the number of unique ids in the current chart and calculate the percentage - var currentObs = chart.filtered_data.filter(function(d) { - return ( - chart.x.domain()[0] <= d.shiftx && - d.shiftx <= chart.x.domain()[1] && - chart.y.domain()[0] <= d.shifty && - d.shifty <= chart.y.domain()[1] - ); - }).length; - - var percentage = d3.format('0.1%')(currentObs / totalObs); - - //clear the annotation - var annotation = d3.select(selector); - annotation.selectAll('*').remove(); - - //update the annotation - var units = id_unit ? ' ' + id_unit : ' participant(s)'; - annotation.text(currentObs + ' of ' + totalObs + units + ' shown (' + percentage + ')'); - } - - function reset() { - this.svg.selectAll('g.boxplot').remove(); - this.svg - .selectAll('g.point') - .classed('selected', false) - .select('circle') - .style('fill', this.config.colors[0]); - this.wrap - .select('.record-note') - .style('text-align', 'center') - .text('Click and drag to select points.'); - this.svg.select('line.identity').remove(); - this.listing.draw([]); - this.listing.wrap.style('display', 'none'); - } - - function onDraw() { - //Annotate selected and total number of participants. - updateParticipantCount(this, '.annote'); - - //Reset things. - reset.call(this); - } - - function drawBoxPlot( - svg, - results, - height, - width, - domain, - boxPlotWidth, - boxColor, - boxInsideColor, - fmt, - horizontal - ) { - //set default orientation to "horizontal" - var horizontal = horizontal == undefined ? true : horizontal; - - //make the results numeric and sort - var results = results - .map(function(d) { - return +d; - }) - .sort(d3.ascending); - - //set up scales - var y = d3.scale.linear().range([height, 0]); - - var x = d3.scale.linear().range([0, width]); - - if (horizontal) { - y.domain(domain); - } else { - x.domain(domain); - } - - 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]); - } - - var boxplot = svg - .append('g') - .attr('class', 'boxplot') - .datum({ values: results, probs: probs }); - - //draw rectangle from q1 to q3 - var box_x = horizontal ? x(0.5 - boxPlotWidth / 2) : x(probs[1]); - var box_width = horizontal - ? x(0.5 + boxPlotWidth / 2) - x(0.5 - boxPlotWidth / 2) - : x(probs[3]) - x(probs[1]); - var box_y = horizontal ? y(probs[3]) : y(0.5 + boxPlotWidth / 2); - var box_height = horizontal - ? -y(probs[3]) + y(probs[1]) - : y(0.5 - boxPlotWidth / 2) - y(0.5 + boxPlotWidth / 2); - - boxplot - .append('rect') - .attr('class', 'boxplot fill') - .attr('x', box_x) - .attr('width', box_width) - .attr('y', box_y) - .attr('height', box_height) - .style('fill', boxColor); - - //draw dividing lines at median, 95% and 5% - var iS = [0, 2, 4]; - var iSclass = ['', 'median', '']; - var iSColor = [boxColor, boxInsideColor, boxColor]; - for (var i = 0; i < iS.length; i++) { - boxplot - .append('line') - .attr('class', 'boxplot ' + iSclass[i]) - .attr('x1', horizontal ? x(0.5 - boxPlotWidth / 2) : x(probs[iS[i]])) - .attr('x2', horizontal ? x(0.5 + boxPlotWidth / 2) : x(probs[iS[i]])) - .attr('y1', horizontal ? y(probs[iS[i]]) : y(0.5 - boxPlotWidth / 2)) - .attr('y2', horizontal ? y(probs[iS[i]]) : y(0.5 + boxPlotWidth / 2)) - .style('fill', iSColor[i]) - .style('stroke', iSColor[i]); - } - - //draw lines from 5% to 25% and from 75% to 95% - var iS = [[0, 1], [3, 4]]; - for (var i = 0; i < iS.length; i++) { - boxplot - .append('line') - .attr('class', 'boxplot') - .attr('x1', horizontal ? x(0.5) : x(probs[iS[i][0]])) - .attr('x2', horizontal ? x(0.5) : x(probs[iS[i][1]])) - .attr('y1', horizontal ? y(probs[iS[i][0]]) : y(0.5)) - .attr('y2', horizontal ? y(probs[iS[i][1]]) : y(0.5)) - .style('stroke', boxColor); - } - - boxplot - .append('circle') - .attr('class', 'boxplot mean') - .attr('cx', horizontal ? x(0.5) : x(d3.mean(results))) - .attr('cy', horizontal ? y(d3.mean(results)) : y(0.5)) - .attr('r', horizontal ? x(boxPlotWidth / 3) : y(1 - boxPlotWidth / 3)) - .style('fill', boxInsideColor) - .style('stroke', boxColor); - - boxplot - .append('circle') - .attr('class', 'boxplot mean') - .attr('cx', horizontal ? x(0.5) : x(d3.mean(results))) - .attr('cy', horizontal ? y(d3.mean(results)) : y(0.5)) - .attr('r', horizontal ? x(boxPlotWidth / 6) : y(1 - boxPlotWidth / 6)) - .style('fill', boxColor) - .style('stroke', 'None'); - - var formatx = fmt ? d3.format(fmt) : d3.format('.2f'); - - boxplot - .selectAll('.boxplot') - .append('title') - .text(function(d) { - return ( - 'N = ' + - d.values.length + - '\n' + - 'Min = ' + - d3.min(d.values) + - '\n' + - '5th % = ' + - formatx(d3.quantile(d.values, 0.05)) + - '\n' + - 'Q1 = ' + - formatx(d3.quantile(d.values, 0.25)) + - '\n' + - 'Median = ' + - formatx(d3.median(d.values)) + - '\n' + - 'Q3 = ' + - formatx(d3.quantile(d.values, 0.75)) + - '\n' + - '95th % = ' + - formatx(d3.quantile(d.values, 0.95)) + - '\n' + - 'Max = ' + - d3.max(d.values) + - '\n' + - 'Mean = ' + - formatx(d3.mean(d.values)) + - '\n' + - 'StDev = ' + - formatx(d3.deviation(d.values)) - ); - }); - } - - function addBoxPlots() { - // Y-axis box plot - var yValues = this.current_data.map(function(d) { - return d.values.y; - }); - var ybox = this.svg.append('g').attr('class', 'yMargin'); - drawBoxPlot(ybox, yValues, this.plot_height, 1, this.y_dom, 10, '#bbb', 'white'); - ybox.select('g.boxplot').attr( - 'transform', - 'translate(' + (this.plot_width + this.config.margin.right / 2) + ',0)' - ); - - //X-axis box plot - var xValues = this.current_data.map(function(d) { - return d.values.x; - }); - var xbox = this.svg.append('g').attr('class', 'xMargin'); - drawBoxPlot( - xbox, //svg element - xValues, //values - 1, //height - this.plot_width, //width - this.x_dom, //domain - 10, //box plot width - '#bbb', //box color - 'white', //detail color - '0.2f', //format - false // horizontal? - ); - xbox.select('g.boxplot').attr( - 'transform', - 'translate(0,' + -(this.config.margin.top / 2) + ')' - ); - } - - function listVisits() { - var _this = this; - - var possibleVisits = d3 - .set( - this.initial_data - .filter(function(f) { - return f[_this.config.measure_col] === _this.config.measure; - }) - .map(function(d) { - return d[_this.config.visit_col]; - }) - ) - .values(); - possibleVisits.sort(function(a, b) { - return _this.visits.indexOf(a) - _this.visits.indexOf(b); - }); - - this.wrap - .select('.possible-visits') - .text( - this.config.measure + - ' is collected at these visits: ' + - possibleVisits.join(', ') + - '.' - ); - } - - function brushing() { - var _this = this; - - var chart = this; - var config = this.config; - var extent = chart.brush.extent(); - var points = this.svg.selectAll('g.point').classed('selected', false); - var decim = d3.format('.2f'); - - points.select('circle').attr('fill-opacity', 0); - - var selected_points = points - .filter(function(d) { - var cx = _this.x(+d.values.x); - var cy = _this.y(+d.values.y); - return ( - extent[0][0] <= cx && - cx <= extent[1][0] && - extent[0][1] <= cy && - cy <= extent[1][1] - ); - }) - .classed('selected', true) - .select('circle') - .attr('fill-opacity', this.config.marks[0].attributes['fill-opacity']); - - //redraw the table with the new data - - var selected_data = selected_points.data().map(function(m) { - return m.values.raw[0]; - }); - chart.participantsSelected = selected_data.map(function(m) { - return m.key; - }); - selected_data.forEach(function(d) { - d.shiftx = decim(d.shiftx); - d.shifty = decim(d.shifty); - d.chg = decim(d.chg); - }); - this.listing.draw(selected_data); - if (selected_data.length === 0) this.listing.wrap.style('display', 'none'); - else this.listing.wrap.style('display', 'block'); - - //footnote - this.wrap - .select('.record-note') - .style('text-align', 'right') - .text('Details of ' + selected_data.length + ' selected points:'); - if (chart.brush.empty()) { - this.wrap - .select('.record-note') - .style('text-align', 'center') - .text('Click and drag to select points.'); - points - .select('circle') - .attr('fill-opacity', this.config.marks[0].attributes['fill-opacity']); - } - } //brushed - - function brushEnd() { - this.events.participantsSelected.data = this.participantsSelected; - this.wrap.node().dispatchEvent(this.events.participantsSelected); - console.log("done brushin'"); - } - - function addBrush() { - var chart = this; - chart.brush = d3.svg - .brush() - .x(d3.scale.identity().domain(this.x.range())) - .y(d3.scale.identity().domain(this.y.range())) - .on('brush', brushing.bind(this)) - .on('brushend', brushEnd.bind(this)); - - this.svg.call(chart.brush); - - this.svg.select('rect.extent').attr({ - 'shape-rendering': 'crispEdges', - 'stroke-width': 1, - stroke: '#ccc', - 'fill-opacity': 0.1 - }); - } - - function addEqualityLine() { - var overallMin = d3.min([this.x.domain()[0], this.y.domain()[0]]); - var overallMax = d3.max([this.x.domain()[1], this.y.domain()[1]]); - - this.svg - .append('line') - .attr('x1', this.x(overallMin)) - .attr('x2', this.x(overallMax)) - .attr('y1', this.y(overallMin)) - .attr('y2', this.y(overallMax)) - .attr('stroke', 'black') - .attr('clip-path', 'URL(#1)') - .attr('class', 'identity'); - } - - function addTooltipsToAxisLabels() { - this.svg - .selectAll('.x.axis .axis-title') - .append('title') - .html( - 'Baseline visit(s):
    ' + - this.config.x_params.visits.join('
    ') - ); - this.svg - .selectAll('.y.axis .axis-title') - .append('title') - .html( - 'Comparison visit(s):
    ' + - this.config.y_params.visits.join('
    ') - ); - } - - function onResize() { - //Add univariate box plots to top and right margins. - addBoxPlots.call(this); - - //Annotate list of visits at which measure has results. - listVisits.call(this); - - //Expand the domains a bit so that points on the edge are brushable - this.x_dom[0] = this.x_dom[0] < 0 ? this.x_dom[0] * 1.01 : this.x_dom[0] * 0.99; - this.x_dom[1] = this.x_dom[1] < 0 ? this.x_dom[1] * 0.99 : this.x_dom[1] * 1.01; - this.y_dom[0] = this.y_dom[0] < 0 ? this.y_dom[0] * 1.01 : this.y_dom[0] * 0.99; - this.y_dom[1] = this.y_dom[1] < 0 ? this.y_dom[1] * 0.99 : this.y_dom[1] * 1.01; - - //Add brush functionality. - addBrush.call(this); - - //add an equality line - addEqualityLine.call(this); - - //Add tooltip to axis labels listing selected visits. - addTooltipsToAxisLabels.call(this); - } - - //polyfills - - function safetyShiftPlot(element, settings) { - //settings - if (settings.time_col && !settings.visit_col) settings.visit_col = settings.time_col; // prevent breaking backwards compatibility - var mergedSettings = deepmerge_1(defaultSettings, settings, { - arrayMerge: function arrayMerge(destination, source) { - return source; - } - }); - var syncedSettings = syncSettings(clone(mergedSettings)); - var syncedControlInputs = syncControlInputs(clone(controlInputs), syncedSettings); - - //layout and styles - defineLayout(element); - defineStyles(); - - //controls - var controls = webcharts.createControls( - document.querySelector(element).querySelector('#ssp-controls'), - { - location: 'top', - inputs: syncedControlInputs - } - ); - - //chart - var chart = webcharts.createChart( - document.querySelector(element).querySelector('#ssp-chart'), - syncedSettings, - controls - ); - chart.on('init', onInit); - chart.on('layout', onLayout); - chart.on('preprocess', onPreprocess); - chart.on('datatransform', onDataTransform); - chart.on('draw', onDraw); - chart.on('resize', onResize); - - //listing - var listing = webcharts.createTable( - document.querySelector(element).querySelector('#ssp-listing'), - listingSettings - ); - listing.init([]); - chart.listing = listing; - - return chart; - } - - return safetyShiftPlot; -}); From 8a4ca2c13cd348b85b2f7613aff35bf09a73aa15 Mon Sep 17 00:00:00 2001 From: jwildfire Date: Tue, 13 Apr 2021 09:20:47 -0400 Subject: [PATCH 2/2] sets name before reordering --- R/makeChartConfig.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/R/makeChartConfig.R b/R/makeChartConfig.R index aae70c18..5426ce4e 100644 --- a/R/makeChartConfig.R +++ b/R/makeChartConfig.R @@ -65,9 +65,8 @@ makeChartConfig <- function(dirs, sourceFiles=TRUE){ return(chart) }) - charts <- charts[order(purrr::map_dbl(charts, function(chart) chart$order))] - names(charts) <- yaml_files %>% file_path_sans_ext %>% basename + charts <- charts[order(purrr::map_dbl(charts, function(chart) chart$order))] message("Found ", length(yaml_files), " config files: ",paste(names(charts),collapse=", "))