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
JavaScript
(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