diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..0339d5d
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["env"]
+}
diff --git a/README.md b/README.md
index 1a6f3c8..8311b0d 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Saftey Outlier Explorer
+# Safety Outlier Explorer
![alt tag](https://user-images.githubusercontent.com/31038805/32173925-d40b050a-bd56-11e7-8750-1bc631296376.gif)
## Overview
diff --git a/build/safetyOutlierExplorer.js b/build/safetyOutlierExplorer.js
deleted file mode 100644
index f45a861..0000000
--- a/build/safetyOutlierExplorer.js
+++ /dev/null
@@ -1,2025 +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.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;
- }
- });
- }
-
- // 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);
- }
- });
- };
-
- var rendererSpecificSettings = {
- id_col: 'USUBJID',
- 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
- }
- ],
- measure_col: 'TEST',
- unit_col: 'STRESU',
- value_col: 'STRESN',
- normal_col_low: 'STNRLO',
- normal_col_high: 'STNRHI',
- start_value: null,
- filters: null,
- custom_marks: null,
- details: [
- { value_col: 'AGE', label: 'Age' },
- { value_col: 'SEX', label: 'Sex' },
- { value_col: 'RACE', label: 'Race' }
- ],
- tooltip_cols: null,
- multiples_sizing: {
- width: 300,
- height: 100
- },
- 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
- 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
- }
- };
-
- var webchartsSettings = {
- 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',
- format: '0.2f'
- },
- marks: [
- {
- per: null, //set in syncSettings()
- type: 'line',
- attributes: {
- 'clip-path': 'url(#1)'
- },
- tooltip: null //set in syncSettings()
- },
- {
- per: null, //set in syncSettings()
- type: 'circle',
- attributes: {
- 'clip-path': 'url(#1)'
- },
- tooltip: null //set in syncSettings()
- }
- ],
- resizable: true,
- margin: { top: 5, bottom: 5, right: 20 }, //create space for box plot
- aspect: 3
- };
-
- var defaultSettings = Object.assign({}, rendererSpecificSettings, webchartsSettings);
-
- // Replicate settings in multiple places in the settings object
- function syncSettings(settings) {
- var time_col = settings.time_cols[0];
-
- //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 =
- 'ID = [' +
- settings.id_col +
- ']\n[' +
- settings.measure_col +
- '] = [' +
- settings.value_col +
- '] [' +
- settings.unit_col +
- ']\n' +
- settings.x.column +
- ' = [' +
- 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 (settings.custom_marks)
- settings.custom_marks.forEach(function(mark) {
- return 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;
- }
-
- // Default Control objects
- var controlInputs = [
- {
- type: 'subsetter',
- value_col: 'measure_unit', // 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'
- }
- ];
-
- // Map values from settings to control inputs
- 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;
- }
-
- function countParticipants() {
- var _this = this;
-
- this.populationCount = d3$1
- .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;
- 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.'
- );
- this.initial_data = clean;
- this.raw_data = clean;
- }
-
- 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) {
- //Append units to measure.
- d.measure_unit = d[_this.config.measure_col];
- if (
- _this.config.unit_col &&
- d.hasOwnProperty(_this.config.unit_col) &&
- d[_this.config.unit_col] !== ''
- )
- d.measure_unit = d.measure_unit + ' (' + d[_this.config.unit_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 captureMeasures() {
- this.measures = d3$1
- .set(
- this.initial_data.map(function(d) {
- return d.measure_unit;
- })
- )
- .values()
- .sort();
- }
-
- function defineVisitOrder() {
- 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;
- })
- );
- } else
- //Otherwise use data-driven visit order.
- time_settings.order = visitOrder;
-
- //Define domain.
- time_settings.domain = time_settings.order;
- });
- }
-
- function updateControlInputs() {
- //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 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$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 setInitialMeasure() {
- this.measure = {};
- this.controls.config.inputs.find(function(input) {
- return input.label === 'Measure';
- }).start =
- this.config.start_value || this.measures[0];
- }
-
- function addIDOrdering() {
- 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 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 Capture unique set of measures.
- captureMeasures.call(this);
-
- // 3c Define ordered x-axis domain with visit order variable.
- defineVisitOrder.call(this);
-
- // 3d Remove invalid control inputs.
- updateControlInputs.call(this);
-
- // 3e Remove filters for nonexistent or single-level variables.
- checkFilters.call(this);
-
- // 3f Choose the start value for the Test filter
- setInitialMeasure.call(this);
-
- // 3g Capture unique set of IDs and apply an ordering.
- addIDOrdering.call(this);
- }
-
- function identifyControls() {
- var controlGroups = this.controls.wrap.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.controls.wrap
- .selectAll('.control-group')
- .filter(function(f) {
- return f.option === 'y.domain[0]';
- })
- .select('input')
- .property('value', _this.config.y.domain[0]);
-
- _this.controls.wrap
- .selectAll('.control-group')
- .filter(function(f) {
- return f.option === 'y.domain[1]';
- })
- .select('input')
- .property('value', _this.config.y.domain[1]);
-
- _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.controls.wrap
- .append('div')
- .attr('id', 'participant-count')
- .style('font-style', 'italic');
- }
-
- function addSmallMultiplesContainer() {
- this.multiples = {
- container: this.wrap.append('div').classed('multiples', true),
- id: null
- };
- }
-
- function onLayout() {
- // Distinguish controls to insert y-axis reset button in the correct position.
- identifyControls.call(this);
-
- //Label x-axis options.
- labelXaxisOptions.call(this);
-
- //Add a button to reset the y-domain
- addYdomainResetButton.call(this);
-
- //Group related controls visually.
- groupControls.call(this);
-
- //Hide normal range input controls depending on the normal range method.
- hideNormalRangeInputs.call(this);
-
- //Add participant count container.
- addParticipantCountContainer.call(this);
-
- //Add container for small multiples.
- 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 === 'measure_unit';
- })
- .select('option:checked')
- .text();
- }
-
- function defineMeasureData() {
- var _this = this;
-
- this.measure.data = this.initial_data.filter(function(d) {
- return d.measure_unit === _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.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() {
- //Define y-domain.
- 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])
- // 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 updateYaxisLimitControls() {
- //Update y-axis limit controls.
- this.controls.wrap
- .selectAll('.control-group')
- .filter(function(f) {
- return f.option === 'y.domain[0]';
- })
- .select('input')
- .property('value', this.config.y.domain[0])
- .style('box-shadow', 'none');
-
- this.controls.wrap
- .selectAll('.control-group')
- .filter(function(f) {
- return f.option === 'y.domain[1]';
- })
- .select('input')
- .property('value', this.config.y.domain[1])
- .style('box-shadow', 'none');
- }
-
- 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 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() {}
-
- // Takes a webcharts object creates a text annotation giving the
-
- function updateParticipantCount(chart, selector, id_unit) {
- //count the number of unique ids in the current chart and calculate the percentage
- var currentObs = d3$1
- .set(
- chart.filtered_data.map(function(d) {
- return d[chart.config.id_col];
- })
- )
- .values().length;
- var percentage = d3$1.format('0.1%')(currentObs / chart.populationCount);
-
- //clear the annotation
- var annotation = d3$1.select(selector);
- d3$1
- .select(selector)
- .selectAll('*')
- .remove();
-
- //update the annotation
- var units = id_unit ? ' ' + id_unit : ' participant(s)';
- annotation.text(
- '\n' +
- currentObs +
- ' of ' +
- chart.populationCount +
- units +
- ' shown (' +
- percentage +
- ')'
- );
- }
-
- function resetChart() {
- this.svg.selectAll('.line,.point').remove();
- //delete this.hovered_id;
- //delete this.selected_id;
- //if (this.multiples.chart)
- // this.multiples.chart.destroy();
- }
-
- function updateBottomMargin() {
- this.config.margin.bottom = this.config.x.vertical_space;
- }
-
- function onDraw() {
- //Annotate participant count.
- updateParticipantCount(this, '#participant-count');
-
- //Clear current multiples.
- resetChart.call(this);
-
- //Update bottom margin for tick label rotation.
- updateBottomMargin.call(this);
- }
-
- function attachMarks() {
- var _this = this;
-
- this.marks.forEach(function(mark) {
- var type = mark.type === 'circle' ? 'point' : mark.type;
- _this[type + 's'] = mark.groups;
- });
- }
-
- 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', this.config.line_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: this.config.point_attributes.radius * 1.5,
- stroke: 'black',
- 'stroke-width': this.config.point_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 orderPoints() {
- var _this = this;
-
- this.marks
- .find(function(mark) {
- return mark.type === 'circle';
- })
- .groups.each(function(d, i) {
- d.order = _this.IDOrder.find(function(di) {
- return d.key.indexOf(di.ID) === 0;
- }).order;
- });
- }
-
- function clearHovered() {
- this.lines
- .filter(function() {
- return !d3$1.select(this).classed('selected');
- })
- .select('path')
- .attr(this.config.line_attributes);
- this.points
- .filter(function() {
- return !d3$1.select(this).classed('selected');
- })
- .select('circle')
- .attr(this.config.point_attributes)
- .attr(
- 'r',
- this.config.marks.find(function(mark) {
- return mark.type === 'circle';
- }).radius
- );
- delete this.hovered_id;
- }
-
- function clearSelected() {
- var _this = this;
-
- this.marks.forEach(function(mark) {
- var type = mark.type === 'circle' ? 'point' : mark.type;
- var element = mark.type === 'line' ? 'path' : mark.type;
- mark.groups
- .classed('selected', false)
- .select(element)
- .attr(_this.config[type + '_attributes']);
- });
- if (this.multiples.chart) this.multiples.chart.destroy();
- delete this.selected_id;
- }
-
- function addOverlayEventListener() {
- var _this = this;
-
- this.overlay
- .on('mouseover', function() {
- clearHovered.call(_this);
- })
- .on('click', function() {
- clearHovered.call(_this);
- clearSelected.call(_this);
- });
- }
-
- 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', this.config.line_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: this.config.point_attributes.radius * 1.25,
- stroke: 'black',
- 'stroke-width': this.config.point_attributes['stroke-width'] * 4
- });
- }
-
- function reorderMarks() {
- var _this = this;
-
- //Move selected line behind all other lines.
- this.lines
- .each(function(d, i) {
- if (d.key.indexOf(_this.selected_id) === 0) d.order = _this.IDOrder.length - 1;
- else if (d.order > _this.selected_id_order) d.order = d.order - 1;
- })
- .sort(function(a, b) {
- return b.order - a.order;
- });
-
- //Move selected points behind all other points.
- this.points
- .each(function(d, i) {
- if (d.key.indexOf(_this.selected_id) === 0) d.order = _this.IDOrder.length - 1;
- else if (d.order > _this.selected_id_order) d.order = d.order - 1;
- })
- .sort(function(a, b) {
- return b.order - a.order;
- });
- }
-
- var _typeof =
- typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol'
- ? function(obj) {
- 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')
- .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
- .select(this)
- .selectAll('option:checked')
- .text();
- clearSelected.call(context);
- context.selected_id = context.multiples.id;
- highlightSelected.call(context);
- smallMultiples.call(context);
- });
- }
-
- 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,
- 'measure_unit',
- 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];
- _this.selected_id_order = _this.IDOrder.find(function(di) {
- return di.ID === _this.selected_id;
- }).order;
- highlightSelected.call(_this);
- reorderMarks.call(_this);
- smallMultiples.call(_this);
- });
- }
-
- function addPointEventListeners() {
- var _this = this;
-
- this.points
- .on('mouseover', function(d) {
- clearHovered.call(_this);
- _this.hovered_id = d.values.raw[0][_this.config.id_col];
- if (_this.hovered_id !== _this.selected_id) highlightHovered.call(_this);
- })
- .on('mouseout', function(d) {
- clearHovered.call(_this);
- })
- .on('click', function(d) {
- clearHovered.call(_this);
- clearSelected.call(_this);
- _this.selected_id = d.values.raw[0][_this.config.id_col];
- _this.selected_id_order = _this.IDOrder.find(function(di) {
- return di.ID === _this.selected_id;
- }).order;
- highlightSelected.call(_this);
- reorderMarks.call(_this);
- smallMultiples.call(_this);
- });
- }
-
- 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('.2f');
-
- //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
- .selectAll('.boxplot')
- .append('title')
- .text(function(d) {
- return (
- 'N = ' +
- d.values.length +
- '\n' +
- 'Min = ' +
- d3$1.min(d.values) +
- '\n' +
- '5th % = ' +
- fmt(d3$1.quantile(d.values, 0.05)) +
- '\n' +
- 'Q1 = ' +
- fmt(d3$1.quantile(d.values, 0.25)) +
- '\n' +
- 'Median = ' +
- fmt(d3$1.median(d.values)) +
- '\n' +
- 'Q3 = ' +
- fmt(d3$1.quantile(d.values, 0.75)) +
- '\n' +
- '95th % = ' +
- fmt(d3$1.quantile(d.values, 0.95)) +
- '\n' +
- 'Max = ' +
- d3$1.max(d.values) +
- '\n' +
- 'Mean = ' +
- fmt(d3$1.mean(d.values)) +
- '\n' +
- 'StDev = ' +
- fmt(d3$1.deviation(d.values))
- );
- });
- }
-
- 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 initial ordering to points; ordering will update as points are clicked.
- orderPoints.call(this);
-
- //Add event listeners to lines, points, and overlay.
- addEventListeners.call(this);
-
- //Draw a marginal box plot.
- addBoxPlot.call(this);
-
- //Rotate tick marks to prevent text overlap.
- adjustTicks.call(this);
- }
-
- //polyfills
-
- function safetyOutlierExplorer(element, settings) {
- //Merge user settings with default settings.
- var mergedSettings = Object.assign({}, defaultSettings, settings);
-
- //Sync options within settings object, e.g. data mappings.
- var syncedSettings = syncSettings(mergedSettings);
-
- //Sync control inputs with with settings object.
- var syncedControlInputs = syncControlInputs(controlInputs, syncedSettings);
- 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$1);
-
- return chart;
- }
-
- return safetyOutlierExplorer;
-});
diff --git a/package-lock.json b/package-lock.json
index 10e6c2d..fc4e81d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "safety-outlier-explorer",
- "version": "2.4.0",
+ "version": "2.5.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -11,9 +11,15 @@
"dev": true
},
"@types/node": {
- "version": "10.12.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.0.tgz",
- "integrity": "sha512-3TUHC3jsBAB7qVRGxT6lWyYo2v96BMmD2PTcl47H25Lu7UXtFH/2qqmKiVrnel6Ne//0TFYf6uvNX+HW2FRkLQ==",
+ "version": "10.12.21",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.21.tgz",
+ "integrity": "sha512-CBgLNk4o3XMnqMc0rhb6lc77IwShMEglz05deDcn2lQxyXEZivfwgYJu7SMha9V5XcrP6qZuevTHV/QrN2vjKQ==",
+ "dev": true
+ },
+ "acorn": {
+ "version": "6.0.7",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.7.tgz",
+ "integrity": "sha512-HNJNgE60C9eOTgn974Tlp3dpLZdUr+SoxxDwPaY9J/kDNOLQTkaDgwBUXAF4SSsrAwD9RpdxuHK/EbuF+W9Ahw==",
"dev": true
},
"adler-32": {
@@ -21,8 +27,8 @@
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz",
"integrity": "sha1-aj5r8KY5ALoVZSgIyxXGgT0aXyU=",
"requires": {
- "exit-on-epipe": "1.0.1",
- "printj": "1.1.2"
+ "exit-on-epipe": "~1.0.1",
+ "printj": "~1.1.0"
}
},
"ansi-regex": {
@@ -43,62 +49,59 @@
"integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
"dev": true,
"requires": {
- "chalk": "1.1.3",
- "esutils": "2.0.2",
- "js-tokens": "3.0.2"
+ "chalk": "^1.1.3",
+ "esutils": "^2.0.2",
+ "js-tokens": "^3.0.2"
+ }
+ },
+ "babel-core": {
+ "version": "6.26.3",
+ "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz",
+ "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==",
+ "dev": true,
+ "requires": {
+ "babel-code-frame": "^6.26.0",
+ "babel-generator": "^6.26.0",
+ "babel-helpers": "^6.24.1",
+ "babel-messages": "^6.23.0",
+ "babel-register": "^6.26.0",
+ "babel-runtime": "^6.26.0",
+ "babel-template": "^6.26.0",
+ "babel-traverse": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "babylon": "^6.18.0",
+ "convert-source-map": "^1.5.1",
+ "debug": "^2.6.9",
+ "json5": "^0.5.1",
+ "lodash": "^4.17.4",
+ "minimatch": "^3.0.4",
+ "path-is-absolute": "^1.0.1",
+ "private": "^0.1.8",
+ "slash": "^1.0.0",
+ "source-map": "^0.5.7"
}
},
"babel-generator": {
- "version": "6.26.0",
- "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.0.tgz",
- "integrity": "sha1-rBriAHC3n248odMmlhMFN3TyDcU=",
- "dev": true,
- "requires": {
- "babel-messages": "6.23.0",
- "babel-runtime": "6.26.0",
- "babel-types": "6.26.0",
- "detect-indent": "4.0.0",
- "jsesc": "1.3.0",
- "lodash": "4.17.4",
- "source-map": "0.5.7",
- "trim-right": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz"
+ "version": "6.26.1",
+ "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz",
+ "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==",
+ "dev": true,
+ "requires": {
+ "babel-messages": "^6.23.0",
+ "babel-runtime": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "detect-indent": "^4.0.0",
+ "jsesc": "^1.3.0",
+ "lodash": "^4.17.4",
+ "source-map": "^0.5.7",
+ "trim-right": "^1.0.1"
},
"dependencies": {
- "detect-indent": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
- "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=",
- "dev": true,
- "requires": {
- "repeating": "2.0.1"
- }
- },
"jsesc": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
"integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=",
"dev": true
- },
- "lodash": {
- "version": "4.17.4",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
- "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
- "dev": true
- },
- "repeating": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
- "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
- "dev": true,
- "requires": {
- "is-finite": "1.0.2"
- }
- },
- "source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
- "dev": true
}
}
},
@@ -108,9 +111,21 @@
"integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=",
"dev": true,
"requires": {
- "babel-helper-explode-assignable-expression": "6.24.1",
- "babel-runtime": "6.26.0",
- "babel-types": "6.26.0"
+ "babel-helper-explode-assignable-expression": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-helper-call-delegate": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz",
+ "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=",
+ "dev": true,
+ "requires": {
+ "babel-helper-hoist-variables": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-traverse": "^6.24.1",
+ "babel-types": "^6.24.1"
}
},
"babel-helper-define-map": {
@@ -119,18 +134,10 @@
"integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=",
"dev": true,
"requires": {
- "babel-helper-function-name": "6.24.1",
- "babel-runtime": "6.26.0",
- "babel-types": "6.26.0",
- "lodash": "4.17.4"
- },
- "dependencies": {
- "lodash": {
- "version": "4.17.4",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
- "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
- "dev": true
- }
+ "babel-helper-function-name": "^6.24.1",
+ "babel-runtime": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "lodash": "^4.17.4"
}
},
"babel-helper-explode-assignable-expression": {
@@ -139,9 +146,9 @@
"integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=",
"dev": true,
"requires": {
- "babel-runtime": "6.26.0",
- "babel-traverse": "6.26.0",
- "babel-types": "6.26.0"
+ "babel-runtime": "^6.22.0",
+ "babel-traverse": "^6.24.1",
+ "babel-types": "^6.24.1"
}
},
"babel-helper-function-name": {
@@ -150,11 +157,11 @@
"integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=",
"dev": true,
"requires": {
- "babel-helper-get-function-arity": "6.24.1",
- "babel-runtime": "6.26.0",
- "babel-template": "6.26.0",
- "babel-traverse": "6.26.0",
- "babel-types": "6.26.0"
+ "babel-helper-get-function-arity": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1",
+ "babel-traverse": "^6.24.1",
+ "babel-types": "^6.24.1"
}
},
"babel-helper-get-function-arity": {
@@ -163,8 +170,18 @@
"integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=",
"dev": true,
"requires": {
- "babel-runtime": "6.26.0",
- "babel-types": "6.26.0"
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-helper-hoist-variables": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz",
+ "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
}
},
"babel-helper-optimise-call-expression": {
@@ -173,8 +190,19 @@
"integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=",
"dev": true,
"requires": {
- "babel-runtime": "6.26.0",
- "babel-types": "6.26.0"
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-helper-regex": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz",
+ "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "lodash": "^4.17.4"
}
},
"babel-helper-remap-async-to-generator": {
@@ -183,11 +211,11 @@
"integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=",
"dev": true,
"requires": {
- "babel-helper-function-name": "6.24.1",
- "babel-runtime": "6.26.0",
- "babel-template": "6.26.0",
- "babel-traverse": "6.26.0",
- "babel-types": "6.26.0"
+ "babel-helper-function-name": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1",
+ "babel-traverse": "^6.24.1",
+ "babel-types": "^6.24.1"
}
},
"babel-helper-replace-supers": {
@@ -196,12 +224,12 @@
"integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=",
"dev": true,
"requires": {
- "babel-helper-optimise-call-expression": "6.24.1",
- "babel-messages": "6.23.0",
- "babel-runtime": "6.26.0",
- "babel-template": "6.26.0",
- "babel-traverse": "6.26.0",
- "babel-types": "6.26.0"
+ "babel-helper-optimise-call-expression": "^6.24.1",
+ "babel-messages": "^6.23.0",
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1",
+ "babel-traverse": "^6.24.1",
+ "babel-types": "^6.24.1"
}
},
"babel-helpers": {
@@ -210,8 +238,8 @@
"integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=",
"dev": true,
"requires": {
- "babel-runtime": "6.26.0",
- "babel-template": "6.26.0"
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1"
}
},
"babel-messages": {
@@ -220,7 +248,16 @@
"integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=",
"dev": true,
"requires": {
- "babel-runtime": "6.26.0"
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-check-es2015-constants": {
+ "version": "6.22.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz",
+ "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
}
},
"babel-plugin-external-helpers": {
@@ -229,7 +266,7 @@
"integrity": "sha1-IoX0iwK9Xe3oUXXK+MYuhq3M76E=",
"dev": true,
"requires": {
- "babel-runtime": "6.26.0"
+ "babel-runtime": "^6.22.0"
}
},
"babel-plugin-syntax-async-functions": {
@@ -256,9 +293,40 @@
"integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=",
"dev": true,
"requires": {
- "babel-helper-remap-async-to-generator": "6.24.1",
- "babel-plugin-syntax-async-functions": "6.13.0",
- "babel-runtime": "6.26.0"
+ "babel-helper-remap-async-to-generator": "^6.24.1",
+ "babel-plugin-syntax-async-functions": "^6.8.0",
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-arrow-functions": {
+ "version": "6.22.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz",
+ "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-block-scoped-functions": {
+ "version": "6.22.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz",
+ "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-block-scoping": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz",
+ "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.26.0",
+ "babel-template": "^6.26.0",
+ "babel-traverse": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "lodash": "^4.17.4"
}
},
"babel-plugin-transform-es2015-classes": {
@@ -267,15 +335,201 @@
"integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=",
"dev": true,
"requires": {
- "babel-helper-define-map": "6.26.0",
- "babel-helper-function-name": "6.24.1",
- "babel-helper-optimise-call-expression": "6.24.1",
- "babel-helper-replace-supers": "6.24.1",
- "babel-messages": "6.23.0",
- "babel-runtime": "6.26.0",
- "babel-template": "6.26.0",
- "babel-traverse": "6.26.0",
- "babel-types": "6.26.0"
+ "babel-helper-define-map": "^6.24.1",
+ "babel-helper-function-name": "^6.24.1",
+ "babel-helper-optimise-call-expression": "^6.24.1",
+ "babel-helper-replace-supers": "^6.24.1",
+ "babel-messages": "^6.23.0",
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1",
+ "babel-traverse": "^6.24.1",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-computed-properties": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz",
+ "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-destructuring": {
+ "version": "6.23.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz",
+ "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-duplicate-keys": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz",
+ "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-for-of": {
+ "version": "6.23.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz",
+ "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-function-name": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz",
+ "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=",
+ "dev": true,
+ "requires": {
+ "babel-helper-function-name": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-literals": {
+ "version": "6.22.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz",
+ "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-modules-amd": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz",
+ "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=",
+ "dev": true,
+ "requires": {
+ "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-modules-commonjs": {
+ "version": "6.26.2",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz",
+ "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==",
+ "dev": true,
+ "requires": {
+ "babel-plugin-transform-strict-mode": "^6.24.1",
+ "babel-runtime": "^6.26.0",
+ "babel-template": "^6.26.0",
+ "babel-types": "^6.26.0"
+ }
+ },
+ "babel-plugin-transform-es2015-modules-systemjs": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz",
+ "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=",
+ "dev": true,
+ "requires": {
+ "babel-helper-hoist-variables": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-modules-umd": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz",
+ "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=",
+ "dev": true,
+ "requires": {
+ "babel-plugin-transform-es2015-modules-amd": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-object-super": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz",
+ "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=",
+ "dev": true,
+ "requires": {
+ "babel-helper-replace-supers": "^6.24.1",
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-parameters": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz",
+ "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=",
+ "dev": true,
+ "requires": {
+ "babel-helper-call-delegate": "^6.24.1",
+ "babel-helper-get-function-arity": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1",
+ "babel-traverse": "^6.24.1",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-shorthand-properties": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz",
+ "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-spread": {
+ "version": "6.22.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz",
+ "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-sticky-regex": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz",
+ "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=",
+ "dev": true,
+ "requires": {
+ "babel-helper-regex": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
+ }
+ },
+ "babel-plugin-transform-es2015-template-literals": {
+ "version": "6.22.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz",
+ "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-typeof-symbol": {
+ "version": "6.23.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz",
+ "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-es2015-unicode-regex": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz",
+ "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=",
+ "dev": true,
+ "requires": {
+ "babel-helper-regex": "^6.24.1",
+ "babel-runtime": "^6.22.0",
+ "regexpu-core": "^2.0.0"
}
},
"babel-plugin-transform-exponentiation-operator": {
@@ -284,344 +538,66 @@
"integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=",
"dev": true,
"requires": {
- "babel-helper-builder-binary-assignment-operator-visitor": "6.24.1",
- "babel-plugin-syntax-exponentiation-operator": "6.13.0",
- "babel-runtime": "6.26.0"
+ "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1",
+ "babel-plugin-syntax-exponentiation-operator": "^6.8.0",
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "babel-plugin-transform-regenerator": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz",
+ "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=",
+ "dev": true,
+ "requires": {
+ "regenerator-transform": "^0.10.0"
+ }
+ },
+ "babel-plugin-transform-strict-mode": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz",
+ "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.22.0",
+ "babel-types": "^6.24.1"
}
},
"babel-preset-env": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.6.1.tgz",
- "integrity": "sha512-W6VIyA6Ch9ePMI7VptNn2wBM6dbG0eSz25HEiL40nQXCsXGTGZSTZu1Iap+cj3Q0S5a7T9+529l/5Bkvd+afNA==",
- "dev": true,
- "requires": {
- "babel-plugin-check-es2015-constants": "6.22.0",
- "babel-plugin-syntax-trailing-function-commas": "6.22.0",
- "babel-plugin-transform-async-to-generator": "6.24.1",
- "babel-plugin-transform-es2015-arrow-functions": "6.22.0",
- "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0",
- "babel-plugin-transform-es2015-block-scoping": "6.26.0",
- "babel-plugin-transform-es2015-classes": "6.24.1",
- "babel-plugin-transform-es2015-computed-properties": "6.24.1",
- "babel-plugin-transform-es2015-destructuring": "6.23.0",
- "babel-plugin-transform-es2015-duplicate-keys": "6.24.1",
- "babel-plugin-transform-es2015-for-of": "6.23.0",
- "babel-plugin-transform-es2015-function-name": "6.24.1",
- "babel-plugin-transform-es2015-literals": "6.22.0",
- "babel-plugin-transform-es2015-modules-amd": "6.24.1",
- "babel-plugin-transform-es2015-modules-commonjs": "6.26.0",
- "babel-plugin-transform-es2015-modules-systemjs": "6.24.1",
- "babel-plugin-transform-es2015-modules-umd": "6.24.1",
- "babel-plugin-transform-es2015-object-super": "6.24.1",
- "babel-plugin-transform-es2015-parameters": "6.24.1",
- "babel-plugin-transform-es2015-shorthand-properties": "6.24.1",
- "babel-plugin-transform-es2015-spread": "6.22.0",
- "babel-plugin-transform-es2015-sticky-regex": "6.24.1",
- "babel-plugin-transform-es2015-template-literals": "6.22.0",
- "babel-plugin-transform-es2015-typeof-symbol": "6.23.0",
- "babel-plugin-transform-es2015-unicode-regex": "6.24.1",
- "babel-plugin-transform-exponentiation-operator": "6.24.1",
- "babel-plugin-transform-regenerator": "6.26.0",
- "browserslist": "2.9.1",
- "invariant": "2.2.2",
- "semver": "5.4.1"
- },
- "dependencies": {
- "babel-helper-call-delegate": {
- "version": "6.24.1",
- "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz",
- "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=",
- "dev": true,
- "requires": {
- "babel-helper-hoist-variables": "6.24.1",
- "babel-runtime": "6.26.0",
- "babel-traverse": "6.26.0",
- "babel-types": "6.26.0"
- }
- },
- "babel-helper-hoist-variables": {
- "version": "6.24.1",
- "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz",
- "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0",
- "babel-types": "6.26.0"
- }
- },
- "babel-helper-regex": {
- "version": "6.26.0",
- "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz",
- "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0",
- "babel-types": "6.26.0",
- "lodash": "4.17.4"
- }
- },
- "babel-plugin-check-es2015-constants": {
- "version": "6.22.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz",
- "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-arrow-functions": {
- "version": "6.22.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz",
- "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-block-scoped-functions": {
- "version": "6.22.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz",
- "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-block-scoping": {
- "version": "6.26.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz",
- "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0",
- "babel-template": "6.26.0",
- "babel-traverse": "6.26.0",
- "babel-types": "6.26.0",
- "lodash": "4.17.4"
- }
- },
- "babel-plugin-transform-es2015-computed-properties": {
- "version": "6.24.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz",
- "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0",
- "babel-template": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-destructuring": {
- "version": "6.23.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz",
- "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-duplicate-keys": {
- "version": "6.24.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz",
- "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0",
- "babel-types": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-for-of": {
- "version": "6.23.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz",
- "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-function-name": {
- "version": "6.24.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz",
- "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=",
- "dev": true,
- "requires": {
- "babel-helper-function-name": "6.24.1",
- "babel-runtime": "6.26.0",
- "babel-types": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-literals": {
- "version": "6.22.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz",
- "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-modules-amd": {
- "version": "6.24.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz",
- "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=",
- "dev": true,
- "requires": {
- "babel-plugin-transform-es2015-modules-commonjs": "6.26.0",
- "babel-runtime": "6.26.0",
- "babel-template": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-modules-commonjs": {
- "version": "6.26.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz",
- "integrity": "sha1-DYOUApt9xqvhqX7xgeAHWN0uXYo=",
- "dev": true,
- "requires": {
- "babel-plugin-transform-strict-mode": "6.24.1",
- "babel-runtime": "6.26.0",
- "babel-template": "6.26.0",
- "babel-types": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-modules-systemjs": {
- "version": "6.24.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz",
- "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=",
- "dev": true,
- "requires": {
- "babel-helper-hoist-variables": "6.24.1",
- "babel-runtime": "6.26.0",
- "babel-template": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-modules-umd": {
- "version": "6.24.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz",
- "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=",
- "dev": true,
- "requires": {
- "babel-plugin-transform-es2015-modules-amd": "6.24.1",
- "babel-runtime": "6.26.0",
- "babel-template": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-object-super": {
- "version": "6.24.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz",
- "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=",
- "dev": true,
- "requires": {
- "babel-helper-replace-supers": "6.24.1",
- "babel-runtime": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-parameters": {
- "version": "6.24.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz",
- "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=",
- "dev": true,
- "requires": {
- "babel-helper-call-delegate": "6.24.1",
- "babel-helper-get-function-arity": "6.24.1",
- "babel-runtime": "6.26.0",
- "babel-template": "6.26.0",
- "babel-traverse": "6.26.0",
- "babel-types": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-shorthand-properties": {
- "version": "6.24.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz",
- "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0",
- "babel-types": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-spread": {
- "version": "6.22.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz",
- "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-sticky-regex": {
- "version": "6.24.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz",
- "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=",
- "dev": true,
- "requires": {
- "babel-helper-regex": "6.26.0",
- "babel-runtime": "6.26.0",
- "babel-types": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-template-literals": {
- "version": "6.22.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz",
- "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-typeof-symbol": {
- "version": "6.23.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz",
- "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0"
- }
- },
- "babel-plugin-transform-es2015-unicode-regex": {
- "version": "6.24.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz",
- "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=",
- "dev": true,
- "requires": {
- "babel-helper-regex": "6.26.0",
- "babel-runtime": "6.26.0",
- "regexpu-core": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz"
- }
- },
- "babel-plugin-transform-regenerator": {
- "version": "6.26.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz",
- "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=",
- "dev": true,
- "requires": {
- "regenerator-transform": "0.10.1"
- }
- },
- "babel-plugin-transform-strict-mode": {
- "version": "6.24.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz",
- "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0",
- "babel-types": "6.26.0"
- }
- },
- "lodash": {
- "version": "4.17.4",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
- "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
- "dev": true
- },
- "regenerator-transform": {
- "version": "0.10.1",
- "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz",
- "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0",
- "babel-types": "6.26.0",
- "private": "0.1.8"
- }
- }
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.7.0.tgz",
+ "integrity": "sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg==",
+ "dev": true,
+ "requires": {
+ "babel-plugin-check-es2015-constants": "^6.22.0",
+ "babel-plugin-syntax-trailing-function-commas": "^6.22.0",
+ "babel-plugin-transform-async-to-generator": "^6.22.0",
+ "babel-plugin-transform-es2015-arrow-functions": "^6.22.0",
+ "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0",
+ "babel-plugin-transform-es2015-block-scoping": "^6.23.0",
+ "babel-plugin-transform-es2015-classes": "^6.23.0",
+ "babel-plugin-transform-es2015-computed-properties": "^6.22.0",
+ "babel-plugin-transform-es2015-destructuring": "^6.23.0",
+ "babel-plugin-transform-es2015-duplicate-keys": "^6.22.0",
+ "babel-plugin-transform-es2015-for-of": "^6.23.0",
+ "babel-plugin-transform-es2015-function-name": "^6.22.0",
+ "babel-plugin-transform-es2015-literals": "^6.22.0",
+ "babel-plugin-transform-es2015-modules-amd": "^6.22.0",
+ "babel-plugin-transform-es2015-modules-commonjs": "^6.23.0",
+ "babel-plugin-transform-es2015-modules-systemjs": "^6.23.0",
+ "babel-plugin-transform-es2015-modules-umd": "^6.23.0",
+ "babel-plugin-transform-es2015-object-super": "^6.22.0",
+ "babel-plugin-transform-es2015-parameters": "^6.23.0",
+ "babel-plugin-transform-es2015-shorthand-properties": "^6.22.0",
+ "babel-plugin-transform-es2015-spread": "^6.22.0",
+ "babel-plugin-transform-es2015-sticky-regex": "^6.22.0",
+ "babel-plugin-transform-es2015-template-literals": "^6.22.0",
+ "babel-plugin-transform-es2015-typeof-symbol": "^6.23.0",
+ "babel-plugin-transform-es2015-unicode-regex": "^6.22.0",
+ "babel-plugin-transform-exponentiation-operator": "^6.22.0",
+ "babel-plugin-transform-regenerator": "^6.22.0",
+ "browserslist": "^3.2.6",
+ "invariant": "^2.2.2",
+ "semver": "^5.3.0"
}
},
"babel-register": {
@@ -630,48 +606,42 @@
"integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=",
"dev": true,
"requires": {
- "babel-core": "6.26.0",
- "babel-runtime": "6.26.0",
- "core-js": "2.5.1",
- "home-or-tmp": "2.0.0",
- "lodash": "4.17.4",
- "mkdirp": "0.5.1",
- "source-map-support": "0.4.18"
+ "babel-core": "^6.26.0",
+ "babel-runtime": "^6.26.0",
+ "core-js": "^2.5.0",
+ "home-or-tmp": "^2.0.0",
+ "lodash": "^4.17.4",
+ "mkdirp": "^0.5.1",
+ "source-map-support": "^0.4.15"
},
"dependencies": {
"babel-core": {
- "version": "6.26.0",
- "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz",
- "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=",
+ "version": "6.26.3",
+ "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz",
+ "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==",
"dev": true,
"requires": {
- "babel-code-frame": "6.26.0",
- "babel-generator": "6.26.0",
- "babel-helpers": "6.24.1",
- "babel-messages": "6.23.0",
- "babel-register": "6.26.0",
- "babel-runtime": "6.26.0",
- "babel-template": "6.26.0",
- "babel-traverse": "6.26.0",
- "babel-types": "6.26.0",
- "babylon": "6.18.0",
- "convert-source-map": "1.5.1",
- "debug": "2.6.9",
- "json5": "0.5.1",
- "lodash": "4.17.4",
- "minimatch": "3.0.4",
- "path-is-absolute": "1.0.1",
- "private": "0.1.8",
- "slash": "1.0.0",
- "source-map": "0.5.7"
+ "babel-code-frame": "^6.26.0",
+ "babel-generator": "^6.26.0",
+ "babel-helpers": "^6.24.1",
+ "babel-messages": "^6.23.0",
+ "babel-register": "^6.26.0",
+ "babel-runtime": "^6.26.0",
+ "babel-template": "^6.26.0",
+ "babel-traverse": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "babylon": "^6.18.0",
+ "convert-source-map": "^1.5.1",
+ "debug": "^2.6.9",
+ "json5": "^0.5.1",
+ "lodash": "^4.17.4",
+ "minimatch": "^3.0.4",
+ "path-is-absolute": "^1.0.1",
+ "private": "^0.1.8",
+ "slash": "^1.0.0",
+ "source-map": "^0.5.7"
}
},
- "babylon": {
- "version": "6.18.0",
- "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
- "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
- "dev": true
- },
"core-js": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz",
@@ -684,20 +654,14 @@
"integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=",
"dev": true,
"requires": {
- "os-homedir": "1.0.2",
- "os-tmpdir": "1.0.2"
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.1"
}
},
- "json5": {
- "version": "0.5.1",
- "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
- "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
- "dev": true
- },
"lodash": {
- "version": "4.17.4",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
- "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
},
"source-map": {
@@ -712,7 +676,7 @@
"integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==",
"dev": true,
"requires": {
- "source-map": "0.5.7"
+ "source-map": "^0.5.6"
}
}
}
@@ -723,8 +687,8 @@
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
"dev": true,
"requires": {
- "core-js": "2.5.1",
- "regenerator-runtime": "0.11.0"
+ "core-js": "^2.4.0",
+ "regenerator-runtime": "^0.11.0"
},
"dependencies": {
"core-js": {
@@ -741,11 +705,11 @@
"integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=",
"dev": true,
"requires": {
- "babel-runtime": "6.26.0",
- "babel-traverse": "6.26.0",
- "babel-types": "6.26.0",
- "babylon": "6.18.0",
- "lodash": "4.17.4"
+ "babel-runtime": "^6.26.0",
+ "babel-traverse": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "babylon": "^6.18.0",
+ "lodash": "^4.17.4"
},
"dependencies": {
"babylon": {
@@ -755,9 +719,9 @@
"dev": true
},
"lodash": {
- "version": "4.17.4",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
- "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
}
}
@@ -768,15 +732,15 @@
"integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=",
"dev": true,
"requires": {
- "babel-code-frame": "6.26.0",
- "babel-messages": "6.23.0",
- "babel-runtime": "6.26.0",
- "babel-types": "6.26.0",
- "babylon": "6.18.0",
- "debug": "2.6.9",
- "globals": "9.18.0",
- "invariant": "2.2.2",
- "lodash": "4.17.4"
+ "babel-code-frame": "^6.26.0",
+ "babel-messages": "^6.23.0",
+ "babel-runtime": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "babylon": "^6.18.0",
+ "debug": "^2.6.8",
+ "globals": "^9.18.0",
+ "invariant": "^2.2.2",
+ "lodash": "^4.17.4"
},
"dependencies": {
"babylon": {
@@ -792,9 +756,9 @@
"dev": true
},
"lodash": {
- "version": "4.17.4",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
- "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
}
}
@@ -805,20 +769,26 @@
"integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=",
"dev": true,
"requires": {
- "babel-runtime": "6.26.0",
- "esutils": "2.0.2",
- "lodash": "4.17.4",
- "to-fast-properties": "1.0.3"
+ "babel-runtime": "^6.26.0",
+ "esutils": "^2.0.2",
+ "lodash": "^4.17.4",
+ "to-fast-properties": "^1.0.3"
},
"dependencies": {
"lodash": {
- "version": "4.17.4",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
- "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
}
}
},
+ "babylon": {
+ "version": "6.18.0",
+ "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
+ "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
+ "dev": true
+ },
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@@ -831,18 +801,18 @@
"integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
"dev": true,
"requires": {
- "balanced-match": "1.0.0",
+ "balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"browserslist": {
- "version": "2.9.1",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.9.1.tgz",
- "integrity": "sha512-3n3nPdbUqn3nWmsy4PeSQthz2ja1ndpoXta+dwFFNhveGjMg6FXpWYe12vsTpNoXJbzx3j7GZXdtoVIdvh3JbA==",
+ "version": "3.2.8",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz",
+ "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==",
"dev": true,
"requires": {
- "caniuse-lite": "1.0.30000778",
- "electron-to-chromium": "1.3.28"
+ "caniuse-lite": "^1.0.30000844",
+ "electron-to-chromium": "^1.3.47"
}
},
"buffer-from": {
@@ -852,9 +822,9 @@
"optional": true
},
"caniuse-lite": {
- "version": "1.0.30000778",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000778.tgz",
- "integrity": "sha1-8efLixOx9nREAikddfC81MMWA2k=",
+ "version": "1.0.30000936",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000936.tgz",
+ "integrity": "sha512-orX4IdpbFhdNO7bTBhSbahp1EBpqzBc+qrvTRVUFfZgA4zta7TdM6PN5ZxkEUgDnz36m+PfWGcdX7AVfFWItJw==",
"dev": true
},
"cfb": {
@@ -863,10 +833,10 @@
"integrity": "sha512-ZqfxNGWTMKhd0a/n6YKJLq8hWbd5kR3cA4kXwUj9vVEdHlwJ09werR8gN15Z7Y1FTXqdD6dE3GGCxv4uc28raA==",
"optional": true,
"requires": {
- "adler-32": "1.2.0",
- "commander": "2.19.0",
- "crc-32": "1.2.0",
- "printj": "1.1.2"
+ "adler-32": "~1.2.0",
+ "commander": "^2.16.0",
+ "crc-32": "~1.2.0",
+ "printj": "~1.1.2"
}
},
"chalk": {
@@ -875,11 +845,11 @@
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
- "ansi-styles": "2.2.1",
- "escape-string-regexp": "1.0.5",
- "has-ansi": "2.0.0",
- "strip-ansi": "3.0.1",
- "supports-color": "2.0.0"
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
}
},
"codepage": {
@@ -888,9 +858,9 @@
"integrity": "sha1-Ty5dfAl13ij4hJgFjcta/KtqX3E=",
"optional": true,
"requires": {
- "commander": "2.19.0",
- "concat-stream": "1.6.2",
- "voc": "1.1.0"
+ "commander": "^2.19.0",
+ "concat-stream": "^1.6.2",
+ "voc": "^1.1.0"
}
},
"colors": {
@@ -916,10 +886,10 @@
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"optional": true,
"requires": {
- "buffer-from": "1.1.1",
- "inherits": "2.0.3",
- "readable-stream": "2.3.6",
- "typedarray": "0.0.6"
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
},
"dependencies": {
"core-util-is": {
@@ -951,13 +921,13 @@
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"optional": true,
"requires": {
- "core-util-is": "1.0.2",
- "inherits": "2.0.3",
- "isarray": "1.0.0",
- "process-nextick-args": "2.0.0",
- "safe-buffer": "5.1.2",
- "string_decoder": "1.1.1",
- "util-deprecate": "1.0.2"
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
}
},
"string_decoder": {
@@ -966,7 +936,7 @@
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"optional": true,
"requires": {
- "safe-buffer": "5.1.2"
+ "safe-buffer": "~5.1.0"
}
},
"util-deprecate": {
@@ -988,12 +958,13 @@
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz",
"integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==",
"requires": {
- "exit-on-epipe": "1.0.1",
- "printj": "1.1.2"
+ "exit-on-epipe": "~1.0.1",
+ "printj": "~1.1.0"
}
},
"d3": {
- "version": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz",
+ "version": "3.5.17",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz",
"integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g="
},
"debug": {
@@ -1005,10 +976,19 @@
"ms": "2.0.0"
}
},
+ "detect-indent": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
+ "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=",
+ "dev": true,
+ "requires": {
+ "repeating": "^2.0.0"
+ }
+ },
"electron-to-chromium": {
- "version": "1.3.28",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.28.tgz",
- "integrity": "sha1-jdTmRYCGZE6fnwoc8y4qH53/2e4=",
+ "version": "1.3.113",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz",
+ "integrity": "sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g==",
"dev": true
},
"escape-string-regexp": {
@@ -1046,7 +1026,7 @@
"integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
"dev": true,
"requires": {
- "ansi-regex": "2.1.1"
+ "ansi-regex": "^2.0.0"
}
},
"invariant": {
@@ -1055,7 +1035,7 @@
"integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=",
"dev": true,
"requires": {
- "loose-envify": "1.3.1"
+ "loose-envify": "^1.0.0"
}
},
"is-finite": {
@@ -1064,7 +1044,7 @@
"integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
"dev": true,
"requires": {
- "number-is-nan": "1.0.1"
+ "number-is-nan": "^1.0.0"
}
},
"js-tokens": {
@@ -1079,36 +1059,49 @@
"integrity": "sha512-3N4a9RBHTr777rxxlvwJVpC+er/neRC+40sm2M/g3RIpWiCJG0iyaGJa8Za1K3NvjhZcKn9Sz5n36TY9ti5RMQ==",
"optional": true,
"requires": {
- "adler-32": "1.2.0",
- "cfb": "1.1.0",
- "codepage": "1.3.8",
- "commander": "2.19.0",
- "crc-32": "1.2.0",
+ "adler-32": "^1.2.0",
+ "cfb": ">=0.10.0",
+ "codepage": "~1.3.6",
+ "commander": "^2.19.0",
+ "crc-32": "^1.2.0",
"jszip": "2.4.0",
- "ssf": "0.8.2"
+ "ssf": "~0.8.1"
}
},
"jsesc": {
- "version": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
"dev": true
},
+ "json5": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+ "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
+ "dev": true
+ },
"jszip": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-2.4.0.tgz",
"integrity": "sha1-SHqTt2w7/6bLCFzWHrk06r4tKU8=",
"optional": true,
"requires": {
- "pako": "0.2.9"
+ "pako": "~0.2.5"
}
},
+ "lodash": {
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+ "dev": true
+ },
"loose-envify": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
"integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
"dev": true,
"requires": {
- "js-tokens": "3.0.2"
+ "js-tokens": "^3.0.0"
}
},
"minimatch": {
@@ -1117,7 +1110,7 @@
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
- "brace-expansion": "1.1.8"
+ "brace-expansion": "^1.1.7"
}
},
"minimist": {
@@ -1189,8 +1182,9 @@
"dev": true
},
"regenerate": {
- "version": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.2.tgz",
- "integrity": "sha1-0ZQcZ7rUN+G+dkM63Vs4X5WxkmA=",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
+ "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==",
"dev": true
},
"regenerator-runtime": {
@@ -1199,37 +1193,61 @@
"integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==",
"dev": true
},
+ "regenerator-transform": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz",
+ "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.18.0",
+ "babel-types": "^6.19.0",
+ "private": "^0.1.6"
+ }
+ },
"regexpu-core": {
- "version": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz",
"integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=",
"dev": true,
"requires": {
- "regenerate": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.2.tgz",
- "regjsgen": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
- "regjsparser": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz"
+ "regenerate": "^1.2.1",
+ "regjsgen": "^0.2.0",
+ "regjsparser": "^0.1.4"
}
},
"regjsgen": {
- "version": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
"integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=",
"dev": true
},
"regjsparser": {
- "version": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
"integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=",
"dev": true,
"requires": {
- "jsesc": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz"
+ "jsesc": "~0.5.0"
+ }
+ },
+ "repeating": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+ "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+ "dev": true,
+ "requires": {
+ "is-finite": "^1.0.0"
}
},
"rollup": {
- "version": "0.66.6",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.66.6.tgz",
- "integrity": "sha512-J7/SWanrcb83vfIHqa8+aVVGzy457GcjA6GVZEnD0x2u4OnOd0Q1pCrEoNe8yLwM6z6LZP02zBT2uW0yh5TqOw==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.1.2.tgz",
+ "integrity": "sha512-OkdMxqMl8pWoQc5D8y1cIinYQPPLV8ZkfLgCzL6SytXeNA2P7UHynEQXI9tYxuAjAMsSyvRaWnyJDLHMxq0XAg==",
"dev": true,
"requires": {
"@types/estree": "0.0.39",
- "@types/node": "10.12.0"
+ "@types/node": "*",
+ "acorn": "^6.0.5"
}
},
"rollup-plugin-babel": {
@@ -1238,7 +1256,7 @@
"integrity": "sha512-ALGPBFtwJZcYHsNPM6RGJlEncTzAARPvZOGjNPZgDe5hS5t6sJGjiOWibEFVEz5LQN7S7spvCBILaS4N1Cql2w==",
"dev": true,
"requires": {
- "rollup-pluginutils": "1.5.2"
+ "rollup-pluginutils": "^1.5.0"
}
},
"rollup-pluginutils": {
@@ -1247,8 +1265,8 @@
"integrity": "sha1-HhVud4+UtyVb+hs9AXi+j1xVJAg=",
"dev": true,
"requires": {
- "estree-walker": "0.2.1",
- "minimatch": "3.0.4"
+ "estree-walker": "^0.2.1",
+ "minimatch": "^3.0.2"
}
},
"safe-buffer": {
@@ -1257,9 +1275,9 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"semver": {
- "version": "5.4.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
- "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==",
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
+ "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==",
"dev": true
},
"slash": {
@@ -1268,6 +1286,12 @@
"integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
"dev": true
},
+ "source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true
+ },
"ssf": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.8.2.tgz",
@@ -1276,7 +1300,7 @@
"requires": {
"colors": "0.6.2",
"frac": "0.3.1",
- "voc": "1.1.0"
+ "voc": "^1.1.0"
}
},
"strip-ansi": {
@@ -1285,7 +1309,7 @@
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"requires": {
- "ansi-regex": "2.1.1"
+ "ansi-regex": "^2.0.0"
}
},
"supports-color": {
@@ -1301,7 +1325,8 @@
"dev": true
},
"trim-right": {
- "version": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
"dev": true
},
@@ -1321,8 +1346,8 @@
"resolved": "https://registry.npmjs.org/webcharts/-/webcharts-1.11.1.tgz",
"integrity": "sha512-sZCz2Z2tdC22+HHI58nb5uY8LxvDGXOdMnoG6pBpE/mht2ruaobMnRtOw+S2MUZnIPYybt5jeHO7h5Sggg8Siw==",
"requires": {
- "d3": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz",
- "js-xlsx": "0.8.22"
+ "d3": "^3",
+ "js-xlsx": "^0.8.22"
}
}
}
diff --git a/package.json b/package.json
index b8b58ab..f5302f6 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,9 @@
{
"name": "safety-outlier-explorer",
- "version": "2.4.0",
+ "version": "2.5.0",
"description": "Chart showing participant trajectories of lab measures, vital signs and other related measures in clinical trials.",
- "main": "./build/safetyOutlierExplorer.js",
- "module": "./src/wrapper.js",
+ "module": "./src/index.js",
+ "main": "./safetyOutlierExplorer.js",
"author": "Rho, Inc.",
"license": "MIT",
"dependencies": {
@@ -11,21 +11,25 @@
"webcharts": "^1.10"
},
"scripts": {
- "build": "npm run bundle && npm run format && npm run build-md",
- "build-md": "node ./scripts/configuration-markdown.js",
+ "build": "npm audit fix && npm run bundle && npm run format && npm run build-wiki && npm run check-settings-schema",
+ "build-wiki": "npm run build-configuration-wiki && npm run build-data-guidelines-wiki",
+ "build-configuration-wiki": "node ./scripts/build-configuration-wiki.js",
+ "build-data-guidelines-wiki": "node ./scripts/build-data-guidelines-wiki.js",
"bundle": "rollup -c",
+ "check-settings-schema": "node ./scripts/check-settings-schema.js",
"format": "npm run format-src && npm run format-bundle",
"format-src": "prettier --print-width=100 --tab-width=4 --single-quote --write \"./src/**/*.js\"",
- "format-bundle": "prettier --print-width=100 --tab-width=4 --single-quote --write ./build/safetyOutlierExplorer.js",
+ "format-bundle": "prettier --print-width=100 --tab-width=4 --single-quote --write ./safetyOutlierExplorer.js",
"test-page": "start chrome ./test-page/index.html && start firefox ./test-page/index.html && start iexplore file://%CD%/test-page/index.html",
"watch": "rollup -c -w"
},
"devDependencies": {
+ "babel-core": "^6.26.3",
"babel-plugin-external-helpers": "^6.22.0",
- "babel-preset-env": "^1.6.0",
+ "babel-preset-env": "^1.7.0",
"babel-register": "^6.26.0",
"prettier": "^1.7.4",
- "rollup": "^0.66.6",
+ "rollup": "^1.1.2",
"rollup-plugin-babel": "^3.0.2"
}
}
diff --git a/safetyOutlierExplorer.js b/safetyOutlierExplorer.js
new file mode 100644
index 0000000..fe67218
--- /dev/null
+++ b/safetyOutlierExplorer.js
@@ -0,0 +1,1703 @@
+(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, 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;
+ };
+
+ // 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 {
+ //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];
+
+ //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 + ']';
+
+ //Conadd 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.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;
+
+ 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.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.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.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[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;
+ }));
+ } else
+ //Otherwise use data-driven visit order.
+ time_settings.order = visitOrder;
+
+ //Define domain.
+ time_settings.domain = time_settings.order;
+ });
+ }
+
+ function measure() {
+ var _this = this;
+
+ this.measures = d3.set(this.initial_data.map(function (d) {
+ return d[_this.config.measure_col];
+ })).values().sort();
+ this.soe_measures = d3.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.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 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);
+ }
+
+ 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.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.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({
+ 'border-top': '1px solid #ccc',
+ '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.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.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 / 15;
+ 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;
+ }
+
+ 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.median(_this.measure.data, function (d) {
+ return +d[_this.config.normal_col_low];
+ });
+ };
+ this.uln = function (d) {
+ return d instanceof Object ? +d[_this.config.normal_col_high] : d3.median(_this.measure.data, function (d) {
+ return +d[_this.config.normal_col_high];
+ });
+ };
+ } else if (this.config.normal_range_method === 'Standard Deviation') {
+ this.mean = d3.mean(this.measure.results);
+ this.sd = d3.deviation(this.measure.results);
+ this.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.measure.results, _this.config.normal_range_quantile_low);
+ };
+ this.uln = function () {
+ return d3.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.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 resetChart() {
+ this.svg.selectAll('.line,.point').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 * 1.5;
+ },
+ 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 orderPoints() {
+ var _this = this;
+
+ this.marks.filter(function (mark) {
+ return mark.type === 'circle';
+ }).forEach(function (mark) {
+ mark.groups.each(function (d, i) {
+ d.order = _this.IDOrder.find(function (di) {
+ return d.key.indexOf(di.ID) === 0;
+ }).order;
+ });
+ });
+ }
+
+ function clearHovered() {
+ this.lines.filter(function () {
+ return !d3.select(this).classed('selected');
+ }).select('path').each(function (d) {
+ d3.select(this).attr(d.attributes);
+ });
+ this.points.filter(function () {
+ return !d3.select(this).classed('selected');
+ }).select('circle').each(function (d) {
+ d3.select(this).attr(d.attributes);
+ d3.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;
+ }
+
+ function addOverlayEventListener() {
+ var _this = this;
+
+ this.overlay.on('mouseover', function () {
+ clearHovered.call(_this);
+ }).on('click', function () {
+ clearHovered.call(_this);
+ clearSelected.call(_this);
+ });
+ }
+
+ 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 * 1.25;
+ },
+ stroke: 'black',
+ 'stroke-width': function strokeWidth(d) {
+ return d.attributes['stroke-width'] * 4;
+ }
+ });
+ }
+
+ function reorderMarks() {
+ var _this = this;
+
+ //Move selected line behind all other lines.
+ this.lines.each(function (d, i) {
+ if (d.key.indexOf(_this.selected_id) === 0) d.order = _this.IDOrder.length - 1;else if (d.order > _this.selected_id_order) d.order = d.order - 1;
+ }).sort(function (a, b) {
+ return b.order - a.order;
+ });
+
+ //Move selected points behind all other points.
+ this.points.each(function (d, i) {
+ if (d.key.indexOf(_this.selected_id) === 0) d.order = _this.IDOrder.length - 1;else if (d.order > _this.selected_id_order) d.order = d.order - 1;
+ }).sort(function (a, b) {
+ return b.order - a.order;
+ });
+ }
+
+ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
+ 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.min(filtered_data.map(function (m) {
+ return +m[_this.config.y.column];
+ }).filter(function (f) {
+ return +f || +f === 0;
+ }));
+ var yhi = d3.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.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.select(this).selectAll('option:checked').text();
+ clearSelected.call(context);
+ context.selected_id = context.multiples.id;
+ highlightSelected.call(context);
+ smallMultiples.call(context);
+ });
+ }
+
+ 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];
+ _this.selected_id_order = _this.IDOrder.find(function (di) {
+ return di.ID === _this.selected_id;
+ }).order;
+ highlightSelected.call(_this);
+ reorderMarks.call(_this);
+ smallMultiples.call(_this);
+ });
+ }
+
+ function addPointEventListeners() {
+ var _this = this;
+
+ this.points.on('mouseover', function (d) {
+ clearHovered.call(_this);
+ _this.hovered_id = d.values.raw[0][_this.config.id_col];
+ if (_this.hovered_id !== _this.selected_id) highlightHovered.call(_this);
+ }).on('mouseout', function (d) {
+ clearHovered.call(_this);
+ }).on('click', function (d) {
+ clearHovered.call(_this);
+ clearSelected.call(_this);
+ _this.selected_id = d.values.raw[0][_this.config.id_col];
+ _this.selected_id_order = _this.IDOrder.find(function (di) {
+ return di.ID === _this.selected_id;
+ }).order;
+ highlightSelected.call(_this);
+ reorderMarks.call(_this);
+ smallMultiples.call(_this);
+ });
+ }
+
+ 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.ascending);
+ var height = this.plot_height;
+ var width = 1;
+ var domain = this.y_dom;
+ var boxPlotWidth = 10;
+ var boxColor = '#bbb';
+ var boxInsideColor = 'white';
+ var fmt = d3.format('.3r');
+
+ //set up scales
+ var x = d3.scale.linear().range([0, width]);
+ var y = d3.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.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.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.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.min(d.values) + '\n' + '5th % = ' + fmt(d3.quantile(d.values, 0.05)).replace(/^ */, '') + '\n' + 'Q1 = ' + fmt(d3.quantile(d.values, 0.25)).replace(/^ */, '') + '\n' + 'Median = ' + fmt(d3.median(d.values)).replace(/^ */, '') + '\n' + 'Q3 = ' + fmt(d3.quantile(d.values, 0.75)).replace(/^ */, '') + '\n' + '95th % = ' + fmt(d3.quantile(d.values, 0.95)).replace(/^ */, '') + '\n' + 'Max = ' + d3.max(d.values) + '\n' + 'Mean = ' + fmt(d3.mean(d.values)).replace(/^ */, '') + '\n' + 'StDev = ' + fmt(d3.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 initial ordering to points; ordering will update as points are clicked.
+ orderPoints.call(this);
+
+ //Add event listeners to lines, points, and overlay.
+ addEventListeners.call(this);
+
+ //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/scripts/build-configuration-wiki.js b/scripts/build-configuration-wiki.js
new file mode 100644
index 0000000..7aef451
--- /dev/null
+++ b/scripts/build-configuration-wiki.js
@@ -0,0 +1,120 @@
+require('babel-register');
+const fs = require('fs');
+const pkg = require('../package');
+const schema = require('../settings-schema');
+const properties = schema.properties;
+const rendererSettings = require('../src/configuration/rendererSettings.js').default;
+const webchartsSettings = require('../src/configuration/webchartsSettings.js').default();
+const markdown = [];
+
+function setDefault(setting) {
+ let settingDefault = '**default:** ';
+
+ if (setting.default === undefined && !setting.defaultObject) {
+ settingDefault = settingDefault + 'none';
+ } else if (setting.type === 'string') {
+ settingDefault = settingDefault + '`"' + setting.default + '"`';
+ } else if (['number', 'boolean'].indexOf(setting.type) > -1) {
+ settingDefault = settingDefault + '`' + setting.default + '`';
+ } else {
+ settingDefault = settingDefault +
+ '\n\`\`\`\n' +
+ JSON.stringify(setting.defaultObject, null, 2) +
+ `\n\`\`\``;
+ }
+
+ if (setting.type !== 'object')
+ return settingDefault;
+}
+
+/*------------------------------------------------------------------------------------------------\
+ Overview
+\------------------------------------------------------------------------------------------------*/
+
+ if (schema.overview)
+ schema.overview
+ .split('\n')
+ .forEach(paragraph => {
+ markdown.push(paragraph);
+ markdown.push('');
+ });
+
+/*------------------------------------------------------------------------------------------------\
+ Renderer-specific settings
+\------------------------------------------------------------------------------------------------*/
+
+ markdown.push(`# Renderer-specific settings`);
+ markdown.push(`The sections below describe each ${pkg.name} setting as of version ${schema.version}.`);
+ markdown.push(``);
+
+ var keys = Object.keys(properties);
+ keys.forEach((property,i) => {
+ var setting = properties[property];
+
+ markdown.push(`## settings.${property}`);
+ markdown.push(`\`${setting.type}\``);
+ markdown.push(``);
+ markdown.push(`${setting.description || setting.title}`);
+
+ if (setting.type !== 'object') {
+ markdown.push(``);
+ markdown.push(setDefault(setting));
+ } else {
+ var subKeys = Object.keys(setting.properties);
+ subKeys.forEach((subProperty,i) => {
+ var subSetting = setting.properties[subProperty];
+ markdown.push(``);
+ markdown.push(`### settings.${property}.${subProperty}`);
+ markdown.push(`\`${subSetting.type}\``);
+ markdown.push(``);
+ markdown.push(`${subSetting.description || subSetting.title}`);
+ markdown.push(``);
+ markdown.push(setDefault(subSetting));
+ });
+ }
+
+ if (setting.type === 'array' && setting.items.type === 'object') {
+ var subKeys = Object.keys(setting.items.properties);
+ subKeys.forEach((subProperty,i) => {
+ var subSetting = setting.items.properties[subProperty];
+ markdown.push(``);
+ markdown.push(`### settings.${property}[].${subProperty}`);
+ markdown.push(`\`${subSetting.type}\``);
+ markdown.push(``);
+ markdown.push(`${subSetting.description || subSetting.title}`);
+ markdown.push(``);
+ markdown.push(setDefault(subSetting));
+ });
+ }
+
+ if (i < keys.length - 1) {
+ markdown.push(``);
+ markdown.push(``);
+ markdown.push(``);
+ }
+ });
+
+/*------------------------------------------------------------------------------------------------\
+ Webcharts settings
+\------------------------------------------------------------------------------------------------*/
+
+ markdown.push(``);
+ markdown.push(`# Webcharts settings`);
+ markdown.push(`The object below contains Webcharts settings as of version ${schema.version} of the ${pkg.name.split('-').map(str => str.substring(0,1).toUpperCase() + str.substring(1).toLowerCase()).join(' ')}.`);
+ markdown.push(``);
+ markdown.push('```');
+ markdown.push(JSON.stringify(webchartsSettings, null, 4));
+ markdown.push('```');
+
+/*------------------------------------------------------------------------------------------------\
+ Configuration markdown
+\------------------------------------------------------------------------------------------------*/
+
+ fs.writeFile(
+ './scripts/configuration-wiki.md',
+ markdown.join('\n'),
+ (err) => {
+ if (err)
+ console.log(err);
+ console.log('The configuration wiki markdown file was built!');
+ });
diff --git a/scripts/build-data-guidelines-wiki.js b/scripts/build-data-guidelines-wiki.js
new file mode 100644
index 0000000..dd64f3d
--- /dev/null
+++ b/scripts/build-data-guidelines-wiki.js
@@ -0,0 +1,97 @@
+require('babel-register');
+const fs = require('fs');
+const pkg = require('../package');
+const schema = require('../settings-schema');
+
+//Create markdown array, one item per line.
+const markdown = [
+ schema['data-guidelines'],
+ '',
+ '## Data structure',
+ schema['data-structure'],
+ '',
+ '## Data specification',
+ 'required and optional variables:',
+ '',
+ '| Setting | Default | Data Type | Description | Required? |',
+ '|:--------|:--------|:----------|:------------|:---------:|',
+];
+
+//Add variable table to markdown array.
+const properties = schema.properties;
+const settings = Object.keys(properties);
+const variables = settings
+ .filter(setting => properties[setting]['data-mapping'])
+ .map(setting => {
+ const property = properties[setting];
+ property.setting = setting;
+
+ return property;
+ });
+variables.forEach(variable => {
+ if (['string', 'number'].indexOf(variable.type) > -1)
+ markdown.push(
+ `|\`${
+ variable.setting}\`|${
+ variable.default ? `_${variable.default}_` : ''
+ }|**${
+ variable['data-type']}**|${
+ variable.description.replace(/variable: /, '')}|${
+ variable.required ? '**Yes**' : ''
+ }|`
+ );
+ else if (variable.type === 'array') {
+ if (variable.defaults)
+ variable.defaults.forEach((item,i) => {
+ markdown.push(
+ `|\`${
+ variable.setting}[${i}]\`|_${
+ item}_|**${
+ variable['data-type']}**|${
+ variable.descriptions[item]}|${
+ variable.required ? '**Yes**' : ''
+ }|`
+ )
+ });
+ else if (variable.properties)
+ Object.keys(variable.properties).forEach(key => {
+ const property = variable.properties[key];
+ markdown.push(
+ `|\`${
+ key
+ }\`|${
+ property.default
+ }|**${
+ property['data-type']
+ }**|${
+ property.description.replace('variable: ', '')
+ }|${
+ property.required ? '**Yes**' : ''
+ }|`
+ );
+ });
+ else
+ markdown.push(
+ `|\`${
+ variable.setting}[]\`||**${
+ variable['data-type']}**|${
+ variable.description}|${
+ variable.required ? '**Yes**' : ''
+ }|`
+ );
+ } else
+ console.warn(`This wiki can't handle ${variable.type}s! Get outta here!`);
+});
+
+/*------------------------------------------------------------------------------------------------\
+ Configuration markdown
+\------------------------------------------------------------------------------------------------*/
+
+ fs.writeFile(
+ './scripts/data-guidelines-wiki.md',
+ markdown.join('\n'),
+ (err) => {
+ if (err)
+ console.log(err);
+ console.log('The data guidelines wiki markdown file was built!');
+ });
diff --git a/scripts/check-settings-schema.js b/scripts/check-settings-schema.js
new file mode 100644
index 0000000..2300714
--- /dev/null
+++ b/scripts/check-settings-schema.js
@@ -0,0 +1,29 @@
+require('babel-register');
+const fs = require('fs');
+const pkg = require('../package');
+
+//settings schema
+const schema = require('../settings-schema');
+const properties = schema.properties;
+const expectedSettings = Object.keys(properties);
+
+//renderer settings
+const settings = require('../src/configuration/rendererSettings.js').default();
+const actualSettings = Object.keys(settings);
+
+//differences
+const missingExpectedSettings = actualSettings
+ .filter(setting => expectedSettings.indexOf(setting) < 0);
+if (missingExpectedSettings.length > 0) {
+ console.log('\x1b[31m%s\x1b[0m', 'These settings are missing from the settings schema:\n');
+ console.log('\x1b[31m%s\x1b[0m', `${JSON.stringify(missingExpectedSettings, null, 4)}\n`);
+}
+const missingActualSettings = expectedSettings
+ .filter(setting => actualSettings.indexOf(setting) < 0);
+if (missingActualSettings.length > 0) {
+ console.log('\x1b[31m%s\x1b[0m', 'These settings are missing from the renderer settings:\n');
+ console.log('\x1b[31m%s\x1b[0m', `${JSON.stringify(missingActualSettings, null, 4)}\n`);
+}
+
+if (missingExpectedSettings.length === 0 && missingActualSettings.length === 0)
+ console.log('The settings schema and the renderer settings are *NSYNC!');
diff --git a/scripts/configuration-markdown.js b/scripts/configuration-markdown.js
deleted file mode 100644
index 8d44574..0000000
--- a/scripts/configuration-markdown.js
+++ /dev/null
@@ -1,132 +0,0 @@
-var pkg = require('../package'),
- schema = require('../settings-schema'),
- properties = schema.properties,
- markdown = [],
- fs = require('fs');
-
-function setDefault(setting) {
- let settingDefault = '**default:** ';
-
- if (setting.default === undefined && !setting.defaultObject) {
- settingDefault = settingDefault + 'none';
- } else if (setting.type === 'string') {
- settingDefault = settingDefault + '`"' + setting.default + '"`';
- } else if (['number', 'boolean'].indexOf(setting.type) > -1) {
- settingDefault = settingDefault + '`' + setting.default + '`';
- } else {
- settingDefault = settingDefault +
- '\n\`\`\`\n' +
- JSON.stringify(setting.defaultObject, null, 2) +
- `\n\`\`\``;
- }
-
- if (setting.type !== 'object')
- return settingDefault;
-}
-
-/*------------------------------------------------------------------------------------------------\
- Overview
-\------------------------------------------------------------------------------------------------*/
-
- if (schema.overview)
- schema.overview
- .split('\n')
- .forEach(paragraph => {
- markdown.push(paragraph);
- markdown.push('');
- });
-
-/*------------------------------------------------------------------------------------------------\
- Renderer-specific settings
-\------------------------------------------------------------------------------------------------*/
-
- markdown.push(`# Renderer-specific settings`);
- markdown.push(`The sections below describe each ${pkg.name} setting as of version ${schema.version}.`);
- markdown.push(``);
-
- var keys = Object.keys(properties);
- keys.forEach((property,i) => {
- var setting = properties[property];
-
- markdown.push(`## settings.${property}`);
- markdown.push(`\`${setting.type}\``);
- markdown.push(``);
- markdown.push(`${setting.description}`);
-
- if (setting.type !== 'object') {
- markdown.push(``);
- markdown.push(setDefault(setting));
- } else {
- var subKeys = Object.keys(setting.properties);
- subKeys.forEach((subProperty,i) => {
- var subSetting = setting.properties[subProperty];
- markdown.push(``);
- markdown.push(`### settings.${property}.${subProperty}`);
- markdown.push(`\`${subSetting.type}\``);
- markdown.push(``);
- markdown.push(`${subSetting.title}`);
- markdown.push(``);
- markdown.push(setDefault(subSetting));
- });
- }
-
- if (setting.type === 'array' && setting.items.type === 'object') {
- var subKeys = Object.keys(setting.items.properties);
- subKeys.forEach((subProperty,i) => {
- var subSetting = setting.items.properties[subProperty];
- markdown.push(``);
- markdown.push(`### settings.${property}[].${subProperty}`);
- markdown.push(`\`${subSetting.type}\``);
- markdown.push(``);
- markdown.push(`${subSetting.title}`);
- markdown.push(``);
- markdown.push(setDefault(subSetting));
- });
- }
-
- if (i < keys.length - 1) {
- markdown.push(``);
- markdown.push(``);
- markdown.push(``);
- }
- });
-
-/*------------------------------------------------------------------------------------------------\
- Webcharts settings
-\------------------------------------------------------------------------------------------------*/
-
- var webchartsSettingsFlag = 0,
- webchartsSettings = fs.readFileSync('./src/defaultSettings.js', 'utf8')
- .split('\n')
- .filter(line => {
- if (line.indexOf('const webchartsSettings') > -1)
- webchartsSettingsFlag = 1;
-
- if (webchartsSettingsFlag === 1 && /};/.test(line))
- webchartsSettingsFlag = 0;
-
- return webchartsSettingsFlag;
- });
- webchartsSettings.splice(0,1,'{\r');
- webchartsSettings.push('}');
-
- markdown.push(``);
- markdown.push(`# Webcharts settings`);
- markdown.push(`The object below contains each Webcharts setting as of version ${schema.version}.`);
- markdown.push(``);
- markdown.push('```');
- markdown.push(webchartsSettings.join(''));
- markdown.push('```');
-
-/*------------------------------------------------------------------------------------------------\
- Configuration markdown
-\------------------------------------------------------------------------------------------------*/
-
- fs.writeFile(
- './scripts/configuration.md',
- markdown.join('\n'),
- (err) => {
- if (err)
- console.log(err);
- console.log('The configuration markdown file was built!');
- });
diff --git a/scripts/configuration.md b/scripts/configuration-wiki.md
similarity index 73%
rename from scripts/configuration.md
rename to scripts/configuration-wiki.md
index 955a2ca..23b1a61 100644
--- a/scripts/configuration.md
+++ b/scripts/configuration-wiki.md
@@ -3,12 +3,12 @@ The most straightforward way to customize the Safety Outlier Explorer is by usin
In addition to the standard Webcharts settings several custom settings not available in the base Webcharts library have been added to the Safety Outlier Explorer to facilitate data mapping and other custom functionality. These custom settings are described in detail below. All defaults can be overwritten by users.
# Renderer-specific settings
-The sections below describe each safety-outlier-explorer setting as of version 2.2.2.
+The sections below describe each safety-outlier-explorer setting as of version 2.5.0.
## settings.id_col
`string`
-unique identifier variable name
+variable: participant identifier
**default:** `"USUBJID"`
@@ -97,7 +97,7 @@ Rotated tick label spacing
## settings.measure_col
`string`
-measure variable name
+variable: measure
**default:** `"TEST"`
@@ -106,7 +106,7 @@ measure variable name
## settings.unit_col
`string`
-measure unit variable name
+variable: units of measurement
**default:** `"STRESU"`
@@ -115,7 +115,7 @@ measure unit variable name
## settings.value_col
`string`
-result variable name
+variable: result
**default:** `"STRESN"`
@@ -124,7 +124,7 @@ result variable name
## settings.normal_col_low
`string`
-LLN variable name
+variable: lower limit of normal
**default:** `"STNRLO"`
@@ -133,7 +133,7 @@ LLN variable name
## settings.normal_col_high
`string`
-ULN variable name
+variable: upper limit of normal
**default:** `"STNRHI"`
@@ -174,7 +174,7 @@ Variable label
## settings.details
`array`
-an array of ID-level variables and associated metadata
+an array of participant-level variables and associated metadata
**default:**
```
@@ -294,7 +294,7 @@ a regular expression that identifies unscheduled visits
-## settings.unscheduled_visits_values
+## settings.unscheduled_visit_values
`array`
an array of strings that identify unscheduled visits; overrides unscheduled_visit_pattern
@@ -311,21 +311,21 @@ an object that defines the line color, thickness, and opacity
### settings.line_attributes.stroke
`string`
-Line Color
+the color of the line
**default:** `"black"`
### settings.line_attributes.stroke-width
`number`
-Line Thickness
+the thickness of the line
**default:** `0.5`
### settings.line_attributes.stroke-opacity
`number`
-Line Opacity
+the opacity of the line
**default:** `0.75`
@@ -339,48 +339,120 @@ an object that defines the point color, radius, and opacity and its outline colo
### settings.point_attributes.stroke
`string`
-Point Outline Color
+the color of the outline of the point
-**default:** `"rgb(102,194,165)"`
+**default:** `"#1f78b4"`
### settings.point_attributes.stroke-width
`number`
-Point Outline Thickness
+the thickness of the outline of the point
**default:** `0.5`
### settings.point_attributes.stroke-opacity
`number`
-Point Outline Opacity
+the opacity of the outline of the point
**default:** `1`
### settings.point_attributes.fill
`string`
-Point Color
+the color of the point
-**default:** `"rgb(102,194,165)"`
+**default:** `"#1f78b4"`
### settings.point_attributes.radius
`number`
-Point Radius
+the radius of the point
**default:** `3`
### settings.point_attributes.fill-opacity
`number`
-Point Opacity
+the opacity of the point
-**default:** `1`
+**default:** `0.2`
+
+
+
+## settings.custom_marks
+`array`
+
+an array of Webcharts mark definitions
+
+**default:** none
+
+
+
+## settings.tooltip_cols
+`array`
+
+an array of tooltip variables and associated metadata
+
+**default:** none
+
+### settings.tooltip_cols[].value_col
+`string`
+
+Variable name
+
+**default:** none
+
+### settings.tooltip_cols[].label
+`string`
+
+Variable label
+
+**default:** none
# Webcharts settings
-The object below contains each Webcharts setting as of version 2.2.2.
+The object below contains Webcharts settings as of version 2.5.0 of the Safety Outlier Explorer.
```
-{
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',
format: '0.2f'
},
marks: [
{
per: null, //set in syncSettings()
type: 'line',
attributes: {
'clip-path': 'url(#1)'
},
tooltip: null //set in syncSettings()
},
{
per: null, //set in syncSettings()
type: 'circle',
attributes: {
'clip-path': 'url(#1)'
},
tooltip: null //set in syncSettings()
}
],
resizable: true,
margin: { top: 5, bottom: 5, right: 20 }, //create space for box plot
aspect: 3
}
+{
+ "x": {
+ "column": null,
+ "type": null,
+ "behavior": "raw"
+ },
+ "y": {
+ "column": null,
+ "stat": "mean",
+ "type": "linear",
+ "label": "Value",
+ "behavior": "raw"
+ },
+ "marks": [
+ {
+ "per": null,
+ "type": "line",
+ "attributes": {
+ "clip-path": null
+ },
+ "tooltip": null,
+ "default": true
+ },
+ {
+ "per": null,
+ "type": "circle",
+ "attributes": {
+ "clip-path": null
+ },
+ "tooltip": null,
+ "default": true
+ }
+ ],
+ "resizable": true,
+ "margin": {
+ "right": 30,
+ "left": 60
+ },
+ "gridlines": "y",
+ "aspect": 3
+}
```
\ No newline at end of file
diff --git a/scripts/data-guidelines-wiki.md b/scripts/data-guidelines-wiki.md
new file mode 100644
index 0000000..d88c937
--- /dev/null
+++ b/scripts/data-guidelines-wiki.md
@@ -0,0 +1,22 @@
+The Safety Outlier Explorer accepts [JSON](https://en.wikipedia.org/wiki/JSON) data of the format returned by [`d3.csv()`](https://github.com/d3/d3-3.x-api-reference/blob/master/CSV.md). The renderer visualizes clinical medical signs data with **one row per participant per visit per medical sign** plus the required variables specified below.
+
+## Data structure
+one record per participant per visit per medical sign
+
+## Data specification
+required and optional variables:
+
+| Setting | Default | Data Type | Description | Required? |
+|:--------|:--------|:----------|:------------|:---------:|
+|`id_col`|_USUBJID_|**character**|participant identifier|**Yes**|
+|`visit_col`|VISIT|**character**|visit|**Yes**|
+|`visit_order_col`|VISITNUM|**numeric**|visit order||
+|`study_day_col`|DY|**numeric**|study day||
+|`measure_col`|_TEST_|**character**|measure|**Yes**|
+|`unit_col`|_STRESU_|**character**|units of measurement||
+|`value_col`|_STRESN_|**numeric**|result|**Yes**|
+|`normal_col_low`|_STNRLO_|**numeric**|lower limit of normal||
+|`normal_col_high`|_STNRHI_|**numeric**|upper limit of normal||
+|`filters[]`||**either**|an array of filter variables and associated metadata||
+|`details[]`||**either**|an array of participant-level variables and associated metadata||
+|`tooltip_cols[]`||**either**|an array of tooltip variables and associated metadata||
\ No newline at end of file
diff --git a/settings-schema.json b/settings-schema.json
index 7678224..5975afa 100644
--- a/settings-schema.json
+++ b/settings-schema.json
@@ -2,14 +2,19 @@
"title": "settings",
"description": "JSON schema for the configuration of safety-outlier-explorer",
"overview": "The most straightforward way to customize the Safety Outlier Explorer is by using a configuration object whose properties describe the behavior and appearance of the chart. Since the Safety Outlier Explorer is a Webcharts `chart` object, many default Webcharts settings are set in the [defaultSettings.js file](https://github.com/RhoInc/safety-outlier-explorer/blob/master/src/defaultSettings.js) as [described below](#webcharts-settings). Refer to the [Webcharts documentation](https://github.com/RhoInc/Webcharts/wiki/Chart-Configuration) for more details on these settings.\nIn addition to the standard Webcharts settings several custom settings not available in the base Webcharts library have been added to the Safety Outlier Explorer to facilitate data mapping and other custom functionality. These custom settings are described in detail below. All defaults can be overwritten by users.",
- "version": "2.2.2",
+ "version": "2.5.0",
"type": "object",
+ "data-guidelines": "The Safety Outlier Explorer accepts [JSON](https://en.wikipedia.org/wiki/JSON) data of the format returned by [`d3.csv()`](https://github.com/d3/d3-3.x-api-reference/blob/master/CSV.md). The renderer visualizes clinical medical signs data with **one row per participant per visit per medical sign** plus the required variables specified below.",
+ "data-structure": "one record per participant per visit per medical sign",
"properties": {
"id_col": {
"type": "string",
- "title": "ID",
- "description": "unique identifier variable name",
- "default": "USUBJID"
+ "title": "Participant ID",
+ "description": "variable: participant identifier",
+ "default": "USUBJID",
+ "data-mapping": true,
+ "data-type": "character",
+ "required": true
},
"time_cols": {
"type": "array",
@@ -77,37 +82,82 @@
"default": 100
}
}
- }
+ },
+ "properties": {
+ "visit_col": {
+ "type": "string",
+ "title": "Visit",
+ "description": "variable: visit",
+ "default": "VISIT",
+ "data-mapping": true,
+ "data-type": "character",
+ "required": true
+ },
+ "visit_order_col": {
+ "type": "string",
+ "title": "Visit Order",
+ "description": "variable: visit order",
+ "default": "VISITNUM",
+ "data-mapping": true,
+ "data-type": "numeric",
+ "required": false
+ },
+ "study_day_col": {
+ "type": "string",
+ "title": "Study Day",
+ "description": "variable: study day",
+ "default": "DY",
+ "data-mapping": true,
+ "data-type": "numeric",
+ "required": false
+ }
+ },
+ "data-mapping": true
},
"measure_col": {
"type": "string",
"title": "Measure",
- "description": "measure variable name",
- "default": "TEST"
+ "description": "variable: measure",
+ "default": "TEST",
+ "data-mapping": true,
+ "data-type": "character",
+ "required": true
},
"unit_col": {
"type": "string",
- "title": "Unit",
- "description": "measure unit variable name",
- "default": "STRESU"
+ "title": "Units",
+ "description": "variable: units of measurement",
+ "default": "STRESU",
+ "data-mapping": true,
+ "data-type": "character",
+ "required": false
},
"value_col": {
"type": "string",
"title": "Result",
- "description": "result variable name",
- "default": "STRESN"
+ "description": "variable: result",
+ "default": "STRESN",
+ "data-mapping": true,
+ "data-type": "numeric",
+ "required": true
},
"normal_col_low": {
"type": "string",
"title": "Lower Limit of Normal",
- "description": "LLN variable name",
- "default": "STNRLO"
+ "description": "variable: lower limit of normal",
+ "default": "STNRLO",
+ "data-mapping": true,
+ "data-type": "numeric",
+ "required": false
},
"normal_col_high": {
"type": "string",
"title": "Upper Limit of Normal",
- "description": "ULN variable name",
- "default": "STNRHI"
+ "description": "variable: upper limit of normal",
+ "default": "STNRHI",
+ "data-mapping": true,
+ "data-type": "numeric",
+ "required": false
},
"start_value": {
"type": "string",
@@ -130,12 +180,15 @@
"title": "Variable label"
}
}
- }
+ },
+ "data-mapping": true,
+ "data-type": "either",
+ "required": false
},
"details": {
"type": "array",
"title": "Participant details",
- "description": "an array of ID-level variables and associated metadata",
+ "description": "an array of participant-level variables and associated metadata",
"defaultObject": [
{
"value_col": "AGE",
@@ -164,7 +217,10 @@
"default": "Age"
}
}
- }
+ },
+ "data-mapping": true,
+ "data-type": "either",
+ "required": false
},
"multiples_sizing": {
"type": "object",
@@ -231,7 +287,7 @@
"description": "a regular expression that identifies unscheduled visits",
"default": "/unscheduled|early termination/i"
},
- "unscheduled_visits_values": {
+ "unscheduled_visit_values": {
"type": "array",
"title": "Unscheduled Visit List",
"description": "an array of strings that identify unscheduled visits; overrides unscheduled_visit_pattern",
@@ -273,7 +329,7 @@
"type": "string",
"title": "Point Outline Color",
"description": "the color of the outline of the point",
- "default": "rgb(102,194,165)"
+ "default": "#1f78b4"
},
"stroke-width": {
"type": "number",
@@ -291,7 +347,7 @@
"type": "string",
"title": "Point Color",
"description": "the color of the point",
- "default": "rgb(102,194,165)"
+ "default": "#1f78b4"
},
"radius": {
"type": "number",
@@ -303,9 +359,40 @@
"type": "number",
"title": "Point Opacity",
"description": "the opacity of the point",
- "default": 1
+ "default": 0.2
+ }
+ }
+ },
+ "custom_marks": {
+ "type": "array",
+ "title": "Custom Marks",
+ "description": "an array of Webcharts mark definitions",
+ "items": {
+ "type": "object",
+ "properties": {
}
}
+ },
+ "tooltip_cols": {
+ "type": "array",
+ "title": "Tooltip Variables",
+ "description": "an array of tooltip variables and associated metadata",
+ "items": {
+ "type": "object",
+ "properties": {
+ "value_col": {
+ "type": "string",
+ "title": "Variable name"
+ },
+ "label": {
+ "type": "string",
+ "title": "Variable label"
+ }
+ }
+ },
+ "data-mapping": true,
+ "data-type": "either",
+ "required": false
}
}
}
diff --git a/src/callbacks/index.js b/src/callbacks/index.js
new file mode 100644
index 0000000..5d0865e
--- /dev/null
+++ b/src/callbacks/index.js
@@ -0,0 +1,17 @@
+import onInit from './onInit';
+import onLayout from './onLayout';
+import onPreprocess from './onPreprocess';
+import onDatatransform from './onDatatransform';
+import onDraw from './onDraw';
+import onResize from './onResize';
+import onDestroy from './onDestroy';
+
+export default {
+ onInit,
+ onLayout,
+ onPreprocess,
+ onDatatransform,
+ onDraw,
+ onResize,
+ onDestroy
+};
diff --git a/src/onDatatransform.js b/src/callbacks/onDatatransform.js
similarity index 100%
rename from src/onDatatransform.js
rename to src/callbacks/onDatatransform.js
diff --git a/src/callbacks/onDestroy.js b/src/callbacks/onDestroy.js
new file mode 100644
index 0000000..67f91a1
--- /dev/null
+++ b/src/callbacks/onDestroy.js
@@ -0,0 +1 @@
+export default function onDestroy() {}
diff --git a/src/onDraw.js b/src/callbacks/onDraw.js
similarity index 67%
rename from src/onDraw.js
rename to src/callbacks/onDraw.js
index 6389632..6b5053b 100644
--- a/src/onDraw.js
+++ b/src/callbacks/onDraw.js
@@ -1,14 +1,18 @@
import updateParticipantCount from './onDraw/updateParticipantCount';
import resetChart from './onDraw/resetChart';
+import extendYDomain from './onDraw/extendYDomain';
import updateBottomMargin from './onDraw/updateBottomMargin';
export default function onDraw() {
//Annotate participant count.
- updateParticipantCount(this, '#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);
}
diff --git a/src/callbacks/onDraw/extendYDomain.js b/src/callbacks/onDraw/extendYDomain.js
new file mode 100644
index 0000000..597a191
--- /dev/null
+++ b/src/callbacks/onDraw/extendYDomain.js
@@ -0,0 +1,11 @@
+export default 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
+ ];
+}
diff --git a/src/onDraw/resetChart.js b/src/callbacks/onDraw/resetChart.js
similarity index 100%
rename from src/onDraw/resetChart.js
rename to src/callbacks/onDraw/resetChart.js
diff --git a/src/onDraw/updateBottomMargin.js b/src/callbacks/onDraw/updateBottomMargin.js
similarity index 100%
rename from src/onDraw/updateBottomMargin.js
rename to src/callbacks/onDraw/updateBottomMargin.js
diff --git a/src/callbacks/onDraw/updateParticipantCount.js b/src/callbacks/onDraw/updateParticipantCount.js
new file mode 100644
index 0000000..6180758
--- /dev/null
+++ b/src/callbacks/onDraw/updateParticipantCount.js
@@ -0,0 +1,21 @@
+import { set, format, select } from 'd3';
+
+export default function updateParticipantCount() {
+ //count the number of unique ids in the current chart and calculate the percentage
+ this.participantCount.n = set(
+ this.filtered_data.map(d => d[this.config.id_col])
+ ).values().length;
+ this.participantCount.percentage = 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
+ })`
+ );
+}
diff --git a/src/callbacks/onInit.js b/src/callbacks/onInit.js
new file mode 100644
index 0000000..eef7572
--- /dev/null
+++ b/src/callbacks/onInit.js
@@ -0,0 +1,22 @@
+import countParticipants from './onInit/countParticipants';
+import cleanData from './onInit/cleanData';
+import addVariables from './onInit/addVariables';
+import defineSets from './onInit/defineSets';
+import checkControls from './onInit/checkControls';
+
+export default 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);
+}
diff --git a/src/onInit/addVariables.js b/src/callbacks/onInit/addVariables.js
similarity index 68%
rename from src/onInit/addVariables.js
rename to src/callbacks/onInit/addVariables.js
index fd0b179..2f81a5f 100644
--- a/src/onInit/addVariables.js
+++ b/src/callbacks/onInit/addVariables.js
@@ -2,14 +2,10 @@ export default function addVariables() {
const ordinalTimeSettings = this.config.time_cols.find(time_col => time_col.type === 'ordinal');
this.raw_data.forEach(d => {
- //Append units to measure.
- d.measure_unit = d[this.config.measure_col];
- if (
- this.config.unit_col &&
- d.hasOwnProperty(this.config.unit_col) &&
- d[this.config.unit_col] !== ''
- )
- d.measure_unit = `${d.measure_unit} (${d[this.config.unit_col]})`;
+ //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;
diff --git a/src/callbacks/onInit/checkControls.js b/src/callbacks/onInit/checkControls.js
new file mode 100644
index 0000000..84ad0d1
--- /dev/null
+++ b/src/callbacks/onInit/checkControls.js
@@ -0,0 +1,9 @@
+import updateMeasureFilter from './checkControls/updateMeasureFilter';
+import removeFilters from './checkControls/removeFilters';
+import updateNormalRangeControl from './checkControls/updateNormalRangeControl';
+
+export default function checkControls() {
+ updateMeasureFilter.call(this);
+ removeFilters.call(this);
+ updateNormalRangeControl.call(this);
+}
diff --git a/src/onInit/checkFilters.js b/src/callbacks/onInit/checkControls/removeFilters.js
similarity index 86%
rename from src/onInit/checkFilters.js
rename to src/callbacks/onInit/checkControls/removeFilters.js
index 0613841..f495cef 100644
--- a/src/onInit/checkFilters.js
+++ b/src/callbacks/onInit/checkControls/removeFilters.js
@@ -1,8 +1,8 @@
import { set } from 'd3';
-export default function checkFilters() {
+export default function removeFilters() {
this.controls.config.inputs = this.controls.config.inputs.filter(input => {
- if (input.type !== 'subsetter') {
+ if (input.type !== 'subsetter' || input.value_col === 'soe_measure') {
return true;
} else if (!this.raw_data[0].hasOwnProperty(input.value_col)) {
console.warn(
diff --git a/src/callbacks/onInit/checkControls/updateMeasureFilter.js b/src/callbacks/onInit/checkControls/updateMeasureFilter.js
new file mode 100644
index 0000000..63abc93
--- /dev/null
+++ b/src/callbacks/onInit/checkControls/updateMeasureFilter.js
@@ -0,0 +1,21 @@
+export default function updateMeasureFilter() {
+ this.measure = {};
+ const measureInput = this.controls.config.inputs.find(input => 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];
+}
diff --git a/src/onInit/updateControlInputs.js b/src/callbacks/onInit/checkControls/updateNormalRangeControl.js
similarity index 91%
rename from src/onInit/updateControlInputs.js
rename to src/callbacks/onInit/checkControls/updateNormalRangeControl.js
index e1cb071..0b72343 100644
--- a/src/onInit/updateControlInputs.js
+++ b/src/callbacks/onInit/checkControls/updateNormalRangeControl.js
@@ -1,4 +1,4 @@
-export default function updateControlInputs() {
+export default 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 ||
diff --git a/src/callbacks/onInit/cleanData.js b/src/callbacks/onInit/cleanData.js
new file mode 100644
index 0000000..d46189e
--- /dev/null
+++ b/src/callbacks/onInit/cleanData.js
@@ -0,0 +1,14 @@
+import removeMissingResults from './cleanData/removeMissingResults';
+import removeNonNumericResults from './cleanData/removeNonNumericResults';
+
+export default 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;
+}
diff --git a/src/callbacks/onInit/cleanData/removeMissingResults.js b/src/callbacks/onInit/cleanData/removeMissingResults.js
new file mode 100644
index 0000000..efb258f
--- /dev/null
+++ b/src/callbacks/onInit/cleanData/removeMissingResults.js
@@ -0,0 +1,55 @@
+import { nest, sum } from 'd3';
+
+export default function removeMissingResults() {
+ //Split data into records with missing and nonmissing results.
+ const missingResults = [];
+ const nonMissingResults = [];
+ this.raw_data.forEach(d => {
+ if (/^\s*$/.test(d[this.config.value_col])) missingResults.push(d);
+ else nonMissingResults.push(d);
+ });
+
+ //Nest missing and nonmissing results by participant.
+ const participantsWithMissingResults = nest()
+ .key(d => d[this.config.id_col])
+ .rollup(d => d.length)
+ .entries(missingResults);
+ const participantsWithNonMissingResults = nest()
+ .key(d => d[this.config.id_col])
+ .rollup(d => d.length)
+ .entries(nonMissingResults);
+
+ //Identify placeholder records, i.e. participants with a single missing result.
+ this.removedRecords.placeholderRecords = participantsWithMissingResults
+ .filter(
+ d =>
+ participantsWithNonMissingResults.map(d => d.key).indexOf(d.key) < 0 &&
+ d.values === 1
+ )
+ .map(d => 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 = sum(
+ participantsWithMissingResults.filter(
+ d => this.removedRecords.placeholderRecords.indexOf(d.key) < 0
+ ),
+ d => 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;
+}
diff --git a/src/callbacks/onInit/cleanData/removeNonNumericResults.js b/src/callbacks/onInit/cleanData/removeNonNumericResults.js
new file mode 100644
index 0000000..62f8b2c
--- /dev/null
+++ b/src/callbacks/onInit/cleanData/removeNonNumericResults.js
@@ -0,0 +1,16 @@
+export default function removeNonNumericResults() {
+ //Filter out non-numeric results.
+ const numericResults = this.raw_data.filter(d => /^-?[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;
+}
diff --git a/src/callbacks/onInit/countParticipants.js b/src/callbacks/onInit/countParticipants.js
new file mode 100644
index 0000000..dbe9e9b
--- /dev/null
+++ b/src/callbacks/onInit/countParticipants.js
@@ -0,0 +1,12 @@
+import { set } from 'd3';
+
+export default function countParticipants() {
+ this.participantCount = {
+ N: set(this.raw_data.map(d => d[this.config.id_col]))
+ .values()
+ .filter(value => !/^\s*$/.test(value)).length,
+ container: null, // set in ../onLayout/addParticipantCountContainer
+ n: null, // set in ../onDraw/updateParticipantCount
+ percentage: null // set in ../onDraw/updateParticipantCount
+ };
+}
diff --git a/src/callbacks/onInit/defineSets.js b/src/callbacks/onInit/defineSets.js
new file mode 100644
index 0000000..0cd3c1e
--- /dev/null
+++ b/src/callbacks/onInit/defineSets.js
@@ -0,0 +1,9 @@
+import participant from './defineSets/participant';
+import visit from './defineSets/visit';
+import measure from './defineSets/measure';
+
+export default function defineSets() {
+ participant.call(this);
+ visit.call(this);
+ measure.call(this);
+}
diff --git a/src/callbacks/onInit/defineSets/measure.js b/src/callbacks/onInit/defineSets/measure.js
new file mode 100644
index 0000000..79acac8
--- /dev/null
+++ b/src/callbacks/onInit/defineSets/measure.js
@@ -0,0 +1,10 @@
+import { set } from 'd3';
+
+export default function measure() {
+ this.measures = set(this.initial_data.map(d => d[this.config.measure_col]))
+ .values()
+ .sort();
+ this.soe_measures = set(this.initial_data.map(d => d.soe_measure))
+ .values()
+ .sort();
+}
diff --git a/src/onInit/attachIDOrdering.js b/src/callbacks/onInit/defineSets/participant.js
similarity index 85%
rename from src/onInit/attachIDOrdering.js
rename to src/callbacks/onInit/defineSets/participant.js
index a107eba..37c21a3 100644
--- a/src/onInit/attachIDOrdering.js
+++ b/src/callbacks/onInit/defineSets/participant.js
@@ -1,5 +1,6 @@
import { set } from 'd3';
-export default function addIDOrdering() {
+
+export default function participant() {
this.IDOrder = set(this.raw_data.map(d => d[this.config.id_col]))
.values()
.sort()
diff --git a/src/onInit/defineVisitOrder.js b/src/callbacks/onInit/defineSets/visit.js
similarity index 97%
rename from src/onInit/defineVisitOrder.js
rename to src/callbacks/onInit/defineSets/visit.js
index 1f83e49..072fac8 100644
--- a/src/onInit/defineVisitOrder.js
+++ b/src/callbacks/onInit/defineSets/visit.js
@@ -1,6 +1,6 @@
import { set, ascending } from 'd3';
-export default function defineVisitOrder() {
+export default function visit() {
//ordinal
this.config.time_cols.filter(time_col => time_col.type === 'ordinal').forEach(time_settings => {
let visits, visitOrder;
diff --git a/src/onLayout.js b/src/callbacks/onLayout.js
similarity index 56%
rename from src/onLayout.js
rename to src/callbacks/onLayout.js
index 05904c2..d638cb1 100644
--- a/src/onLayout.js
+++ b/src/callbacks/onLayout.js
@@ -4,27 +4,18 @@ import addYdomainResetButton from './onLayout/addYdomainResetButton';
import groupControls from './onLayout/groupControls';
import hideNormalRangeInputs from './onLayout/hideNormalRangeInputs';
import addParticipantCountContainer from './onLayout/addParticipantCountContainer';
+import addRemovedRecordsContainer from './onLayout/addRemovedRecordsContainer';
+import addBorderAboveChart from './onLayout/addBorderAboveChart';
import addSmallMultiplesContainer from './onLayout/addSmallMultiplesContainer';
export default function onLayout() {
- // Distinguish controls to insert y-axis reset button in the correct position.
- identifyControls.call(this);
-
- //Label x-axis options.
+ identifyControls.call(this); // Distinguish controls to insert y-axis reset button in the correct position.
labelXaxisOptions.call(this);
-
- //Add a button to reset the y-domain
addYdomainResetButton.call(this);
-
- //Group related controls visually.
- groupControls.call(this);
-
- //Hide normal range input controls depending on the normal range method.
- hideNormalRangeInputs.call(this);
-
- //Add participant count container.
+ groupControls.call(this); // Group related controls visually.
+ hideNormalRangeInputs.call(this); // Hide normal range input controls depending on the normal range method.
addParticipantCountContainer.call(this);
-
- //Add container for small multiples.
+ addRemovedRecordsContainer.call(this);
+ addBorderAboveChart.call(this);
addSmallMultiplesContainer.call(this);
}
diff --git a/src/callbacks/onLayout/addBorderAboveChart.js b/src/callbacks/onLayout/addBorderAboveChart.js
new file mode 100644
index 0000000..da0bc07
--- /dev/null
+++ b/src/callbacks/onLayout/addBorderAboveChart.js
@@ -0,0 +1,5 @@
+export default function addBorderAboveChart() {
+ this.wrap.style({
+ 'border-top': '1px solid #ccc'
+ });
+}
diff --git a/src/callbacks/onLayout/addParticipantCountContainer.js b/src/callbacks/onLayout/addParticipantCountContainer.js
new file mode 100644
index 0000000..8fb963e
--- /dev/null
+++ b/src/callbacks/onLayout/addParticipantCountContainer.js
@@ -0,0 +1,12 @@
+export default 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
+ });
+}
diff --git a/src/callbacks/onLayout/addRemovedRecordsContainer.js b/src/callbacks/onLayout/addRemovedRecordsContainer.js
new file mode 100644
index 0000000..bbb85e5
--- /dev/null
+++ b/src/callbacks/onLayout/addRemovedRecordsContainer.js
@@ -0,0 +1,46 @@
+export default function addRemovedRecordsNote() {
+ if (this.removedRecords.missing > 0 || this.removedRecords.nonNumeric > 0) {
+ const 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', () => this.removedRecords.container.style('display', 'none'));
+ }
+}
diff --git a/src/callbacks/onLayout/addSmallMultiplesContainer.js b/src/callbacks/onLayout/addSmallMultiplesContainer.js
new file mode 100644
index 0000000..df8467d
--- /dev/null
+++ b/src/callbacks/onLayout/addSmallMultiplesContainer.js
@@ -0,0 +1,12 @@
+export default function addSmallMultiplesContainer() {
+ this.multiples = {
+ container: this.wrap
+ .append('div')
+ .classed('multiples', true)
+ .style({
+ 'border-top': '1px solid #ccc',
+ 'padding-top': '10px'
+ }),
+ id: null
+ };
+}
diff --git a/src/onLayout/addYdomainResetButton.js b/src/callbacks/onLayout/addYdomainResetButton.js
similarity index 62%
rename from src/onLayout/addYdomainResetButton.js
rename to src/callbacks/onLayout/addYdomainResetButton.js
index a558b1f..fe43fa4 100644
--- a/src/onLayout/addYdomainResetButton.js
+++ b/src/callbacks/onLayout/addYdomainResetButton.js
@@ -20,19 +20,6 @@ export default function addYdomainResetButton() {
.style('padding', '0px 5px')
.on('click', () => {
this.config.y.domain = this.measure.domain; //reset axis to full range
-
- this.controls.wrap
- .selectAll('.control-group')
- .filter(f => f.option === 'y.domain[0]')
- .select('input')
- .property('value', this.config.y.domain[0]);
-
- this.controls.wrap
- .selectAll('.control-group')
- .filter(f => f.option === 'y.domain[1]')
- .select('input')
- .property('value', this.config.y.domain[1]);
-
this.draw();
});
}
diff --git a/src/onLayout/groupControls.js b/src/callbacks/onLayout/groupControls.js
similarity index 100%
rename from src/onLayout/groupControls.js
rename to src/callbacks/onLayout/groupControls.js
diff --git a/src/onLayout/groupControls/insertGrouping.js b/src/callbacks/onLayout/groupControls/insertGrouping.js
similarity index 100%
rename from src/onLayout/groupControls/insertGrouping.js
rename to src/callbacks/onLayout/groupControls/insertGrouping.js
diff --git a/src/onLayout/hideNormalRangeInputs.js b/src/callbacks/onLayout/hideNormalRangeInputs.js
similarity index 100%
rename from src/onLayout/hideNormalRangeInputs.js
rename to src/callbacks/onLayout/hideNormalRangeInputs.js
diff --git a/src/onLayout/identifyControls.js b/src/callbacks/onLayout/identifyControls.js
similarity index 89%
rename from src/onLayout/identifyControls.js
rename to src/callbacks/onLayout/identifyControls.js
index 90e3d8c..4e16f03 100644
--- a/src/onLayout/identifyControls.js
+++ b/src/callbacks/onLayout/identifyControls.js
@@ -1,7 +1,9 @@
import { select } from 'd3';
export default function identifyControls() {
- const controlGroups = this.controls.wrap.selectAll('.control-group');
+ const controlGroups = this.controls.wrap
+ .style('padding-bottom', '8px')
+ .selectAll('.control-group');
//Give each control a unique ID.
controlGroups.attr('id', d => d.label.toLowerCase().replace(' ', '-')).each(function(d) {
diff --git a/src/onLayout/labelXaxisOptions.js b/src/callbacks/onLayout/labelXaxisOptions.js
similarity index 100%
rename from src/onLayout/labelXaxisOptions.js
rename to src/callbacks/onLayout/labelXaxisOptions.js
diff --git a/src/onPreprocess.js b/src/callbacks/onPreprocess.js
similarity index 89%
rename from src/onPreprocess.js
rename to src/callbacks/onPreprocess.js
index 7a682db..9f6e9db 100644
--- a/src/onPreprocess.js
+++ b/src/callbacks/onPreprocess.js
@@ -2,6 +2,7 @@ import getCurrentMeasure from './onPreprocess/getCurrentMeasure';
import defineMeasureData from './onPreprocess/defineMeasureData';
import setXdomain from './onPreprocess/setXdomain';
import setYdomain from './onPreprocess/setYdomain';
+import calculateYPrecision from './onPreprocess/calculateYPrecision';
import updateYaxisLimitControls from './onPreprocess/updateYaxisLimitControls';
import setYaxisLabel from './onPreprocess/setYaxisLabel';
import updateYaxisResetButton from './onPreprocess/updateYaxisResetButton';
@@ -20,6 +21,9 @@ export default function onPreprocess() {
// 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);
diff --git a/src/callbacks/onPreprocess/calculateYPrecision.js b/src/callbacks/onPreprocess/calculateYPrecision.js
new file mode 100644
index 0000000..0d2e90d
--- /dev/null
+++ b/src/callbacks/onPreprocess/calculateYPrecision.js
@@ -0,0 +1,21 @@
+export default 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
+ let step = this.measure.range / 15;
+ if (step < 1) {
+ let 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;
+}
diff --git a/src/onPreprocess/defineMeasureData.js b/src/callbacks/onPreprocess/defineMeasureData.js
similarity index 79%
rename from src/onPreprocess/defineMeasureData.js
rename to src/callbacks/onPreprocess/defineMeasureData.js
index c4e6c42..0cdaec3 100644
--- a/src/onPreprocess/defineMeasureData.js
+++ b/src/callbacks/onPreprocess/defineMeasureData.js
@@ -1,7 +1,7 @@
import { extent } from 'd3';
export default function defineMeasureData() {
- this.measure.data = this.initial_data.filter(d => d.measure_unit === this.measure.current);
+ this.measure.data = this.initial_data.filter(d => 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]
@@ -11,5 +11,6 @@ export default function defineMeasureData() {
.sort((a, b) => a - b);
this.measure.domain = 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(d => this.config.unscheduled_visits || !d.unscheduled);
}
diff --git a/src/onPreprocess/deriveStatistics.js b/src/callbacks/onPreprocess/deriveStatistics.js
similarity index 100%
rename from src/onPreprocess/deriveStatistics.js
rename to src/callbacks/onPreprocess/deriveStatistics.js
diff --git a/src/onPreprocess/getCurrentMeasure.js b/src/callbacks/onPreprocess/getCurrentMeasure.js
similarity index 77%
rename from src/onPreprocess/getCurrentMeasure.js
rename to src/callbacks/onPreprocess/getCurrentMeasure.js
index 1667618..362a97a 100644
--- a/src/onPreprocess/getCurrentMeasure.js
+++ b/src/callbacks/onPreprocess/getCurrentMeasure.js
@@ -2,7 +2,7 @@ export default function getCurrentMeasure() {
this.measure.previous = this.measure.current;
this.measure.current = this.controls.wrap
.selectAll('.control-group')
- .filter(d => d.value_col && d.value_col === 'measure_unit')
+ .filter(d => d.value_col && d.value_col === 'soe_measure')
.select('option:checked')
.text();
}
diff --git a/src/onPreprocess/setXdomain.js b/src/callbacks/onPreprocess/setXdomain.js
similarity index 100%
rename from src/onPreprocess/setXdomain.js
rename to src/callbacks/onPreprocess/setXdomain.js
diff --git a/src/onPreprocess/setXdomain/removeUnscheduledVisits.js b/src/callbacks/onPreprocess/setXdomain/removeUnscheduledVisits.js
similarity index 100%
rename from src/onPreprocess/setXdomain/removeUnscheduledVisits.js
rename to src/callbacks/onPreprocess/setXdomain/removeUnscheduledVisits.js
diff --git a/src/onPreprocess/setXdomain/removeVisitsWithoutData.js b/src/callbacks/onPreprocess/setXdomain/removeVisitsWithoutData.js
similarity index 100%
rename from src/onPreprocess/setXdomain/removeVisitsWithoutData.js
rename to src/callbacks/onPreprocess/setXdomain/removeVisitsWithoutData.js
diff --git a/src/onPreprocess/setYaxisLabel.js b/src/callbacks/onPreprocess/setYaxisLabel.js
similarity index 100%
rename from src/onPreprocess/setYaxisLabel.js
rename to src/callbacks/onPreprocess/setYaxisLabel.js
diff --git a/src/callbacks/onPreprocess/setYdomain.js b/src/callbacks/onPreprocess/setYdomain.js
new file mode 100644
index 0000000..c45326f
--- /dev/null
+++ b/src/callbacks/onPreprocess/setYdomain.js
@@ -0,0 +1,6 @@
+export default 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
+}
diff --git a/src/callbacks/onPreprocess/updateYaxisLimitControls.js b/src/callbacks/onPreprocess/updateYaxisLimitControls.js
new file mode 100644
index 0000000..506d98a
--- /dev/null
+++ b/src/callbacks/onPreprocess/updateYaxisLimitControls.js
@@ -0,0 +1,17 @@
+export default function updateYaxisLimitControls() {
+ this.controls.wrap
+ .selectAll('.control-group')
+ .filter(f => 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(f => 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]);
+}
diff --git a/src/onPreprocess/updateYaxisResetButton.js b/src/callbacks/onPreprocess/updateYaxisResetButton.js
similarity index 100%
rename from src/onPreprocess/updateYaxisResetButton.js
rename to src/callbacks/onPreprocess/updateYaxisResetButton.js
diff --git a/src/onResize.js b/src/callbacks/onResize.js
similarity index 100%
rename from src/onResize.js
rename to src/callbacks/onResize.js
diff --git a/src/onResize/addBoxPlot.js b/src/callbacks/onResize/addBoxPlot.js
similarity index 79%
rename from src/onResize/addBoxPlot.js
rename to src/callbacks/onResize/addBoxPlot.js
index 93dcdc0..5457c89 100644
--- a/src/onResize/addBoxPlot.js
+++ b/src/callbacks/onResize/addBoxPlot.js
@@ -13,7 +13,7 @@ export default function addBoxPlot() {
const boxPlotWidth = 10;
const boxColor = '#bbb';
const boxInsideColor = 'white';
- const fmt = format('.2f');
+ const fmt = format('.3r');
const horizontal = true;
//set up scales
@@ -112,40 +112,37 @@ export default function addBoxPlot() {
.style('fill', boxColor)
.style('stroke', 'None');
- boxplot
- .selectAll('.boxplot')
- .append('title')
- .text(function(d) {
- return (
- 'N = ' +
- d.values.length +
- '\n' +
- 'Min = ' +
- min(d.values) +
- '\n' +
- '5th % = ' +
- fmt(quantile(d.values, 0.05)) +
- '\n' +
- 'Q1 = ' +
- fmt(quantile(d.values, 0.25)) +
- '\n' +
- 'Median = ' +
- fmt(median(d.values)) +
- '\n' +
- 'Q3 = ' +
- fmt(quantile(d.values, 0.75)) +
- '\n' +
- '95th % = ' +
- fmt(quantile(d.values, 0.95)) +
- '\n' +
- 'Max = ' +
- max(d.values) +
- '\n' +
- 'Mean = ' +
- fmt(mean(d.values)) +
- '\n' +
- 'StDev = ' +
- fmt(deviation(d.values))
- );
- });
+ boxplot.append('title').text(function(d) {
+ const tooltip =
+ 'N = ' +
+ d.values.length +
+ '\n' +
+ 'Min = ' +
+ min(d.values) +
+ '\n' +
+ '5th % = ' +
+ fmt(quantile(d.values, 0.05)).replace(/^ */, '') +
+ '\n' +
+ 'Q1 = ' +
+ fmt(quantile(d.values, 0.25)).replace(/^ */, '') +
+ '\n' +
+ 'Median = ' +
+ fmt(median(d.values)).replace(/^ */, '') +
+ '\n' +
+ 'Q3 = ' +
+ fmt(quantile(d.values, 0.75)).replace(/^ */, '') +
+ '\n' +
+ '95th % = ' +
+ fmt(quantile(d.values, 0.95)).replace(/^ */, '') +
+ '\n' +
+ 'Max = ' +
+ max(d.values) +
+ '\n' +
+ 'Mean = ' +
+ fmt(mean(d.values)).replace(/^ */, '') +
+ '\n' +
+ 'StDev = ' +
+ fmt(deviation(d.values)).replace(/^ */, '');
+ return tooltip;
+ });
}
diff --git a/src/onResize/addEventListeners.js b/src/callbacks/onResize/addEventListeners.js
similarity index 100%
rename from src/onResize/addEventListeners.js
rename to src/callbacks/onResize/addEventListeners.js
diff --git a/src/onResize/addEventListeners/addLineEventListeners.js b/src/callbacks/onResize/addEventListeners/addLineEventListeners.js
similarity index 100%
rename from src/onResize/addEventListeners/addLineEventListeners.js
rename to src/callbacks/onResize/addEventListeners/addLineEventListeners.js
diff --git a/src/onResize/addEventListeners/addNormalRangeEventListener.js b/src/callbacks/onResize/addEventListeners/addNormalRangeEventListener.js
similarity index 100%
rename from src/onResize/addEventListeners/addNormalRangeEventListener.js
rename to src/callbacks/onResize/addEventListeners/addNormalRangeEventListener.js
diff --git a/src/onResize/addEventListeners/addOverlayEventListener.js b/src/callbacks/onResize/addEventListeners/addOverlayEventListener.js
similarity index 100%
rename from src/onResize/addEventListeners/addOverlayEventListener.js
rename to src/callbacks/onResize/addEventListeners/addOverlayEventListener.js
diff --git a/src/onResize/addEventListeners/addPointEventListeners.js b/src/callbacks/onResize/addEventListeners/addPointEventListeners.js
similarity index 100%
rename from src/onResize/addEventListeners/addPointEventListeners.js
rename to src/callbacks/onResize/addEventListeners/addPointEventListeners.js
diff --git a/src/onResize/addEventListeners/functions/clearHovered.js b/src/callbacks/onResize/addEventListeners/functions/clearHovered.js
similarity index 63%
rename from src/onResize/addEventListeners/functions/clearHovered.js
rename to src/callbacks/onResize/addEventListeners/functions/clearHovered.js
index d9beae5..6a5e24a 100644
--- a/src/onResize/addEventListeners/functions/clearHovered.js
+++ b/src/callbacks/onResize/addEventListeners/functions/clearHovered.js
@@ -6,13 +6,17 @@ export default function clearHovered() {
return !select(this).classed('selected');
})
.select('path')
- .attr(this.config.line_attributes);
+ .each(function(d) {
+ select(this).attr(d.attributes);
+ });
this.points
.filter(function() {
return !select(this).classed('selected');
})
.select('circle')
- .attr(this.config.point_attributes)
- .attr('r', this.config.marks.find(mark => mark.type === 'circle').radius);
+ .each(function(d) {
+ select(this).attr(d.attributes);
+ select(this).attr('r', d.radius);
+ });
delete this.hovered_id;
}
diff --git a/src/onResize/addEventListeners/functions/clearSelected.js b/src/callbacks/onResize/addEventListeners/functions/clearSelected.js
similarity index 73%
rename from src/onResize/addEventListeners/functions/clearSelected.js
rename to src/callbacks/onResize/addEventListeners/functions/clearSelected.js
index 47acad8..8756615 100644
--- a/src/onResize/addEventListeners/functions/clearSelected.js
+++ b/src/callbacks/onResize/addEventListeners/functions/clearSelected.js
@@ -1,11 +1,10 @@
export default function clearSelected() {
this.marks.forEach(mark => {
- const type = mark.type === 'circle' ? 'point' : mark.type;
const element = mark.type === 'line' ? 'path' : mark.type;
mark.groups
.classed('selected', false)
.select(element)
- .attr(this.config[`${type}_attributes`]);
+ .attr(mark.attributes);
});
if (this.multiples.chart) this.multiples.chart.destroy();
delete this.selected_id;
diff --git a/src/onResize/addEventListeners/functions/highlightHovered.js b/src/callbacks/onResize/addEventListeners/functions/highlightHovered.js
similarity index 66%
rename from src/onResize/addEventListeners/functions/highlightHovered.js
rename to src/callbacks/onResize/addEventListeners/functions/highlightHovered.js
index 86045e5..b071e69 100644
--- a/src/onResize/addEventListeners/functions/highlightHovered.js
+++ b/src/callbacks/onResize/addEventListeners/functions/highlightHovered.js
@@ -3,15 +3,15 @@ export default function highlightHovered() {
this.lines
.filter(d => d.values[0].values.raw[0][this.config.id_col] === this.hovered_id)
.select('path')
- .attr('stroke-width', this.config.line_attributes['stroke-width'] * 4);
+ .attr('stroke-width', d => d.attributes['stroke-width'] * 4);
//Update attributes of hovered points.
this.points
.filter(d => d.values.raw[0][this.config.id_col] === this.hovered_id)
.select('circle')
.attr({
- r: this.config.point_attributes.radius * 1.25,
+ r: d => d.radius * 1.25,
stroke: 'black',
- 'stroke-width': this.config.point_attributes['stroke-width'] * 4
+ 'stroke-width': d => d.attributes['stroke-width'] * 4
});
}
diff --git a/src/onResize/addEventListeners/functions/highlightSelected.js b/src/callbacks/onResize/addEventListeners/functions/highlightSelected.js
similarity index 79%
rename from src/onResize/addEventListeners/functions/highlightSelected.js
rename to src/callbacks/onResize/addEventListeners/functions/highlightSelected.js
index fe0a4a3..6915a3d 100644
--- a/src/onResize/addEventListeners/functions/highlightSelected.js
+++ b/src/callbacks/onResize/addEventListeners/functions/highlightSelected.js
@@ -14,15 +14,15 @@ export default function highlightSelected() {
this.lines
.filter(d => d.values[0].values.raw[0][this.config.id_col] === this.selected_id)
.select('path')
- .attr('stroke-width', this.config.line_attributes['stroke-width'] * 8);
+ .attr('stroke-width', d => d.attributes['stroke-width'] * 8);
//Update attributes of selected points.
this.points
.filter(d => d.values.raw[0][this.config.id_col] === this.selected_id)
.select('circle')
.attr({
- r: this.config.point_attributes.radius * 1.5,
+ r: d => d.radius * 1.5,
stroke: 'black',
- 'stroke-width': this.config.point_attributes['stroke-width'] * 8
+ 'stroke-width': d => d.attributes['stroke-width'] * 8
});
}
diff --git a/src/onResize/addEventListeners/functions/reorderMarks.js b/src/callbacks/onResize/addEventListeners/functions/reorderMarks.js
similarity index 100%
rename from src/onResize/addEventListeners/functions/reorderMarks.js
rename to src/callbacks/onResize/addEventListeners/functions/reorderMarks.js
diff --git a/src/onResize/addEventListeners/functions/smallMultiples.js b/src/callbacks/onResize/addEventListeners/functions/smallMultiples.js
similarity index 92%
rename from src/onResize/addEventListeners/functions/smallMultiples.js
rename to src/callbacks/onResize/addEventListeners/functions/smallMultiples.js
index 3868777..3dcf37a 100644
--- a/src/onResize/addEventListeners/functions/smallMultiples.js
+++ b/src/callbacks/onResize/addEventListeners/functions/smallMultiples.js
@@ -22,7 +22,7 @@ export default function smallMultiples() {
onResize.call(this);
//Initialize small multiples.
- multiply(this.multiples.chart, this.multiples.data, 'measure_unit', this.measures);
+ multiply(this.multiples.chart, this.multiples.data, 'soe_measure', this.measures);
//Update participant dropdown.
updateParticipantDropdown.call(this);
diff --git a/src/onResize/addEventListeners/functions/smallMultiples/callbacks/onLayout.js b/src/callbacks/onResize/addEventListeners/functions/smallMultiples/callbacks/onLayout.js
similarity index 100%
rename from src/onResize/addEventListeners/functions/smallMultiples/callbacks/onLayout.js
rename to src/callbacks/onResize/addEventListeners/functions/smallMultiples/callbacks/onLayout.js
diff --git a/src/onResize/addEventListeners/functions/smallMultiples/callbacks/onLayout/updateParticipantDropdown.js b/src/callbacks/onResize/addEventListeners/functions/smallMultiples/callbacks/onLayout/updateParticipantDropdown.js
similarity index 100%
rename from src/onResize/addEventListeners/functions/smallMultiples/callbacks/onLayout/updateParticipantDropdown.js
rename to src/callbacks/onResize/addEventListeners/functions/smallMultiples/callbacks/onLayout/updateParticipantDropdown.js
diff --git a/src/onResize/addEventListeners/functions/smallMultiples/callbacks/onPreprocess.js b/src/callbacks/onResize/addEventListeners/functions/smallMultiples/callbacks/onPreprocess.js
similarity index 100%
rename from src/onResize/addEventListeners/functions/smallMultiples/callbacks/onPreprocess.js
rename to src/callbacks/onResize/addEventListeners/functions/smallMultiples/callbacks/onPreprocess.js
diff --git a/src/onResize/addEventListeners/functions/smallMultiples/callbacks/onResize.js b/src/callbacks/onResize/addEventListeners/functions/smallMultiples/callbacks/onResize.js
similarity index 100%
rename from src/onResize/addEventListeners/functions/smallMultiples/callbacks/onResize.js
rename to src/callbacks/onResize/addEventListeners/functions/smallMultiples/callbacks/onResize.js
diff --git a/src/onResize/addEventListeners/functions/smallMultiples/callbacks/onResize/rangePolygon.js b/src/callbacks/onResize/addEventListeners/functions/smallMultiples/callbacks/onResize/rangePolygon.js
similarity index 100%
rename from src/onResize/addEventListeners/functions/smallMultiples/callbacks/onResize/rangePolygon.js
rename to src/callbacks/onResize/addEventListeners/functions/smallMultiples/callbacks/onResize/rangePolygon.js
diff --git a/src/onResize/addEventListeners/functions/smallMultiples/defineSmallMultiples.js b/src/callbacks/onResize/addEventListeners/functions/smallMultiples/defineSmallMultiples.js
similarity index 97%
rename from src/onResize/addEventListeners/functions/smallMultiples/defineSmallMultiples.js
rename to src/callbacks/onResize/addEventListeners/functions/smallMultiples/defineSmallMultiples.js
index bc9b808..530a7e1 100644
--- a/src/onResize/addEventListeners/functions/smallMultiples/defineSmallMultiples.js
+++ b/src/callbacks/onResize/addEventListeners/functions/smallMultiples/defineSmallMultiples.js
@@ -1,4 +1,4 @@
-import clone from '../../../../util/clone';
+import clone from '../../../../../util/clone';
import { createControls } from 'webcharts';
import { createChart } from 'webcharts';
diff --git a/src/onResize/addEventListeners/functions/smallMultiples/participantCharacteristics.js b/src/callbacks/onResize/addEventListeners/functions/smallMultiples/participantCharacteristics.js
similarity index 100%
rename from src/onResize/addEventListeners/functions/smallMultiples/participantCharacteristics.js
rename to src/callbacks/onResize/addEventListeners/functions/smallMultiples/participantCharacteristics.js
diff --git a/src/onResize/addEventListeners/functions/smallMultiples/updateParticipantDropdown.js b/src/callbacks/onResize/addEventListeners/functions/smallMultiples/updateParticipantDropdown.js
similarity index 92%
rename from src/onResize/addEventListeners/functions/smallMultiples/updateParticipantDropdown.js
rename to src/callbacks/onResize/addEventListeners/functions/smallMultiples/updateParticipantDropdown.js
index 256fc97..6aae72b 100644
--- a/src/onResize/addEventListeners/functions/smallMultiples/updateParticipantDropdown.js
+++ b/src/callbacks/onResize/addEventListeners/functions/smallMultiples/updateParticipantDropdown.js
@@ -9,6 +9,7 @@ export default function updateParticipantDropdown() {
const participantDropdown = this.multiples.controls.wrap
.style('margin', 0)
.selectAll('.control-group')
+ .filter(d => d.option === 'selected_id')
.style('margin', 0)
.style('display', 'block'); // firefox is being weird about inline-table
participantDropdown.selectAll('*').style('display', 'inline-block');
@@ -19,8 +20,7 @@ export default function updateParticipantDropdown() {
.style('width', null)
.style('max-width', '10%')
.on('change', function(d) {
- context.multiples.id = d3
- .select(this)
+ context.multiples.id = select(this)
.selectAll('option:checked')
.text();
clearSelected.call(context);
diff --git a/src/onResize/adjustTicks.js b/src/callbacks/onResize/adjustTicks.js
similarity index 100%
rename from src/onResize/adjustTicks.js
rename to src/callbacks/onResize/adjustTicks.js
diff --git a/src/callbacks/onResize/attachMarks.js b/src/callbacks/onResize/attachMarks.js
new file mode 100644
index 0000000..76567b2
--- /dev/null
+++ b/src/callbacks/onResize/attachMarks.js
@@ -0,0 +1,10 @@
+export default function attachMarks() {
+ this.marks.forEach(mark => {
+ mark.groups.each(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');
+}
diff --git a/src/onResize/drawNormalRange.js b/src/callbacks/onResize/drawNormalRange.js
similarity index 100%
rename from src/onResize/drawNormalRange.js
rename to src/callbacks/onResize/drawNormalRange.js
diff --git a/src/onResize/maintainHighlight.js b/src/callbacks/onResize/maintainHighlight.js
similarity index 100%
rename from src/onResize/maintainHighlight.js
rename to src/callbacks/onResize/maintainHighlight.js
diff --git a/src/callbacks/onResize/orderPoints.js b/src/callbacks/onResize/orderPoints.js
new file mode 100644
index 0000000..3206321
--- /dev/null
+++ b/src/callbacks/onResize/orderPoints.js
@@ -0,0 +1,7 @@
+export default function orderPoints() {
+ this.marks.filter(mark => mark.type === 'circle').forEach(mark => {
+ mark.groups.each((d, i) => {
+ d.order = this.IDOrder.find(di => d.key.indexOf(di.ID) === 0).order;
+ });
+ });
+}
diff --git a/src/configuration/controlInputs.js b/src/configuration/controlInputs.js
new file mode 100644
index 0000000..714563c
--- /dev/null
+++ b/src/configuration/controlInputs.js
@@ -0,0 +1,62 @@
+export default function controlInputs() {
+ return [
+ {
+ type: 'subsetter',
+ value_col: 'soe_measure', // set in syncControlInputs()
+ label: 'Measure',
+ start: null
+ },
+ {
+ type: 'dropdown',
+ option: 'x.column',
+ label: 'X-axis',
+ require: true
+ },
+ {
+ type: 'number',
+ option: 'y.domain[0]',
+ label: 'Lower',
+ require: true
+ },
+ {
+ type: 'number',
+ option: 'y.domain[1]',
+ label: 'Upper',
+ require: true
+ },
+ {
+ type: 'dropdown',
+ option: 'normal_range_method',
+ label: 'Method',
+ values: ['None', 'LLN-ULN', 'Standard Deviation', 'Quantiles'],
+ require: true
+ },
+ {
+ type: 'number',
+ option: 'normal_range_sd',
+ label: '# Std. Dev.'
+ },
+ {
+ type: 'number',
+ label: 'Lower',
+ option: 'normal_range_quantile_low'
+ },
+ {
+ type: 'number',
+ label: 'Upper',
+ option: 'normal_range_quantile_high'
+ },
+ {
+ type: 'checkbox',
+ inline: true,
+ option: 'visits_without_data',
+ label: 'Without Data'
+ },
+ {
+ type: 'checkbox',
+ inline: true,
+ option: 'unscheduled_visits',
+ label: 'Unscheduled'
+ }
+ ];
+}
diff --git a/src/configuration/index.js b/src/configuration/index.js
new file mode 100644
index 0000000..bdc9f95
--- /dev/null
+++ b/src/configuration/index.js
@@ -0,0 +1,14 @@
+import rendererSettings from './rendererSettings';
+import webchartsSettings from './webchartsSettings';
+import syncSettings from './syncSettings';
+import controlInputs from './controlInputs';
+import syncControlInputs from './syncControlInputs';
+
+export default {
+ rendererSettings,
+ webchartsSettings,
+ settings: Object.assign({}, rendererSettings(), webchartsSettings()),
+ syncSettings,
+ controlInputs,
+ syncControlInputs
+};
diff --git a/src/configuration/rendererSettings.js b/src/configuration/rendererSettings.js
new file mode 100644
index 0000000..456ae8f
--- /dev/null
+++ b/src/configuration/rendererSettings.js
@@ -0,0 +1,79 @@
+export default function rendererSettings() {
+ return {
+ //participant
+ id_col: 'USUBJID',
+ details: [
+ { value_col: 'AGE', label: 'Age' },
+ { value_col: 'SEX', label: 'Sex' },
+ { value_col: 'RACE', label: 'Race' }
+ ],
+
+ //timing
+ time_cols: [
+ {
+ type: 'ordinal',
+ value_col: 'VISIT',
+ label: 'Visit',
+ order_col: 'VISITNUM',
+ order: null,
+ rotate_tick_labels: true,
+ vertical_space: 100
+ },
+ {
+ type: 'linear',
+ value_col: 'DY',
+ label: 'Study Day',
+ order_col: 'DY',
+ order: null,
+ rotate_tick_labels: false,
+ vertical_space: 0
+ }
+ ],
+ visits_without_data: false,
+ unscheduled_visits: false,
+ unscheduled_visit_pattern: '/unscheduled|early termination/i',
+ unscheduled_visit_values: null, // takes precedence over unscheduled_visit_pattern
+
+ //measure
+ measure_col: 'TEST',
+ start_value: null,
+ unit_col: 'STRESU',
+
+ //result
+ value_col: 'STRESN',
+
+ //normal range
+ normal_col_low: 'STNRLO',
+ normal_col_high: 'STNRHI',
+ normal_range_method: 'LLN-ULN',
+ normal_range_sd: 1.96,
+ normal_range_quantile_low: 0.05,
+ normal_range_quantile_high: 0.95,
+
+ //filters
+ filters: null,
+
+ //marks
+ line_attributes: {
+ stroke: 'black',
+ 'stroke-width': 0.5,
+ 'stroke-opacity': 0.75
+ },
+ point_attributes: {
+ stroke: '#1f78b4',
+ 'stroke-width': 0.5,
+ 'stroke-opacity': 1,
+ radius: 3,
+ fill: '#1f78b4',
+ 'fill-opacity': 0.2
+ },
+ tooltip_cols: null,
+ custom_marks: null,
+
+ //multiples
+ multiples_sizing: {
+ width: 300,
+ height: 100
+ }
+ };
+}
diff --git a/src/configuration/syncControlInputs.js b/src/configuration/syncControlInputs.js
new file mode 100644
index 0000000..93e2126
--- /dev/null
+++ b/src/configuration/syncControlInputs.js
@@ -0,0 +1,35 @@
+export default function syncControlInputs(controlInputs, settings) {
+ const xAxisControl = controlInputs.find(d => d.label === 'X-axis');
+ xAxisControl.values = settings.time_cols.map(d => d.value_col);
+
+ if (settings.filters) {
+ settings.filters.forEach(function(d, i) {
+ const thisFilter = {
+ type: 'subsetter',
+ value_col: d.value_col ? d.value_col : d,
+ label: d.label ? d.label : d.value_col ? d.value_col : d
+ };
+ //add the filter to the control inputs (as long as it isn't already there)
+ var current_value_cols = controlInputs
+ .filter(f => f.type == 'subsetter')
+ .map(m => m.value_col);
+ if (current_value_cols.indexOf(thisFilter.value_col) == -1)
+ controlInputs.splice(4 + i, 0, thisFilter);
+ });
+ }
+
+ //Remove unscheduled visit control if unscheduled visit pattern is unscpecified.
+ if (
+ !settings.unscheduled_visit_regex &&
+ !(
+ Array.isArray(settings.unscheduled_visit_values) &&
+ settings.unscheduled_visit_values.length
+ )
+ )
+ controlInputs.splice(
+ controlInputs.map(controlInput => controlInput.label).indexOf('Unscheduled Visits'),
+ 1
+ );
+
+ return controlInputs;
+}
diff --git a/src/configuration/syncSettings.js b/src/configuration/syncSettings.js
new file mode 100644
index 0000000..eced49b
--- /dev/null
+++ b/src/configuration/syncSettings.js
@@ -0,0 +1,77 @@
+export default function syncSettings(settings) {
+ const time_col = settings.time_cols[0];
+
+ //x-axis
+ settings.x.column = time_col.value_col;
+ settings.x.type = time_col.type;
+ settings.x.label = time_col.label;
+ settings.x.order = time_col.order;
+
+ //y-axis
+ settings.y.column = settings.value_col;
+
+ //lines
+ const lines = settings.marks.find(mark => mark.type === 'line');
+ lines.per = [settings.id_col, settings.measure_col];
+ lines.tooltip = `[${settings.id_col}]`;
+ Object.assign(lines.attributes, settings.line_attributes);
+ lines.attributes['stroke-width'] = settings.line_attributes['stroke-width'] || 0.5;
+
+ //points
+ const points = settings.marks.find(mark => mark.type === 'circle');
+ points.per = [settings.id_col, settings.measure_col, time_col.value_col, settings.value_col];
+ points.tooltip = `Participant = [${settings.id_col}]\n[${settings.measure_col}] = [${
+ settings.value_col
+ }] [${settings.unit_col}]\n${settings.x.label} = [${settings.x.column}]`;
+
+ //Conadd custom tooltip values
+ if (settings.tooltip_cols) {
+ settings.tooltip_cols.forEach(function(tooltip) {
+ var obj = typeof tooltip == 'string' ? { label: tooltip, value_col: tooltip } : tooltip;
+ points.tooltip = points.tooltip + `\n${obj.label} = [${obj.value_col}]`;
+ });
+ }
+
+ Object.assign(points.attributes, settings.point_attributes);
+ points.radius = settings.point_attributes.radius || 3;
+
+ //Add custom marks to settings.marks.
+ if (Array.isArray(settings.custom_marks) && settings.custom_marks.length)
+ settings.custom_marks.forEach(mark => {
+ if (mark instanceof Object) {
+ mark.default = false; // distinguish custom marks from default marks
+ if (mark.type === 'line')
+ mark.attributes = Object.assign({}, lines.attributes, mark.attributes);
+ else if (mark.type === 'circle') {
+ mark.attributes = Object.assign({}, points.attributes, mark.attributes);
+ mark.radius = mark.radius || points.radius;
+ }
+ settings.marks.push(mark);
+ }
+ });
+
+ //Define margins for box plot and rotated x-axis tick labels.
+ if (settings.margin) settings.margin.bottom = time_col.vertical_space;
+ else
+ settings.margin = {
+ right: 20,
+ bottom: time_col.vertical_space
+ };
+
+ settings.rotate_x_tick_labels = time_col.rotate_tick_labels;
+
+ //Convert unscheduled_visit_pattern from string to regular expression.
+ if (
+ typeof settings.unscheduled_visit_pattern === 'string' &&
+ settings.unscheduled_visit_pattern !== ''
+ ) {
+ const flags = settings.unscheduled_visit_pattern.replace(/.*?\/([gimy]*)$/, '$1'),
+ pattern = settings.unscheduled_visit_pattern.replace(
+ new RegExp('^/(.*?)/' + flags + '$'),
+ '$1'
+ );
+ settings.unscheduled_visit_regex = new RegExp(pattern, flags);
+ }
+
+ return settings;
+}
diff --git a/src/configuration/webchartsSettings.js b/src/configuration/webchartsSettings.js
new file mode 100644
index 0000000..b0882d9
--- /dev/null
+++ b/src/configuration/webchartsSettings.js
@@ -0,0 +1,43 @@
+export default function webchartsSettings() {
+ return {
+ x: {
+ column: null, // set in ./syncSettings
+ type: null, // set in ./syncSettings
+ behavior: 'raw'
+ },
+ y: {
+ column: null, // set in ./syncSettings
+ stat: 'mean',
+ type: 'linear',
+ label: 'Value',
+ behavior: 'raw'
+ },
+ marks: [
+ {
+ per: null, // set in ./syncSettings
+ type: 'line',
+ attributes: {
+ 'clip-path': null // set in ./syncSettings
+ },
+ tooltip: null, // set in ./syncSettings
+ default: true
+ },
+ {
+ per: null, // set in ./syncSettings
+ type: 'circle',
+ attributes: {
+ 'clip-path': null // set in ./syncSettings
+ },
+ tooltip: null, // set in ./syncSettings
+ default: true
+ }
+ ],
+ resizable: true,
+ margin: {
+ right: 30, // create space for box plot
+ left: 60
+ },
+ gridlines: 'y',
+ aspect: 3
+ };
+}
diff --git a/src/defaultSettings.js b/src/defaultSettings.js
deleted file mode 100644
index 3d335d1..0000000
--- a/src/defaultSettings.js
+++ /dev/null
@@ -1,266 +0,0 @@
-export const rendererSpecificSettings = {
- id_col: 'USUBJID',
- 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
- }
- ],
- measure_col: 'TEST',
- unit_col: 'STRESU',
- value_col: 'STRESN',
- normal_col_low: 'STNRLO',
- normal_col_high: 'STNRHI',
- start_value: null,
- filters: null,
- custom_marks: null,
- details: [
- { value_col: 'AGE', label: 'Age' },
- { value_col: 'SEX', label: 'Sex' },
- { value_col: 'RACE', label: 'Race' }
- ],
- tooltip_cols: null,
- multiples_sizing: {
- width: 300,
- height: 100
- },
- 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
- 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
- }
-};
-
-export const webchartsSettings = {
- 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',
- format: '0.2f'
- },
- marks: [
- {
- per: null, //set in syncSettings()
- type: 'line',
- attributes: {
- 'clip-path': 'url(#1)'
- },
- tooltip: null //set in syncSettings()
- },
- {
- per: null, //set in syncSettings()
- type: 'circle',
- attributes: {
- 'clip-path': 'url(#1)'
- },
- tooltip: null //set in syncSettings()
- }
- ],
- resizable: true,
- margin: { top: 5, bottom: 5, right: 20 }, //create space for box plot
- aspect: 3
-};
-
-export default Object.assign({}, rendererSpecificSettings, webchartsSettings);
-
-// Replicate settings in multiple places in the settings object
-export function syncSettings(settings) {
- const time_col = settings.time_cols[0];
-
- //x-axis
- settings.x.column = time_col.value_col;
- settings.x.type = time_col.type;
- settings.x.label = time_col.label;
- settings.x.order = time_col.order;
-
- //y-axis
- settings.y.column = settings.value_col;
-
- //lines
- const lines = settings.marks.find(mark => mark.type === 'line');
- lines.per = [settings.id_col, settings.measure_col];
- lines.tooltip = `[${settings.id_col}]`;
- Object.assign(lines.attributes, settings.line_attributes);
- lines.attributes['stroke-width'] = settings.line_attributes['stroke-width'] || 0.5;
-
- //points
- const points = settings.marks.find(mark => mark.type === 'circle');
- points.per = [settings.id_col, settings.measure_col, time_col.value_col, settings.value_col];
- points.tooltip = `ID = [${settings.id_col}]\n[${settings.measure_col}] = [${
- settings.value_col
- }] [${settings.unit_col}]\n${settings.x.column} = [${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 (settings.custom_marks) settings.custom_marks.forEach(mark => settings.marks.push(mark));
-
- //Define margins for box plot and rotated x-axis tick labels.
- if (settings.margin) settings.margin.bottom = time_col.vertical_space;
- else
- settings.margin = {
- right: 20,
- bottom: time_col.vertical_space
- };
-
- settings.rotate_x_tick_labels = time_col.rotate_tick_labels;
-
- //Convert unscheduled_visit_pattern from string to regular expression.
- if (
- typeof settings.unscheduled_visit_pattern === 'string' &&
- settings.unscheduled_visit_pattern !== ''
- ) {
- const flags = settings.unscheduled_visit_pattern.replace(/.*?\/([gimy]*)$/, '$1'),
- pattern = settings.unscheduled_visit_pattern.replace(
- new RegExp('^/(.*?)/' + flags + '$'),
- '$1'
- );
- settings.unscheduled_visit_regex = new RegExp(pattern, flags);
- }
-
- return settings;
-}
-
-// Default Control objects
-export const controlInputs = [
- {
- type: 'subsetter',
- value_col: 'measure_unit', // 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'
- }
-];
-
-// Map values from settings to control inputs
-export function syncControlInputs(controlInputs, settings) {
- const xAxisControl = controlInputs.find(d => d.label === 'X-axis');
- xAxisControl.values = settings.time_cols.map(d => d.value_col);
-
- if (settings.filters) {
- settings.filters.forEach(function(d, i) {
- const thisFilter = {
- type: 'subsetter',
- value_col: d.value_col ? d.value_col : d,
- label: d.label ? d.label : d.value_col ? d.value_col : d
- };
- //add the filter to the control inputs (as long as it isn't already there)
- var current_value_cols = controlInputs
- .filter(f => f.type == 'subsetter')
- .map(m => m.value_col);
- if (current_value_cols.indexOf(thisFilter.value_col) == -1)
- controlInputs.splice(4 + i, 0, thisFilter);
- });
- }
-
- //Remove unscheduled visit control if unscheduled visit pattern is unscpecified.
- if (
- !settings.unscheduled_visit_regex &&
- !(
- Array.isArray(settings.unscheduled_visit_values) &&
- settings.unscheduled_visit_values.length
- )
- )
- controlInputs.splice(
- controlInputs.map(controlInput => controlInput.label).indexOf('Unscheduled Visits'),
- 1
- );
-
- return controlInputs;
-}
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..704640c
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,38 @@
+import './util/polyfills';
+import './util/moveTo';
+import configuration from './configuration/index';
+import { createChart, createControls } from 'webcharts';
+import callbacks from './callbacks/index';
+
+export default function safetyOutlierExplorer(element, settings) {
+ //Merge user settings with default settings.
+ const mergedSettings = Object.assign({}, configuration.settings, settings);
+
+ //Sync options within settings object, e.g. data mappings.
+ const syncedSettings = configuration.syncSettings(mergedSettings);
+
+ //Sync control inputs with with settings object.
+ const syncedControlInputs = configuration.syncControlInputs(
+ configuration.controlInputs(),
+ syncedSettings
+ );
+
+ //Define controls.
+ const controls = createControls(element, {
+ location: 'top',
+ inputs: syncedControlInputs
+ });
+
+ //Define chart.
+ const chart = createChart(element, syncedSettings, controls);
+ chart.config.marks.forEach(mark => {
+ mark.attributes = mark.attributes || {};
+ mark.attributes['clip-path'] = `url(#${chart.id})`;
+ });
+
+ //Attach callbacks to chart.
+ for (const callback in callbacks)
+ chart.on(callback.substring(2).toLowerCase(), callbacks[callback]);
+
+ return chart;
+}
diff --git a/src/onDraw/updateParticipantCount.js b/src/onDraw/updateParticipantCount.js
deleted file mode 100644
index f90a083..0000000
--- a/src/onDraw/updateParticipantCount.js
+++ /dev/null
@@ -1,30 +0,0 @@
-// Takes a webcharts object creates a text annotation giving the
-// number and percentage of observations shown in the current view
-//
-// inputs:
-// - chart - a webcharts chart object
-// - selector - css selector for the annotation
-// - id_unit - a text string to label the units in the annotation (default = "participants")
-import { set, format, select } from 'd3';
-
-export default function updateParticipantCount(chart, selector, id_unit) {
- //count the number of unique ids in the current chart and calculate the percentage
- var currentObs = set(
- chart.filtered_data.map(function(d) {
- return d[chart.config.id_col];
- })
- ).values().length;
- var percentage = format('0.1%')(currentObs / chart.populationCount);
-
- //clear the annotation
- var annotation = select(selector);
- select(selector)
- .selectAll('*')
- .remove();
-
- //update the annotation
- var units = id_unit ? ' ' + id_unit : ' participant(s)';
- annotation.text(
- '\n' + currentObs + ' of ' + chart.populationCount + units + ' shown (' + percentage + ')'
- );
-}
diff --git a/src/onInit.js b/src/onInit.js
deleted file mode 100644
index a8ca29c..0000000
--- a/src/onInit.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import countParticipants from './onInit/countParticipants';
-import cleanData from './onInit/cleanData';
-import addVariables from './onInit/addVariables';
-import captureMeasures from './onInit/captureMeasures';
-import defineVisitOrder from './onInit/defineVisitOrder';
-import updateControlInputs from './onInit/updateControlInputs';
-import checkFilters from './onInit/checkFilters';
-import setInitialMeasure from './onInit/setInitialMeasure';
-import attachIDOrdering from './onInit/attachIDOrdering';
-
-export default 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 Capture unique set of measures.
- captureMeasures.call(this);
-
- // 3c Define ordered x-axis domain with visit order variable.
- defineVisitOrder.call(this);
-
- // 3d Remove invalid control inputs.
- updateControlInputs.call(this);
-
- // 3e Remove filters for nonexistent or single-level variables.
- checkFilters.call(this);
-
- // 3f Choose the start value for the Test filter
- setInitialMeasure.call(this);
-
- // 3g Capture unique set of IDs and apply an ordering.
- attachIDOrdering.call(this);
-}
diff --git a/src/onInit/captureMeasures.js b/src/onInit/captureMeasures.js
deleted file mode 100644
index 85610ea..0000000
--- a/src/onInit/captureMeasures.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { set } from 'd3';
-
-export default function captureMeasures() {
- this.measures = set(this.initial_data.map(d => d.measure_unit))
- .values()
- .sort();
-}
diff --git a/src/onInit/cleanData.js b/src/onInit/cleanData.js
deleted file mode 100644
index 3490c8c..0000000
--- a/src/onInit/cleanData.js
+++ /dev/null
@@ -1,18 +0,0 @@
-export default function cleanData() {
- //Remove missing and non-numeric data.
- const preclean = this.raw_data;
- const clean = this.raw_data.filter(d => /^-?[0-9.]+$/.test(d[this.config.value_col]));
- const nPreclean = preclean.length;
- const nClean = clean.length;
- const 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;
-}
diff --git a/src/onInit/countParticipants.js b/src/onInit/countParticipants.js
deleted file mode 100644
index f6714a7..0000000
--- a/src/onInit/countParticipants.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { set } from 'd3';
-
-export default function countParticipants() {
- this.populationCount = set(this.raw_data.map(d => d[this.config.id_col])).values().length;
-}
diff --git a/src/onInit/setInitialMeasure.js b/src/onInit/setInitialMeasure.js
deleted file mode 100644
index cb3a4df..0000000
--- a/src/onInit/setInitialMeasure.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export default function setInitialMeasure() {
- this.measure = {};
- this.controls.config.inputs.find(input => input.label === 'Measure').start =
- this.config.start_value || this.measures[0];
-}
diff --git a/src/onLayout/addParticipantCountContainer.js b/src/onLayout/addParticipantCountContainer.js
deleted file mode 100644
index 66b99ea..0000000
--- a/src/onLayout/addParticipantCountContainer.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default function addParticipantCountContainer() {
- this.controls.wrap
- .append('div')
- .attr('id', 'participant-count')
- .style('font-style', 'italic');
-}
diff --git a/src/onLayout/addSmallMultiplesContainer.js b/src/onLayout/addSmallMultiplesContainer.js
deleted file mode 100644
index 8333c82..0000000
--- a/src/onLayout/addSmallMultiplesContainer.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default function addSmallMultiplesContainer() {
- this.multiples = {
- container: this.wrap.append('div').classed('multiples', true),
- id: null
- };
-}
diff --git a/src/onPreprocess/setYdomain.js b/src/onPreprocess/setYdomain.js
deleted file mode 100644
index 3901085..0000000
--- a/src/onPreprocess/setYdomain.js
+++ /dev/null
@@ -1,12 +0,0 @@
-export default function setYdomain() {
- //Define y-domain.
- 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])
- // 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(
- (d, i) => (i === 0 ? d - d * 0.01 : d + d * 0.01)
- ); // domain with zero range
-}
diff --git a/src/onPreprocess/updateYaxisLimitControls.js b/src/onPreprocess/updateYaxisLimitControls.js
deleted file mode 100644
index 7e357d5..0000000
--- a/src/onPreprocess/updateYaxisLimitControls.js
+++ /dev/null
@@ -1,16 +0,0 @@
-export default function updateYaxisLimitControls() {
- //Update y-axis limit controls.
- this.controls.wrap
- .selectAll('.control-group')
- .filter(f => f.option === 'y.domain[0]')
- .select('input')
- .property('value', this.config.y.domain[0])
- .style('box-shadow', 'none');
-
- this.controls.wrap
- .selectAll('.control-group')
- .filter(f => f.option === 'y.domain[1]')
- .select('input')
- .property('value', this.config.y.domain[1])
- .style('box-shadow', 'none');
-}
diff --git a/src/onResize/attachMarks.js b/src/onResize/attachMarks.js
deleted file mode 100644
index 4942e29..0000000
--- a/src/onResize/attachMarks.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default function attachMarks() {
- this.marks.forEach(mark => {
- const type = mark.type === 'circle' ? 'point' : mark.type;
- this[`${type}s`] = mark.groups;
- });
-}
diff --git a/src/onResize/orderPoints.js b/src/onResize/orderPoints.js
deleted file mode 100644
index 8a8fae9..0000000
--- a/src/onResize/orderPoints.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export default function orderPoints() {
- this.marks.find(mark => mark.type === 'circle').groups.each((d, i) => {
- d.order = this.IDOrder.find(di => d.key.indexOf(di.ID) === 0).order;
- });
-}
diff --git a/src/util/polyfills.js b/src/util/polyfills.js
index b886d2a..32225f3 100644
--- a/src/util/polyfills.js
+++ b/src/util/polyfills.js
@@ -119,3 +119,9 @@ if (!Array.prototype.findIndex) {
}
});
}
+
+Math.log10 =
+ Math.log10 ||
+ function(x) {
+ return Math.log(x) * Math.LOG10E;
+ };
diff --git a/src/wrapper.js b/src/wrapper.js
deleted file mode 100644
index 21d386d..0000000
--- a/src/wrapper.js
+++ /dev/null
@@ -1,42 +0,0 @@
-//polyfills
-import './util/polyfills';
-import './util/moveTo';
-
-//settings
-import defaultSettings from './defaultSettings';
-import { controlInputs, syncControlInputs, syncSettings } from './defaultSettings';
-
-//webcharts
-import { createChart, createControls } from 'webcharts';
-import onInit from './onInit';
-import onLayout from './onLayout';
-import onPreprocess from './onPreprocess';
-import onDatatransform from './onDatatransform';
-import onDraw from './onDraw';
-import onResize from './onResize';
-
-export default function safetyOutlierExplorer(element, settings) {
- //Merge user settings with default settings.
- const mergedSettings = Object.assign({}, defaultSettings, settings);
-
- //Sync options within settings object, e.g. data mappings.
- const syncedSettings = syncSettings(mergedSettings);
-
- //Sync control inputs with with settings object.
- const syncedControlInputs = syncControlInputs(controlInputs, syncedSettings);
- const controls = createControls(element, {
- location: 'top',
- inputs: syncedControlInputs
- });
-
- //Create chart.
- const chart = 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);
-
- return chart;
-}
diff --git a/test-page/index.html b/test-page/index.html
index 0b422f7..6857b98 100644
--- a/test-page/index.html
+++ b/test-page/index.html
@@ -6,7 +6,7 @@
-
+
diff --git a/test-page/index.js b/test-page/index.js
index 78bfb32..6a24c42 100644
--- a/test-page/index.js
+++ b/test-page/index.js
@@ -1,5 +1,6 @@
d3.csv(
- 'https://rawgit.com/RhoInc/viz-library/master/data/safetyData/ADBDS.csv',
+ 'https://raw.githubusercontent.com/RhoInc/data-library/master/data/clinical-trials/renderer-specific/adbds.csv',
+ //'../../data-library/data/clinical-trials/renderer-specific/adbds.csv',
function(d,i) {
return d;
},
@@ -8,7 +9,35 @@ d3.csv(
console.log(error);
var settings = {
- tooltip_cols: [{label:"Date",value_col:"DT"}],
+ filters: [
+ {
+ label: 'Treatment Group',
+ value_col: 'ARM'
+ },
+ ],
+ tooltip_cols: [
+ {
+ label: 'Date',
+ value_col: 'DT'
+ }
+ ],
+ custom_marks: [
+ {
+ per: ['USUBJID', 'VISIT', 'TEST', 'STRESN'],
+ type: 'circle',
+ attributes: {
+ fill: 'red',
+ 'fill-opacity': 1,
+ stroke: 'black',
+ 'stroke-opacity': 1,
+ },
+ radius: 4,
+ tooltip: '[USUBJID] is right on schedule at [VISIT] (Study day [DY]).',
+ values: {
+ DY: ['56', '112', '168', '224', '280', '336']
+ },
+ }
+ ],
};
var instance = safetyOutlierExplorer(
'#container',