UNPKG

safety-results-over-time

Version:

Chart showing population averages for lab measures, vital signs and other related measures during the course of a clinical trial.

1,390 lines (1,228 loc) 67.9 kB
(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 (lab