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/build/queryOverview.js b/build/queryOverview.js index e54fd94..ef5a7ac 100644 --- a/build/queryOverview.js +++ b/build/queryOverview.js @@ -1,13 +1,12 @@ (function(global, factory) { typeof exports === 'object' && typeof module !== 'undefined' - ? (module.exports = factory(require('d3'), require('webcharts'))) + ? (module.exports = factory(require('webcharts'))) : typeof define === 'function' && define.amd - ? define(['d3', 'webcharts'], factory) - : (global.queryOverview = factory(global.d3, global.webCharts)); -})(this, function(d3$1, webcharts) { + ? define(['webcharts'], factory) + : (global.queryOverview = factory(global.webCharts)); +})(this, function(webcharts) { 'use strict'; - //[].find if (!Array.prototype.find) { Object.defineProperty(Array.prototype, 'find', { value: function value(predicate) { @@ -52,13 +51,9 @@ }); } - //{}.assign if (typeof Object.assign != 'function') { Object.defineProperty(Object, 'assign', { value: function assign(target, varArgs) { - // .length of function is 2 - 'use strict'; - if (target == null) { // TypeError if undefined or null throw new TypeError('Cannot convert undefined or null to object'); @@ -87,7 +82,6 @@ }); } - //[].findIndex if (!Array.prototype.findIndex) { Object.defineProperty(Array.prototype, 'findIndex', { value: function value(predicate) { @@ -132,10 +126,8 @@ }); } - d3.selection.prototype.moveToFront = function() { - return this.each(function() { - this.parentNode.appendChild(this); - }); + Array.prototype.flatMap = function(lambda) { + return Array.prototype.concat.apply([], this.map(lambda)); }; d3.selection.prototype.moveToBack = function() { @@ -145,6 +137,12 @@ }); }; + d3.selection.prototype.moveToFront = function() { + return this.each(function() { + this.parentNode.appendChild(this); + }); + }; + var _typeof = typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' ? function(obj) { @@ -159,122 +157,6 @@ : typeof obj; }; - var asyncGenerator = (function() { - function AwaitValue(value) { - this.value = value; - } - - function AsyncGenerator(gen) { - var front, back; - - function send(key, arg) { - return new Promise(function(resolve, reject) { - var request = { - key: key, - arg: arg, - resolve: resolve, - reject: reject, - next: null - }; - - if (back) { - back = back.next = request; - } else { - front = back = request; - resume(key, arg); - } - }); - } - - function resume(key, arg) { - try { - var result = gen[key](arg); - var value = result.value; - - if (value instanceof AwaitValue) { - Promise.resolve(value.value).then( - function(arg) { - resume('next', arg); - }, - function(arg) { - resume('throw', arg); - } - ); - } else { - settle(result.done ? 'return' : 'normal', result.value); - } - } catch (err) { - settle('throw', err); - } - } - - function settle(type, value) { - switch (type) { - case 'return': - front.resolve({ - value: value, - done: true - }); - break; - - case 'throw': - front.reject(value); - break; - - default: - front.resolve({ - value: value, - done: false - }); - break; - } - - front = front.next; - - if (front) { - resume(front.key, front.arg); - } else { - back = null; - } - } - - this._invoke = send; - - if (typeof gen.return !== 'function') { - this.return = undefined; - } - } - - if (typeof Symbol === 'function' && Symbol.asyncIterator) { - AsyncGenerator.prototype[Symbol.asyncIterator] = function() { - return this; - }; - } - - AsyncGenerator.prototype.next = function(arg) { - return this._invoke('next', arg); - }; - - AsyncGenerator.prototype.throw = function(arg) { - return this._invoke('throw', arg); - }; - - AsyncGenerator.prototype.return = function(arg) { - return this._invoke('return', arg); - }; - - return { - wrap: function(fn) { - return function() { - return new AsyncGenerator(fn.apply(this, arguments)); - }; - }, - await: function(value) { - return new AwaitValue(value); - } - }; - })(); - function clone(obj) { var copy = void 0; @@ -310,90 +192,106 @@ throw new Error('Unable to copy [obj]! Its type is not supported.'); } - var rendererSettings = { - //query variables - form_col: 'formoid', - formDescription_col: 'ecrfpagename', - field_col: 'fieldname', - fieldDescription_col: 'fieldname', //there is not a dscriptive column in the test data prescribed by heather - marking_group_col: 'markinggroup', - visit_col: 'folderoid', - - //query open time settings - open_col: 'open_time', - open_category_col: 'Query Open Time Category', - open_category_order: ['0-7 days', '8-14 days', '15-30 days', '>30 days'], - - //query age settings - age_col: 'qdays', - age_category_col: 'Query Age Category', - age_category_order: null, - age_category_colors: [ - '#fd8d3c', - '#fc4e2a', - '#e31a1c', - '#bd0026', - '#800026', - '#1f78b4', - 'gray' - ], - - //query status settings - status_col: 'querystatus', - status_order: ['Open', 'Answered', 'Closed', 'Cancelled'], - status_colors: ['#fb9a99', '#fdbf6f', '#1f78b4', 'gray'], - - groups: null, - status_groups: null, - site_col: 'sitename', - filters: null, - details: null, - dropdown_size: 6, - cutoff: 'All', - alphabetize: true, - exportable: true, - nRowsPerPage: 10 - }; + function rendererSettings() { + return { + //query variables + form_col: 'formoid', + formDescription_col: 'ecrfpagename', + field_col: 'fieldname', + fieldDescription_col: null, + site_col: 'sitename', + marking_group_col: 'markinggroup', + visit_col: 'folderoid', + color_by_col: 'queryage', // options: [ 'queryage' , 'querystatus' ] or any of status_groups[].value_col + + //query age + age_statuses: ['Open'], + age_col: 'qdays', + age_cutoffs: [14, 28, 56, 112], + age_range_colors: [ + '#ffffcc', + '#ffeda0', + '#fed976', + '#feb24c', + '#fd8d3c', + '#fc4e2a', + '#e31a1c', + '#bd0026', + '#800026' + ], + + //query status + status_col: 'querystatus', + status_order: ['Open', 'Answered', 'Closed', 'Cancelled'], + status_colors: ['#fd8d3c', '#4daf4a', '#377eb8', '#999999'], + + //query recency + recency_category_col: 'open_time', + recency_col: 'odays', + recency_cutoffs: [7, 14, 30], + + //miscellany + groups: null, + status_groups: null, + filters: null, + dropdown_size: 6, + details: null, + bar_arrangement: 'stacked', + cutoff: 'All', + alphabetize: true, + truncate_listing_cells: true, + truncation_cutoff: 100 + }; + } - var webchartsSettings = { - x: { - label: '# of Queries', - behavior: 'flex' - }, - y: { - type: 'ordinal', - column: null, // set in syncSettings() - label: 'Form', - sort: 'total-descending' - }, - marks: [ - { - type: 'bar', - per: [null], // set in syncSettings() - split: null, // set in syncSettings() - arrange: 'stacked', - summarizeX: 'count', - tooltip: null // set in syncSettings() - } - ], - color_by: null, // set in syncSettings() - color_dom: null, // set in syncSettings() - legend: { - location: 'top', - // label: 'Query Status', - label: null, - order: null // set in syncSettings() - }, - range_band: 15, - margin: { - right: '50' // room for count annotation - } - }; + function chartSettings() { + return { + x: { + label: '# of Queries', + column: null, + behavior: 'flex' + }, + y: { + type: 'ordinal', + column: null, // set in syncSettings() + label: 'Form', + sort: null // set in syncSettings() + }, + marks: [ + { + type: 'bar', + per: [null], // set in syncSettings() + split: null, // set in syncSettings() + arrange: null, // set in syncSettings() + summarizeX: 'count', + tooltip: null // set in syncSettings() + } + ], + color_by: null, // set in syncSettings() + color_dom: null, // set in syncSettings() + legend: { + location: 'top', + label: null, // set in syncSettings() + order: null // set in syncSettings() + }, + margin: { + right: '50' // room for count annotation + }, + range_band: 25 + }; + } + + function listingSettings() { + return { + nRowsPerPage: 25, + exportable: true + }; + } function arrayOfVariablesCheck(defaultVariables, userDefinedVariables) { var validSetting = userDefinedVariables instanceof Array && userDefinedVariables.length - ? d3$1 + ? d3 .merge([ defaultVariables, userDefinedVariables.filter(function(item) { @@ -411,7 +309,7 @@ }) ]) .map(function(item) { - var itemObject = {}; + var itemObject = item instanceof Object ? Object.assign({}, item) : {}; itemObject.value_col = item instanceof Object ? item.value_col : item; itemObject.label = @@ -426,87 +324,202 @@ return validSetting; } - function syncSettings(settings) { - var syncedSettings = clone(settings); - - //groups + function syncGroups(settings) { var defaultGroups = [ - { value_col: syncedSettings.form_col, label: 'Form' }, + { value_col: settings.form_col, label: 'Form' }, { value_col: 'Form: Field', label: 'Form: Field' }, - { value_col: syncedSettings.site_col, label: 'Site' }, - { value_col: syncedSettings.marking_group_col, label: 'Marking Group' }, - { value_col: syncedSettings.visit_col, label: 'Visit/Folder' } + { value_col: settings.site_col, label: 'Site' }, + { value_col: settings.marking_group_col, label: 'Marking Group' }, + { value_col: settings.visit_col, label: 'Visit/Folder' } ]; - syncedSettings.groups = arrayOfVariablesCheck(defaultGroups, settings.groups); + settings.groups = settings.arrayOfVariablesCheck(defaultGroups, settings.groups); + } + + function syncStatusGroups(settings) { + //age ranges + settings.ageRanges = settings.age_cutoffs.map(function(d, i) { + return i > 0 ? [settings.age_cutoffs[i - 1], d] : [0, d]; + }); + settings.ageRanges.push([settings.age_cutoffs[settings.age_cutoffs.length - 1], null]); + + //age range categories + settings.ageRangeCategories = settings.age_cutoffs.every(function(age_range) { + return age_range % 7 === 0; + }) + ? settings.ageRanges.map(function(ageRange, i) { + return i < settings.ageRanges.length - 1 + ? ageRange + .map(function(days) { + return days / 7; + }) + .join('-') + ' wks' + : '>' + ageRange[0] / 7 + ' wks'; + }) + : settings.ageRanges.map(function(ageRange, i) { + return i < settings.ageRanges.length - 1 + ? ageRange.join('-') + ' days' + : '>' + ageRange[0] + ' days'; + }); + + //age range colors + var ageRangeColors = settings.age_range_colors.slice( + settings.age_range_colors.length - settings.ageRanges.length + ); + settings.status_order.forEach(function(status, i) { + if (settings.age_statuses.indexOf(status) < 0) + ageRangeColors.push(settings.status_colors[i]); + }); - //status_groups + //reconcile settings.status_order with settings.status_colors to ensure equal length + if (settings.status_order.length !== settings.status_colors.length) { + console.warn('The number of query statuses does not match the number of query colors:'); + console.log(settings.status_order); + console.log(settings.status_colors); + } + + //default status groups var defaultStatusGroups = [ { - value_col: syncedSettings.age_category_col, - label: 'Query Age Category', - order: syncedSettings.age_category_order, - colors: syncedSettings.age_category_colors, - derive_source: syncedSettings.age_col + value_col: 'queryage', // derived in ../chart/onInit/defineNewVariables + label: 'Query Age', + order: settings.ageRangeCategories.concat( + settings.status_order.filter(function(status) { + return settings.age_statuses.indexOf(status) < 0; + }) + ), + colors: ageRangeColors }, { - value_col: syncedSettings.status_col, + value_col: settings.status_col, label: 'Query Status', - order: syncedSettings.status_order, - colors: syncedSettings.status_colors + order: settings.status_order, + colors: settings.status_colors } ]; - syncedSettings.status_groups = arrayOfVariablesCheck( + + //add custom status groups + settings.status_groups = settings.arrayOfVariablesCheck( defaultStatusGroups, settings.status_groups ); + settings.status_group = settings.status_groups.find(function(status_group) { + return status_group.value_col === settings.color_by_col; + }); + } + function syncWebchartsSettings(settings) { //y-axis - syncedSettings.y.column = syncedSettings.form_col; + settings.y.column = settings.form_col; + settings.y.sort = settings.alphabetize ? 'alphabetical-ascending' : 'total-descending'; + settings.y.range_band = settings.range_band || 25; + + //mark settings + settings.marks[0].per[0] = settings.form_col; + settings.marks[0].split = settings.status_group.value_col; + settings.marks[0].arrange = settings.bar_arrangement; + settings.marks[0].tooltip = '[' + settings.status_group.value_col + '] - $x queries'; //stratification - syncedSettings.color_by = syncedSettings.status_groups[0].value_col; - syncedSettings.color_dom = syncedSettings.status_groups[0].order - ? syncedSettings.status_groups[0].order.slice() + settings.color_by = settings.status_group.value_col; + settings.color_dom = settings.status_group.order + ? settings.status_group.order.slice() : null; - syncedSettings.colors = syncedSettings.status_groups[0].colors; - syncedSettings.legend.label = syncedSettings.status_groups[0].label; - syncedSettings.legend.order = syncedSettings.status_groups[0].order - ? syncedSettings.status_groups[0].order.slice() + settings.colors = settings.status_group.colors; + + //legend + settings.legend.label = settings.status_group.label; + settings.legend.order = settings.status_group.order + ? settings.status_group.order.slice() : null; + } - //mark settings - syncedSettings.marks[0].per[0] = syncedSettings.form_col; - syncedSettings.marks[0].split = syncedSettings.color_by; - syncedSettings.marks[0].tooltip = '[' + syncedSettings.color_by + '] - $x queries'; + function syncFilters(settings) { + //recency ranges + settings.recencyRanges = settings.recency_cutoffs.map(function(d, i) { + return i > 0 ? [settings.recency_cutoffs[i - 1], d] : [0, d]; + }); + settings.recencyRanges.push([ + settings.recency_cutoffs[settings.recency_cutoffs.length - 1], + null + ]); + + //recency range categories + settings.recencyRangeCategories = settings.recency_cutoffs.every(function(recency_range) { + return recency_range % 7 === 0; + }) + ? settings.recencyRanges.map(function(recencyRange, i) { + return i < settings.recencyRanges.length - 1 + ? recencyRange + .map(function(days) { + return days / 7; + }) + .join('-') + ' weeks' + : '>' + recencyRange[0] / 7 + ' weeks'; + }) + : settings.recencyRanges.map(function(recencyRange, i) { + return i < settings.recencyRanges.length - 1 + ? recencyRange.join('-') + ' days' + : '>' + recencyRange[0] + ' days'; + }); - //filters + //default filters var defaultFilters = [ { - value_col: syncedSettings.open_category_col, - derive_source: syncedSettings.open_col, - label: 'Query Open Time', - multiple: true, - order: syncedSettings.open_category_order + value_col: 'queryrecency', + label: 'Query Recency', + multiple: true + }, + { + value_col: settings.form_col, + label: 'Form', + multiple: true }, - { value_col: syncedSettings.form_col, label: 'Form', multiple: true }, - { value_col: syncedSettings.site_col, label: 'Site', multiple: true }, - { value_col: syncedSettings.marking_group_col, label: 'Marking Group', multiple: true }, - { value_col: syncedSettings.visit_col, label: 'Visit/Folder', multiple: true } + { + value_col: settings.site_col, + label: 'Site', + multiple: true + }, + { + value_col: settings.marking_group_col, + label: 'Marking Group', + multiple: true + }, + { + value_col: settings.visit_col, + label: 'Visit/Folder', + multiple: true + } ]; - syncedSettings.status_groups.reverse().forEach(function(status_group) { - status_group.multiple = true; - defaultFilters.unshift(clone(status_group)); - }); - syncedSettings.filters = arrayOfVariablesCheck(defaultFilters, settings.filters); + //add status group variables to list of filters + settings.status_groups + .slice() + .reverse() + .forEach(function(status_group) { + status_group.multiple = true; + defaultFilters.unshift(settings.clone(status_group)); + }); + + //add custom filters + settings.filters = settings.arrayOfVariablesCheck(defaultFilters, settings.filters); + } + + function syncCutoff(settings) { + if (!(+settings.cutoff > 0 || settings.cutoff === 'All')) settings.cutoff = 10; + } - //cutoff - if (!(+syncedSettings.cutoff > 0 || syncedSettings.cutoff === 'All')) - syncedSettings.cutoff = 10; + function syncSettings(settings) { + var syncedSettings = Object.assign({}, clone(settings), { + clone: clone, + arrayOfVariablesCheck: arrayOfVariablesCheck + }); + syncGroups(syncedSettings); + syncStatusGroups(syncedSettings); + syncWebchartsSettings(syncedSettings); + syncFilters(syncedSettings); + syncCutoff(syncedSettings); + syncCutoff(syncedSettings); - //details - syncedSettings.details = arrayOfVariablesCheck([], settings.details); - if (syncedSettings.details.length === 0) delete syncedSettings.details; return syncedSettings; } @@ -515,16 +528,14 @@ type: 'dropdown', option: 'y.label', label: 'Group by', - description: 'variable toggle', start: null, // set in syncControlInputs() values: null, // set in syncControlInputs() require: true }, { - type: 'dropdown', + type: 'radio', label: 'Status Group', - description: 'stratification', - options: ['marks.0.split', 'color_by'], // will want to change tooltip too + option: 'color_by_col', start: null, // set in syncControlInputs() values: null, // set in syncControlInputs() require: true @@ -544,59 +555,259 @@ { type: 'checkbox', option: 'alphabetize', - label: 'Alphabetical?' + label: 'Order Groups Alphabetically?' } ]; - function syncControlInputs(controlInputs, settings) { - var syncedControlInputs = clone(controlInputs); - - //Group by - var groupByControl = syncedControlInputs.find(function(controlInput) { + function syncGroupBy(controlInputs, settings) { + var groupByControl = controlInputs.find(function(controlInput) { return controlInput.label === 'Group by'; }); groupByControl.values = settings.groups.map(function(group) { return group.label; }); + } - //Status Group - var statusGroupControl = syncedControlInputs.find(function(controlInput) { + function syncStatusGroup(controlInputs, settings) { + var statusGroupControl = controlInputs.find(function(controlInput) { return controlInput.label === 'Status Group'; }); + statusGroupControl.start = settings.color_by; statusGroupControl.values = settings.status_groups.map(function(status_group) { - return status_group.value_col; + return status_group.label; }); + } - //filters + function syncFilters$1(controlInputs, settings) { settings.filters.forEach(function(filter, i) { filter.type = 'subsetter'; - filter.description = 'filter'; - syncedControlInputs.splice(2 + i, 0, filter); + controlInputs.splice(2 + i, 0, filter); }); + } - //Show First N Groups - var nGroupsControl = syncedControlInputs.find(function(controlInput) { + function syncShowFirstNGroups(controlInputs, settings) { + var nGroupsControl = controlInputs.find(function(controlInput) { return controlInput.label === 'Show First N Groups'; }); if (nGroupsControl.values.indexOf(settings.cutoff.toString()) === -1) { + settings.cutoff = settings.cutoff.toString(); nGroupsControl.values.push(settings.cutoff.toString()); nGroupsControl.values.sort(function(a, b) { return a === 'All' ? 1 : b === 'All' ? -1 : +a - +b; }); } else settings.cutoff = settings.cutoff.toString() || nGroupsControl.values[0]; + } + + function syncControlInputs(controlInputs, settings) { + var syncedControlInputs = settings.clone(controlInputs); + syncGroupBy(syncedControlInputs, settings); + syncStatusGroup(syncedControlInputs, settings); + syncFilters$1(syncedControlInputs, settings); + syncShowFirstNGroups(syncedControlInputs, settings); return syncedControlInputs; } var configuration = { rendererSettings: rendererSettings, - webchartsSettings: webchartsSettings, - settings: Object.assign({}, rendererSettings, webchartsSettings), + chartSettings: chartSettings, + listingSettings: listingSettings, + settings: Object.assign({}, rendererSettings(), chartSettings(), listingSettings()), syncSettings: syncSettings, controlInputs: controlInputs, syncControlInputs: syncControlInputs }; + function layout(element) { + var containers = { + main: d3 + .select(element) + .append('div') + .classed('.query-overview', true) + }; + + containers.topRow = containers.main.append('div').classed('qo-row qo-row--top', true); + containers.controls = containers.topRow + .append('div') + .classed('qo-component qo-component--controls', true); + containers.chart = containers.controls + .append('div') + .classed('qo-component qo-component--chart', true); + containers.bottomRow = containers.main.append('div').classed('qo-row qo-row--bottom', true); + containers.listing = containers.bottomRow + .append('div') + .classed('qo-component qo-component--listing', true); + + return containers; + } + + function styles() { + var styles = [ + /***--------------------------------------------------------------------------------------\ + Framework + \--------------------------------------------------------------------------------------***/ + + '.query-overview {' + ' width: 100%;' + ' display: inline-block;' + '}', + '.qo-row {' + ' width: 100%;' + ' display: inline-block;' + '}', + '.qo-component {' + '}', + '.qo-row--top {' + '}', + '.qo-row--bottom {' + '}', + + /***--------------------------------------------------------------------------------------\ + Controls + \--------------------------------------------------------------------------------------***/ + + '.qo-component--controls {' + ' width: 100%;' + '}', + '.qo-component--controls .wc-controls {' + ' margin-bottom: 0;' + '}', + '.qo-control-grouping {' + ' display: inline-block;' + '}', + '.qo-button {' + ' float: left;' + ' display: block;' + '}', + '.qo-control-grouping--label,' + + '.wc-control-label {' + + ' cursor: help;' + + ' margin-bottom: 3px;' + + '}', + '.qo-control-grouping--label {' + + ' text-align: center;' + + ' width: 100%;' + + ' font-size: 24px;' + + ' border-bottom: 2px solid #aaa;' + + '}', + '.span-description {' + ' display: none !important;' + '}', + + /****---------------------------------------------------------------------------------\ + Other controls + \---------------------------------------------------------------------------------****/ + + '.qo-control-grouping--other-controls {' + + ' width: 20%;' + + ' float: right;' + + '}', + '.qo-control-grouping--other-controls .control-group {' + + ' width: 100%;' + + ' margin-bottom: 15px;' + + '}', + '.qo-control-grouping--other-controls .control-group:nth-child(n+3) {' + + ' border-top: 1px solid #aaa;' + + '}', + '.qo-control-grouping--other-controls .control-group .wc-control-label {' + + ' text-align: center;' + + ' font-size: 110%;' + + '}', + + //dropdowns + '.qo-dropdown {' + '}', + '.qo-dropdown .wc-control-label {' + '}', + '.qo-dropdown .changer {' + ' margin: 0 auto;' + '}', + + //radio buttons + '.qo-radio {' + + ' display: flex !important;' + + ' justify-content: center;' + + ' flex-wrap: wrap;' + + '}', + '.qo-radio .wc-control-label {' + ' width: 100%;' + '}', + '.qo-radio .radio {' + ' margin-top: 0 !important;' + '}', + + //checkboxes + '.qo-checkbox {' + + ' display: flex !important;' + + ' justify-content: center;' + + '}', + '.qo-checkbox .wc-control-label {' + ' margin-right: 5px;' + '}', + '.qo-checkbox .changer {' + ' margin-top: 5px !important;' + '}', + + /****---------------------------------------------------------------------------------\ + Filters + \---------------------------------------------------------------------------------****/ + + '.qo-control-grouping--filters {' + + ' width: 20%;' + + ' float: left;' + + ' display: flex;' + + ' flex-wrap: wrap;' + + ' justify-content: space-evenly;' + + '}', + '.qo-subsetter {' + + ' margin: 5px 2px !important;' + + ' border-top: 1px solid #aaa;' + + ' padding-top: 5px;' + + '}', + '.qo-subsetter .wc-control-label {' + + ' margin: 0 5px 3px 0;' + + ' text-align: center;' + + '}', + '.qo-select-all {' + '}', + '.qo-subsetter .changer {' + ' margin: 0 auto;' + '}', + + /***--------------------------------------------------------------------------------------\ + Chart + \--------------------------------------------------------------------------------------***/ + + '.qo-component--chart {' + + ' width: 58%;' + + ' margin: 0 auto;' + + ' position: relative;' + + '}', + '.qo-button--reset-chart {' + + ' position: absolute;' + + ' top: 0;' + + ' left: 0;' + + ' z-index: 2;' + + ' width: 91px;' + + ' padding: 3px 0;' + + '}', + '.qo-button--undo {' + + ' position: absolute;' + + ' bottom: 0;' + + ' left: 0;' + + ' z-index: 2;' + + ' width: 91px;' + + ' padding: 3px 0;' + + '}', + '.qo-component--chart .wc-chart {' + ' z-index: 1;' + '}', + '.qo-component--chart .legend-title {' + ' cursor: help;' + '}', + '.qo-component--chart .legend-item {' + + ' cursor: pointer;' + + ' border-radius: 4px;' + + ' padding: 5px;' + + ' padding-left: 8px;' + + ' margin-right: 5px !important;' + + '}', + '.qo-footnote {' + + ' width: 100%;' + + ' text-align: center;' + + ' font-style: italic;' + + '}', + + /***--------------------------------------------------------------------------------------\ + Listing + \--------------------------------------------------------------------------------------***/ + + '.qo-component--listing {' + ' width: 100%;' + '}', + '.qo-button--reset-listing {' + + ' padding: 3px;' + + ' margin: 10px 5px 10px 0;' + + '}', + '.qo-table-container {' + + ' overflow-x: auto;' + + ' width: 100%;' + + ' transform: rotate(180deg);' + + ' -webkit-transform: rotate(180deg); ' + + '}', + '.qo-table {' + + ' width: 100%;' + + ' transform: rotate(180deg);' + + ' -webkit-transform: rotate(180deg); ' + + '}' + ]; + + //Attach styles to DOM. + var style = document.createElement('style'); + style.type = 'text/css'; + style.innerHTML = styles.join('\n'); + document.getElementsByTagName('head')[0].appendChild(style); + } + function defineListingSettings() { this.listing.config.cols = this.config.details ? this.config.details.map(function(d) { @@ -617,60 +828,53 @@ function defineNewVariables() { var _this = this; + var queryAgeCol = this.config.status_groups.find(function(status_group) { + return status_group.label === 'Query Age'; + }).value_col; + var queryRecencyCol = this.config.filters.find(function(filter) { + return filter.label === 'Query Recency'; + }).value_col; + this.raw_data.forEach(function(d) { + //Concatenate form and field to avoid duplicates across forms. d['Form: Field'] = d[_this.config.form_col] + ': ' + d[_this.config.field_col]; - //Define query age category. - if (!_this.config.age_category_order) { - var queryAge = - /^ *\d+ *$/.test(d[_this.config.age_col]) && - ['Closed', 'Cancelled'].indexOf(d[_this.config.status_col]) < 0 - ? +d[_this.config.age_col] - : NaN; - switch (true) { - case queryAge <= 14: - d['Query Age Category'] = '0-2 weeks'; - break; - case queryAge <= 28: - d['Query Age Category'] = '2-4 weeks'; - break; - case queryAge <= 56: - d['Query Age Category'] = '4-8 weeks'; - break; - case queryAge <= 112: - d['Query Age Category'] = '8-16 weeks'; - break; - case queryAge > 112: - d['Query Age Category'] = '>16 weeks'; - break; - default: - d['Query Age Category'] = d[_this.config.status_col]; - break; - } + //Define query age. + if (_this.config.age_statuses.indexOf(d[_this.config.status_col]) < 0) + d[queryAgeCol] = d[_this.config.status_col]; + else { + var age = +d[_this.config.age_col]; + _this.config.ageRanges.forEach(function(ageRange, i) { + if (i === 0 && ageRange[0] <= age && age <= ageRange[1]) + d[queryAgeCol] = _this.config.ageRangeCategories[i]; + else if (i === _this.config.ageRanges.length - 1 && ageRange[0] < age) + d[queryAgeCol] = _this.config.ageRangeCategories[i]; + else if (ageRange[0] < age && age <= ageRange[1]) + d[queryAgeCol] = _this.config.ageRangeCategories[i]; + }); } - //Define query open time category. - var openTime = /^ *\d+ *$/.test(d[_this.config.open_col]) - ? +d[_this.config.open_col] - : NaN; - switch (true) { - case openTime <= 7: - d['Query Open Time Category'] = '0-7 days'; - break; - case openTime <= 14: - d['Query Open Time Category'] = '8-14 days'; - break; - case openTime <= 30: - d['Query Open Time Category'] = '15-30 days'; - break; - default: - d['Query Open Time Category'] = '>30 days'; - break; + //Define query recency. + if (d.hasOwnProperty(_this.config.recency_category_col)) { + d[queryRecencyCol] = d[_this.config.recency_category_col] || 'N/A'; + } else if (d.hasOwnProperty(_this.config.recency_col)) { + var recency = +d[_this.config.recency_col]; + _this.config.recencyRanges.forEach(function(recencyRange, i) { + if (i === 0 && recencyRange[0] <= recency && recency <= recencyRange[1]) + d[queryRecencyCol] = _this.config.recencyRangeCategories[i]; + else if ( + i === _this.config.recencyRanges.length - 1 && + recencyRange[0] < recency + ) + d[queryRecencyCol] = _this.config.recencyRangeCategories[i]; + else if (recencyRange[0] < recency && recency <= recencyRange[1]) + d[queryRecencyCol] = _this.config.recencyRangeCategories[i]; + }); } }); } - function defineQueryStatuses() { + function defineQueryStatusSet() { var _this = this; var queryStatusInput = this.controls.config.inputs.find(function(input) { @@ -679,74 +883,61 @@ var queryStatusGroup = this.config.status_groups.find(function(status_group) { return status_group.value_col === _this.config.status_col; }); - var queryStatusOrder = Array.isArray(queryStatusGroup.order) - ? queryStatusGroup.order.concat( - d3 + var queryStatusOrder = + Array.isArray(queryStatusGroup.order) && queryStatusGroup.order.length + ? queryStatusGroup.order.concat( + d3 + .set( + this.raw_data.map(function(d) { + return d[_this.config.status_col]; + }) + ) + .values() + .filter(function(value) { + return queryStatusGroup.order.indexOf(value) < 0; + }) + .sort() + ) + : d3 .set( this.raw_data.map(function(d) { return d[_this.config.status_col]; }) ) .values() - .filter(function(value) { - return queryStatusGroup.order.indexOf(value) < 0; - }) - .sort() - ) - : d3 - .set( - this.raw_data.map(function(d) { - return d[_this.config.status_col]; - }) - ) - .values() - .sort(); + .sort(); queryStatusInput.order = queryStatusOrder; queryStatusGroup.order = queryStatusOrder; } - function defineQueryAgeCategories() { - var _this = this; - - var queryAgeCategoryInput = this.controls.config.inputs.find(function(input) { - return input.value_col === _this.config.age_category_col; + function defineQueryRecencySet() { + var queryRecencyInput = this.controls.config.inputs.find(function(input) { + return input.value_col === 'queryrecency'; }); - var queryAgeCategoryGroup = this.config.status_groups.find(function(age_category_group) { - return age_category_group.value_col === _this.config.age_category_col; - }); - var queryStatusOrder = this.config.status_groups.find(function(status_group) { - return status_group.value_col === _this.config.status_col; - }).order; - var queryAgeCategoryOrder = Array.isArray(queryAgeCategoryGroup.order) - ? queryAgeCategoryGroup.order.concat( - d3 - .set( - this.raw_data.map(function(d) { - return d[_this.config.age_category_col]; - }) - ) - .values() - .filter(function(value) { - return queryAgeCategoryGroup.order.indexOf(value) < 0; - }) - .sort() - ) - : d3 - .set( - this.raw_data.map(function(d) { - return d[_this.config.age_category_col]; - }) - ) - .values() - .sort(function(a, b) { - var aIndex = queryStatusOrder.indexOf(a); - var bIndex = queryStatusOrder.indexOf(b); - var diff = aIndex - bIndex; - return diff ? diff : a < b ? -1 : 1; - }); - queryAgeCategoryInput.order = queryAgeCategoryOrder; - queryAgeCategoryGroup.order = queryAgeCategoryOrder; + if (this.raw_data[0].hasOwnProperty(this.config.recency_category_col)) { + queryRecencyInput.values = d3 + .set( + this.raw_data.map(function(d) { + return d.queryrecency; + }) + ) + .values() + .sort(function(a, b) { + var anum = parseFloat(a); + var bnum = parseFloat(b); + var diff = anum - bnum; + return diff ? diff : a < b ? -1 : a > b ? 1 : 0; + }); + } else if (this.raw_data[0].hasOwnProperty(this.config.recency_col)) + queryRecencyInput.values = this.config.recencyRangeCategories; + else + this.controls.config.inputs.splice( + this.controls.config.inputs.findIndex(function(input) { + return input.value_col === 'queryrecency'; + }), + 1 + ); } function removeInvalidControls() { @@ -768,10 +959,10 @@ defineNewVariables.call(this); //Define query statuses. - defineQueryStatuses.call(this); + defineQueryStatusSet.call(this); - //Define query age categories. - defineQueryAgeCategories.call(this); + //Define query recency categories. + defineQueryRecencySet.call(this); //Define detail listing settings. defineListingSettings.call(this); @@ -783,10 +974,125 @@ this.listing.init(this.raw_data); } + function classControls() { + this.controls.controlGroups = this.controls.wrap + .selectAll('.control-group') + .attr('class', function(d) { + return ( + 'control-group qo-' + + d.type + + ' qo-' + + d.type + + '--' + + d.label.toLowerCase().replace(/[^_a-zA-Z-]/g, '-') + ); + }); + } + + function groupControls() { + var context = this; + + //Group filters. + this.controls.filters = { + container: this.controls.wrap + .insert('div', '.qo-subsetter') + .classed('qo-control-grouping qo-control-grouping--filters', true) + }; + this.controls.filters.container + .append('div') + .classed('qo-control-grouping--label', true) + .attr( + 'title', + 'Filters subset the data underlying the chart and listing.\nHover over filter labels to view more information about them.' + ) + .text('Filters'); + this.controls.filters.controlGroups = this.controls.wrap.selectAll('.qo-subsetter'); + this.controls.filters.labels = this.controls.filters.controlGroups.selectAll( + '.wc-control-label' + ); + this.controls.filters.selects = this.controls.filters.controlGroups.selectAll('.changer'); + this.controls.filters.controlGroups.each(function(d) { + context.controls.filters.container.node().appendChild(this); + }); + + //Group other controls. + this.controls.otherControls = { + container: this.controls.wrap + .insert('div', ':first-child') + .classed('qo-control-grouping qo-control-grouping--other-controls', true) + }; + this.controls.otherControls.label = this.controls.otherControls.container + .append('div') + .classed('qo-control-grouping--label', true) + .attr( + 'title', + 'Controls alter the display of the chart.\nHover over control labels to view more information about them.' + ) + .text('Controls'); + this.controls.otherControls.controlGroups = this.controls.wrap.selectAll( + '.control-group:not(.qo-subsetter)' + ); + this.controls.otherControls.labels = this.controls.otherControls.controlGroups.selectAll( + '.wc-control-label' + ); + this.controls.otherControls.controlGroups.each(function(d) { + context.controls.otherControls.container.node().appendChild(this); + }); + } + + function addControlTooltips() { + var tooltips = { + //other controls + 'Group by': + 'Controls the variable with which the queries are grouped; each group is plotted along the vertical axis of the chart.', + 'Status Group': 'Controls the variable with which the bars are subdivided.', + 'Bar Arrangement': + 'Controls the layout of the status groups.\n- stacked: status groups are plotted side-by-side horizontally\n- grouped: status groups are plotted side-by-side vertically', + 'Show First N Groups': 'Controls the number of groups displayed on the vertical axis.', + 'Order Groups Alphabetically?': + 'Controls the order of the groups; uncheck to sort groups by magnitude (largest to smallest number of queries) instead of alphabetically.', + + //filters + 'Query Age': + 'Open queries are broken down into how long they have been open. All other queries are classified by status (answered, closed, cancelled).', + 'Query Status': + 'Open: site has not responded to the issue\nAnswered: site has responded to issue; DM needs to review\nClosed: issue resolved\nCancelled: query cancelled by DM', + 'Query Recency': + 'Number of days a query has been open, regardless of its current status (applies only to queries opened in the past 30 days)', + Form: + 'CRF page abbreviation; hover over the abbreviation in the chart to see its full name.', + Site: 'Name or ID of site', + 'Marking Group': 'Entity that opened the query', + 'Visit/Folder': + 'Visit/folder abbreviation; hover over the visit/folder abbreviation in the chart to see the full name.' + }; + this.controls.controlGroups.each(function(d) { + var tooltip = + tooltips[d.label] || + 'This ' + + d.type + + ' controls ' + + (d.value_col || d.option || d.options.join(', ')) + + '.'; + if (tooltips[d.label] === undefined) + console.warn( + 'The control labeled ' + + d.label + + ' does not have a curated tooltip. Defaulting to ' + + tooltip + + '.' + ); + d3 + .select(this) + .selectAll('.wc-control-label') + .attr('title', tooltip); + }); + } + function updateGroupByOptions() { var context = this; - var groupByControl = this.controls.wrap + this.controls.wrap .selectAll('.control-group select') .filter(function(d) { return d.label === 'Group by'; @@ -810,6 +1116,64 @@ }); } + function addGroupByHighlight() { + var _this = this; + + this.controls.otherControls.controlGroups + .filter(function(d) { + return d.label === 'Group by'; + }) + .on('mouseover', function() { + _this.svg.selectAll('.y.axis .axis-title').style({ + 'font-weight': 'bold', + 'text-decoration': 'underline', + fill: 'red' + }); + }) + .on('mouseout', function() { + _this.svg.selectAll('.y.axis .axis-title').style({ + 'font-weight': 'normal', + 'text-decoration': 'none', + fill: 'black' + }); + }); + } + + function checkInitialStatusGroup() { + var _this = this; + + this.controls.otherControls.controlGroups + .filter(function(d) { + return d.label === 'Status Group'; + }) + .selectAll('.radio') + .selectAll('.changer') + .property('checked', function(d) { + return d === _this.config.legend.label; + }); + } + + function addStatusGroupHighlight() { + var _this = this; + + this.controls.otherControls.controlGroups + .filter(function(d) { + return d.label === 'Status Group'; + }) + .on('mouseover', function() { + _this.legend.select('.legend-title').style({ + 'text-decoration': 'underline', + color: 'red' + }); + }) + .on('mouseout', function() { + _this.legend.select('.legend-title').style({ + 'text-decoration': 'none', + color: 'black' + }); + }); + } + function customizeMultiSelects() { var context = this; @@ -844,6 +1208,97 @@ }); } + function addSelectAll() { + var context = this; + + this.controls.filters.labels + .filter(function(d) { + return d.multiple; + }) + .each(function(d) { + var label = d3 + .select(this) + .html(d.label + ' '); + var checkbox = label + .select('input') + .datum(d) + .attr('title', 'Deselect all ' + d.label + ' options') + .property('checked', true) + .on('click', function(di) { + var checkbox = d3.select(this); + var checked = this.checked; + + //Update checkbox tooltip. + checkbox.attr( + 'title', + checked + ? 'Deselect all ' + di.label + ' options' + : 'Select all ' + di.label + ' options' + ); + + //Update filter object. + var filter = context.filters.find(function(filter) { + return filter.col === di.value_col; + }); + if (checked) filter.val = filter.choices; + else filter.val = []; + + //Redraw. + context.draw(); + }); + }); + this.controls.filters.checkboxes = this.controls.filters.labels.selectAll('.qo-select-all'); + } + + function updateSelectAll(d, selectedOptions) { + //Update filter object. + var filter = this.filters.find(function(filter) { + return filter.col === d.value_col; + }); + filter.val = d.multiple ? selectedOptions : selectedOptions.pop(); + + //Update checkbox. + if (d.multiple) { + var checked = filter.val.length === filter.choices.length; + var checkbox = this.controls.filters.checkboxes + .filter(function(di) { + return di.value_col === d.value_col; + }) + .attr( + 'title', + checked + ? 'Deselect all ' + d.label + ' options' + : 'Select all ' + d.label + ' options' + ) + .property('checked', checked); + } + } + + function updateFilterEventListeners() { + var context = this; + + this.controls.filters.selects.on('change', function(d) { + var select = d3.select(this); + var selectedOptions = select.selectAll('option:checked').data(); + updateSelectAll.call(context, d, selectedOptions); + context.draw(); + }); + } + + function sortQueryRecencyOptions() { + this.controls.filters.selects + .filter(function(d) { + return d.value_col === 'queryrecency'; + }) + .selectAll('option') + .sort(function(a, b) { + var anum = parseFloat(a); + var bnum = parseFloat(b); + var diff = anum - bnum; + return diff ? diff : a < b ? -1 : a > b ? 1 : 0; + }); + } + function setYAxisDomainLength() { var context = this; @@ -872,21 +1327,21 @@ function addResetButton() { var _this = this; - this.controls.wrap + this.resetButton = d3 + .select(this.div) .insert('button', ':first-child') - .attr('id', 'reset-chart') - .style({ - margin: '5px', - padding: '5px', - float: 'right' - }) + .classed('qo-button qo-button--reset-chart', true) .text('Reset chart') .on('click', function() { - var element = clone(_this.div), - settings = clone(_this.initialSettings), - data = clone(_this.raw_data); + var element = _this.element; + var settings = clone(_this.initialSettings); + var data = clone(_this.raw_data); _this.listing.destroy(); _this.destroy(); + d3 + .select(_this.element) + .selectAll('*') + .remove(); queryOverview(element, settings).init(data); }); } @@ -913,25 +1368,57 @@ //Reset listing. context.listing.wrap.selectAll('*').remove(); - context.wrap.select('#listing-instruction').style('display', 'block'); context.listing.init(context.filtered_data); }); } - function addListingInstruction() { - this.wrap - .append('em') - .attr('id', 'listing-instruction') - .text('Click a bar to view its underlying data.'); + function addFootnotes() { + this.footnotes = { + barClick: this.wrap + .append('div') + .classed('qo-footnote qo-footnote--bar-click', true) + .text('Click one or more bars to view the underlying data in the listing below.'), + deselectBars: this.wrap + .append('div') + .classed('qo-footnote qo-footnote--deselect-bars', true) + .text('Click in the white area to deselect all bars.') + }; } function onLayout() { + //Class controls for unique selection. + classControls.call(this); + + //Group controls logically. + groupControls.call(this); + + //Add tooltips to controls. + addControlTooltips.call(this); + //Display group label rather than group column name in Group by control. updateGroupByOptions.call(this); + //Highlight y-axis label when user hovers over Status Group control. + addGroupByHighlight.call(this); + + //Check radio button of initial status group. + checkInitialStatusGroup.call(this); + + //Highlight legend when user hovers over Status Group control. + addStatusGroupHighlight.call(this); + //Customize dropdowns with multiple options. customizeMultiSelects.call(this); + //Add select all checkbox to filters. + addSelectAll.call(this); + + //Update filter event listeners to toggle select all checkbox on change. + updateFilterEventListeners.call(this); + + //Sort query recency categories numerically if possible. + sortQueryRecencyOptions.call(this); + //Handle y-domain length control setYAxisDomainLength.call(this); @@ -941,28 +1428,28 @@ //Clear listing when controls change. clearListingOnChange.call(this); - //Add listing instruction. - addListingInstruction.call(this); + //Add chart footnotes. + addFootnotes.call(this); } function updateStratification() { - var _this = this; - - var stratification = this.config.status_groups.find(function(status_group) { - return status_group.value_col === _this.config.color_by; + var statusGroup = this.controls.wrap + .selectAll('.qo-radio--status-group') + .selectAll('.radio') + .filter(function() { + var label = d3.select(this); + var radio = label.select('.changer'); + return radio.property('checked'); + }) + .text(); + this.config.status_group = this.config.status_groups.find(function(status_group) { + return status_group.label === statusGroup; }); - this.config.color_dom = - stratification.order || - d3$1 - .set( - this.raw_data.map(function(d) { - return d[_this.config.color_by]; - }) - ) - .values() - .sort(); - this.config.colors = stratification.colors; - this.config.legend.label = stratification.label; + this.config.marks[0].split = this.config.status_group.value_col; + this.config.color_by = this.config.status_group.value_col; + this.config.color_dom = this.config.status_group.order; + this.config.colors = this.config.status_group.colors; + this.config.legend.label = this.config.status_group.label; this.config.legend.order = this.config.color_dom.slice(); this.config.marks[0].tooltip = '[' + this.config.color_by + '] - $x queries'; } @@ -990,9 +1477,9 @@ function updateRangeBand() { if (this.config.marks[0].arrange === 'stacked') { - this.config.range_band = 15; + this.config.range_band = this.initialSettings.range_band; } else { - this.config.range_band = 15 * this.config.color_dom.length; + this.config.range_band = this.initialSettings.range_band * this.config.color_dom.length; } } @@ -1006,7 +1493,7 @@ function setLeftMargin() { var fontSize = parseInt(this.wrap.style('font-size')); - this.config.margin.left = + this.config.margin.left = Math.max( Math.max( 7, d3.max(this.y_dom, function(d) { @@ -1015,8 +1502,14 @@ ) * fontSize * 0.5 + - fontSize * 1.5 * 1.5 + - 6; + fontSize * 1.5 * 1.5 + + 6, + 100 + ); + } + + function setXDomain() { + if (this.filtered_data.length === 0) this.x_dom = [0, 0]; } function setYDomain() { @@ -1066,9 +1559,10 @@ function setChartHeight() { //Match chart height to number of bars currently displayed. - this.raw_height = - (+this.config.range_band + this.config.range_band * this.config.padding) * - this.y_dom.length; + this.raw_height = this.filtered_data.length + ? (+this.config.range_band + this.config.range_band * this.config.padding) * + this.y_dom.length + : 100; } function updateXAxisLabel() { @@ -1089,28 +1583,34 @@ function onDraw() { setLeftMargin.call(this); + setXDomain.call(this); setYDomain.call(this); setChartHeight.call(this); updateXAxisLabel.call(this); } + //TODO: modularize/refactor function legendFilter() { var _this = this; var context = this; + //Alter layout of legend. + var legend = this.legend; + legend.style('margin-left', this.margin.left + 'px'); + var legendTitle = legend.select('.legend-title'); + legendTitle.attr( + 'title', + 'Add and remove queries by clicking the legend items to the left.' + ); + legend.node().appendChild(legendTitle.node()); + //Filter data by clicking on legend. var statusFilter = this.filters.find(function(filter) { return filter.col === _this.config.color_by; }); var legendItems = this.wrap .selectAll('.legend-item') - .style({ - cursor: 'pointer', - 'border-radius': '4px', - padding: '5px', - 'padding-left': '8px' - }) .classed('selected', function(d) { return statusFilter.val === 'All' || statusFilter.val.indexOf(d.label) > -1; }) @@ -1119,17 +1619,15 @@ ? 'lightgray' : 'white'; }); - var statusOptions = this.controls.wrap - .selectAll('.control-group') - .filter(function(d) { - return d.value_col === context.config.marks[0].split; - }) - .selectAll('.changer option'); // status filter options + var statusControlGroup = this.controls.wrap.selectAll('.control-group').filter(function(d) { + return d.value_col === context.config.marks[0].split; + }); + var statusOptions = statusControlGroup.selectAll('.changer option'); // status filter options legendItems.selectAll('.legend-mark-text').remove(); // don't need 'em + legendItems.selectAll('.legend-color-block').attr('width', '8px'); legendItems.on('click', function(d) { - var legendItem = d3.select(this), - // clicked legend item - selected = !legendItem.classed('selected'); // selected boolean + var legendItem = d3.select(this); // clicked legend item + var selected = !legendItem.classed('selected'); // selected boolean legendItem.classed('selected', selected); // toggle selected class var selectedLegendItems = legendItems .filter(function() { @@ -1148,6 +1646,7 @@ return selectedLegendItems.indexOf(d) > -1; }) .property('selected', true); // set selected property of status options corresponding to selected statuses to true + updateSelectAll.call(context, statusControlGroup.datum(), selectedLegendItems); var filtered_data = context.raw_data.filter(function(d) { var filtered = selectedLegendItems.indexOf(d[context.config.marks[0].split]) === -1; @@ -1182,9 +1681,8 @@ }); context.draw(); - //Remove listing and display listing instruction. + //Remove listing and display bar click footnote. context.listing.wrap.selectAll('*').remove(); - context.wrap.select('#listing-instruction').style('display', 'block'); context.listing.init(filtered_data); }); } @@ -1237,39 +1735,92 @@ .style('fill', 'blue') .style('text-decoration', 'underline'); yLabels.style('cursor', 'pointer').on('click', function(yLabel) { - _this.controls.wrap - .selectAll('.control-group') - .filter(function(d) { - return d.label === 'Group by'; + //Update Group by control. + var groupByControl = _this.controls.otherControls.controlGroups.filter(function(d) { + return d.label === 'Group by'; + }); + groupByControl + .select('.wc-control-label') + .style({ + 'font-weight': 'bold', + 'text-decoration': 'underline', + color: 'red' }) - .selectAll('option') - .property('selected', function(d) { - return d === 'Form: Field'; + .transition() + .delay(3000) + .style({ + 'font-weight': 'normal', + 'text-decoration': 'none', + color: 'black' }); + groupByControl.selectAll('option').property('selected', function(d) { + return d === 'Form: Field'; + }); + + //Update chart settings. _this.config.y.column = 'Form: Field'; _this.config.y.label = 'Form: Field'; _this.config.marks[0].per[0] = 'Form: Field'; - _this.controls.wrap - .selectAll('.control-group') - .filter(function(d) { - return d.label === 'Form'; + + //Update Form filter. + var formFilter = _this.controls.filters.controlGroups.filter(function(d) { + return d.label === 'Form'; + }); + formFilter + .select('.wc-control-label') + .style({ + 'font-weight': 'bold', + 'text-decoration': 'underline', + color: 'red' }) - .selectAll('option') - .property('selected', function(d) { - return d === yLabel; + .transition() + .delay(3000) + .style({ + 'font-weight': 'normal', + 'text-decoration': 'none', + color: 'black' }); - _this.filters.filter(function(filter) { - return filter.col === _this.config.form_col; - })[0].val = yLabel; + formFilter.selectAll('option').property('selected', function(d) { + return d === yLabel; + }); - _this.draw( - _this.filtered_data.filter(function(d) { - return d[_this.config.form_col] === yLabel; - }) + //Update Form filter object in `chart.filters`. + var filter = _this.filters.find(function(filter) { + return filter.col === _this.config.form_col; + }); + filter.val = yLabel; + updateSelectAll.call( + _this, + _this.controls.filters.controlGroups + .filter(function(d) { + return d.value_col === _this.config.form_col; + }) + .datum(), + [yLabel] ); + + //Redraw chart and listing. + + //Update Group by control. + _this.draw(); _this.listing.wrap.selectAll('*').remove(); - _this.wrap.select('listing-instruction').style('display', 'block'); _this.listing.init(_this.filtered_data); + + //Highlight y-axis label. + _this.svg + .select('.y.axis .axis-title') + .style({ + 'font-weight': 'bold', + 'text-decoration': 'underline', + fill: 'red' + }) + .transition() + .delay(3000) + .style({ + 'font-weight': 'normal', + 'text-decoration': 'none', + fill: 'black' + }); }); } } @@ -1375,85 +1926,148 @@ } } - // from https://gist.github.com/samgiles/762ee337dff48623e729 + function mouseoverStyle(bar, selected) { + if (!selected) + bar.style({ + 'stroke-width': '5px', + fill: 'black' + }); + } - Array.prototype.flatMap = function(lambda) { - return Array.prototype.concat.apply([], this.map(lambda)); - }; + function mouseoverAttrib(bar, selected) { + if (!selected) + bar.attr({ + width: function width(d) { + return this.getBBox().width - 2.5; + }, + x: function x(d) { + return this.getBBox().x + 2.5; + } + }); + } + + function mouseoutStyle(bar, selected) { + var _this = this; + + var clear = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + + if (!(selected || clear) || (selected && clear)) + bar.style({ + 'stroke-width': '1px', + fill: function fill(d) { + return _this.colorScale(d.key); + } + }); + } + + function mouseoutAttrib(bar, selected) { + var clear = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + + if (!(selected || clear) || (selected && clear)) + bar.attr({ + width: function width(d) { + return this.getBBox().width + 2.5; + }, + x: function x(d) { + return this.getBBox().x - 2.5; + } + }); + } + + function initListing() { + //Clear listing container. + this.listing.wrap.selectAll('*').remove(); + + //Capture data from selected bars. + var selectedData = d3 + .selectAll('rect.selected') + .data() + .flatMap(function(d) { + return d.values.raw; + }); + + //Feed data from selected bars into listing. + if (selectedData.length > 0) this.listing.init(selectedData); + else this.listing.init(this.filtered_data); + } function addBarClick() { var context = this; - var barGroups = this.svg.selectAll('.bar-group'); - var bars = this.svg.selectAll('.bar'); // will subtract and add to bar to offset increase in stroke-width and prevent bars // from overlapping as much when neighbors are both selected. - var mouseoverAttrib = { - width: function width(d) { - return this.getBBox().width - 2.5; - }, - x: function x(d) { - return this.getBBox().x + 2.5; - } - }; - var mouseoverStyle = { - 'stroke-width': '5px', - fill: 'black' - }; - var mouseoutAttrib = { - width: function width(d) { - return this.getBBox().width + 2.5; - }, - x: function x(d) { - return this.getBBox().x - 2.5; - } - }; - var mouseoutStyle = { - 'stroke-width': '1px', - fill: function fill(d) { - return context.colorScale(d.key); - } - }; - bars + this.bars = this.svg + .selectAll('.bar') .style('cursor', 'pointer') .on('mouseover', function() { - if (!d3.select(this).classed('selected')) d3.select(this).style(mouseoverStyle); - if (!d3.select(this).classed('selected')) d3.select(this).attr(mouseoverAttrib); + var bar = d3.select(this); + var selected = bar.classed('selected'); + + //Apply highlight attributes and styles to bar. + mouseoverStyle.call(context, bar, selected); + mouseoverAttrib.call(context, bar, selected); + //moveToFront causes an issue preventing onMouseout from firing in Internet Explorer so only call it in other browsers. if (!/trident/i.test(navigator.userAgent)) d3.select(this).moveToFront(); }) .on('mouseout', function() { - if (!d3.select(this).classed('selected')) d3.select(this).style(mouseoutStyle); - if (!d3.select(this).classed('selected')) d3.select(this).attr(mouseoutAttrib); - bars - .filter(function() { - return d3.select(this).classed('selected'); - }) - .moveToFront(); + var bar = d3.select(this); + var selected = bar.classed('selected'); + + //Apply default attributes and styles to bar. + mouseoutStyle.call(context, bar, selected); + mouseoutAttrib.call(context, bar, selected); + + //moveToFront causes an issue preventing onMouseout from firing in Internet Explorer so only call it in other browsers. + if (!/trident/i.test(navigator.userAgent)) + context.bars + .filter(function() { + return d3.select(this).classed('selected'); + }) + .moveToFront(); }) .on('click', function(d) { - // this doesn't need a style because mouseout isn't applied when the bar is selected - d3 - .select(this) - .classed('selected', d3.select(this).classed('selected') ? false : true); - context.listing.wrap.selectAll('*').remove(); - // feed listing data for all selected bars - context.listing.init( - d3 - .selectAll('rect.selected') - .data() - .flatMap(function(d) { - return d.values.raw; - }) - ); - context.wrap.select('#listing-instruction').style('display', 'none'); // remove bar instructions - // display filtered data if no bars are selected - if (d3.selectAll('rect.selected')[0].length === 0) { - context.listing.wrap.selectAll('*').remove(); - context.wrap.select('#listing-instruction').style('display', 'block'); - context.listing.init(context.filtered_data); - } + var bar = d3.select(this); + var selected = bar.classed('selected'); + + //Update selected class of clicked bar. + bar.classed('selected', !selected); + + //Re-initialize listing. + initListing.call(context); + }); + } + + function addBarDeselection() { + var _this = this; + + var context = this; + + this.overlay.on('click', function() { + _this.bars.each(function(d) { + var bar = d3.select(this); + var selected = bar.classed('selected'); + mouseoutStyle.call(context, bar, selected, true); + mouseoutAttrib.call(context, bar, selected, true); + bar.classed('selected', false); }); + initListing.call(_this); + }); + } + + function addNoDataIndicator() { + this.svg.select('.qo-no-data').remove(); + + if (this.filtered_data.length === 0) + this.svg + .append('text') + .classed('qo-no-data', true) + .attr({ + x: this.plot_width / 2, + y: this.plot_height / 2, + 'text-anchor': 'middle' + }) + .text('No queries selected. Verify that no filter selections are in conflict.'); } function onResize() { @@ -1477,22 +2091,34 @@ //Add bar click-ability. addBarClick.call(this); + + //Add bar deselection. + addBarDeselection.call(this); + + //Add informational text to the chart canvas when filters are in conflict. + addNoDataIndicator.call(this); } + function onDestroy() {} + + var chartCallbacks = { + onInit: onInit, + onLayout: onLayout, + onPreprocess: onPreprocess, + onDataTransform: onDataTransform, + onDraw: onDraw, + onResize: onResize, + onDestroy: onDestroy + }; + function onInit$1() {} - function resetListing() { + function addResetButton$1() { var _this = this; - this.wrap + this.resetButton = this.wrap .insert('button', ':first-child') - .attr('id', 'clear-listing') - .style({ - margin: '5px', - padding: '5px', - float: 'left', - display: 'block' - }) + .classed('qo-button qo-button--reset-listing', true) .text('Reset listing') .on('click', function() { _this.wrap.selectAll('*').remove(); @@ -1515,7 +2141,6 @@ } }); _this.chart.listing.init(_this.chart.filtered_data); - _this.chart.wrap.select('#listing-instruction').style('display', 'block'); }); } @@ -1525,10 +2150,10 @@ var table = this.table.node(); this.tableContainer = this.wrap .append('div') - .classed('query-table-container', true) + .classed('qo-table-container', true) .node(); - this.wrap.select('table').classed('query-table', true); // I want to ensure that no other webcharts tables get flipped upside down + this.wrap.select('table').classed('qo-table', true); // I want to ensure that no other webcharts tables get flipped upside down table.parentNode.insertBefore(this.tableContainer, table); this.tableContainer.appendChild(table); @@ -1536,12 +2161,14 @@ } function onLayout$1() { - resetListing.call(this); + addResetButton$1.call(this); addTableContainer.call(this); this.wrap.select('.sortable-container').classed('hidden', false); this.table.style('width', '100%').style('display', 'table'); } + function onPreprocess$1() {} + function manualSort() { var _this = this; @@ -1590,7 +2217,7 @@ this.draw(this.data.manually_sorted); } - function onClick() { + function updateColumnSorting() { var context = this; this.thead_cells.on('click', function(d) { @@ -1667,6 +2294,25 @@ }); } + function truncateCellText() { + var _this = this; + + if (this.data.raw.length) + this.tbody + .selectAll('td') + .attr('title', function(d) { + return d.text; + }) + .filter(function(d) { + return d.text.length > _this.chart.initialSettings.truncation_cutoff; + }) + .text(function(d) { + return ( + d.text.substring(0, _this.chart.initialSettings.truncation_cutoff) + '...' + ); + }); + } + function moveScrollBarLeft() { var _this = this; @@ -1680,7 +2326,11 @@ } function onDraw$1() { - onClick.call(this); + //Update default Webcharts column sorting. + updateColumnSorting.call(this); + + //Truncate cells with length greater than `settings.truncation_cutoff`. + truncateCellText.call(this); //Move table scrollbar all the way to the left. moveScrollBarLeft.call(this); @@ -1688,63 +2338,52 @@ function onDestroy$1() {} - function defineStyles() { - var styles = [ - '.query-table-container {' + - ' overflow-x: auto;' + - ' width : 100%;' + - ' transform: rotate(180deg);' + - ' -webkit-transform:rotate(180deg); ' + - '}', - '.query-table {' + - ' transform: rotate(180deg);' + - ' -webkit-transform:rotate(180deg); ' + - '}' - ]; - - //Attach styles to DOM. - this.style = document.createElement('style'); - this.style.type = 'text/css'; - this.style.innerHTML = styles.join('\n'); - document.getElementsByTagName('head')[0].appendChild(this.style); - } + var listingCallbacks = { + onInit: onInit$1, + onLayout: onLayout$1, + onPreprocess: onPreprocess$1, + onDraw: onDraw$1, + onDestroy: onDestroy$1 + }; function queryOverview$1(element, settings) { + //Settings var mergedSettings = Object.assign({}, configuration.settings, settings); var syncedSettings = configuration.syncSettings(mergedSettings); var syncedControlInputs = configuration.syncControlInputs( configuration.controlInputs, syncedSettings ); - var controls = webcharts.createControls(element, { + + //Layout and styles + var containers = layout(element); + styles(); + + //Controls + var controls = webcharts.createControls(containers.controls.node(), { location: 'top', inputs: syncedControlInputs }); - var chart = webcharts.createChart(element, syncedSettings, controls); - var listing = webcharts.createTable(element, { + controls.element = element; + + //Chart + var chart = webcharts.createChart(containers.chart.node(), syncedSettings, controls); + chart.element = element; + chart.initialSettings = clone(mergedSettings); + for (var callback in chartCallbacks) { + chart.on(callback.substring(2).toLowerCase(), chartCallbacks[callback]); + } //Listing + var listing = webcharts.createTable(containers.listing.node(), { sortable: false, exportable: syncedSettings.exportable }); - - chart.initialSettings = clone(mergedSettings); - chart.on('init', onInit); - chart.on('layout', onLayout); - chart.on('preprocess', onPreprocess); - chart.on('datatransform', onDataTransform); - chart.on('draw', onDraw); - chart.on('resize', onResize); - - listing.on('init', onInit$1); - listing.on('layout', onLayout$1); - listing.on('draw', onDraw$1); - listing.on('destroy', onDestroy$1); - + listing.element = element; + for (var _callback in listingCallbacks) { + listing.on(_callback.substring(2).toLowerCase(), listingCallbacks[_callback]); + } //Intertwine chart.listing = listing; listing.chart = chart; - //add Table stylesheet - defineStyles.call(listing); - return chart; } diff --git a/build/test-page/index.js b/build/test-page/index.js deleted file mode 100644 index d7d9e76..0000000 --- a/build/test-page/index.js +++ /dev/null @@ -1,23 +0,0 @@ -//Load local build if in local environment. -if (window.origin !== 'https://rhoinc.github.io') { - var head = document.getElementsByTagName('head')[0]; - var script = document.createElement('script'); - script.type = 'text/javascript'; - script.src = '../queryOverview.js'; - head.appendChild(script); -} - -d3.csv( - 'https://rawgit.com/RhoInc/viz-library/master/data/queries/queries.csv', - function(error,data) { - if (error) - console.log(error); - - var settings = {}; - var instance = queryOverview( - '#container', - settings - ); - instance.init(data); - } -); diff --git a/package-lock.json b/package-lock.json index da16085..81d30ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,95 @@ { "name": "query-overview", - "version": "1.2.3", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/node": { + "version": "10.12.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.9.tgz", + "integrity": "sha512-eajkMXG812/w3w4a1OcBlaTwsFPO5F7fJ/amy+tieQxEMWBlbV1JGSjkFM+zkHNf81Cad+dfIRA+IBkvmvdAeA==", + "dev": true + }, "adler-32": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz", "integrity": "sha1-aj5r8KY5ALoVZSgIyxXGgT0aXyU=", "optional": true, "requires": { - "exit-on-epipe": "1.0.1", - "printj": "1.1.2" + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "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.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" } }, "babel-helper-builder-binary-assignment-operator-visitor": { @@ -20,9 +98,9 @@ "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" }, "dependencies": { "babel-runtime": { @@ -31,8 +109,8 @@ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "core-js": "2.5.4", - "regenerator-runtime": "0.11.1" + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" } }, "babel-types": { @@ -41,10 +119,10 @@ "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", "dev": true, "requires": { - "babel-runtime": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.5", - "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" } }, "core-js": { @@ -85,9 +163,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" }, "dependencies": { "ansi-regex": { @@ -108,9 +186,9 @@ "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-messages": { @@ -119,7 +197,7 @@ "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", "dev": true, "requires": { - "babel-runtime": "6.26.0" + "babel-runtime": "^6.22.0" } }, "babel-runtime": { @@ -128,8 +206,8 @@ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "core-js": "2.5.4", - "regenerator-runtime": "0.11.1" + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" } }, "babel-traverse": { @@ -138,15 +216,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.4", - "lodash": "4.17.5" + "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" } }, "babel-types": { @@ -155,10 +233,10 @@ "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", "dev": true, "requires": { - "babel-runtime": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.5", - "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" } }, "babylon": { @@ -173,11 +251,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" } }, "core-js": { @@ -219,7 +297,7 @@ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "invariant": { @@ -228,7 +306,7 @@ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "dev": true, "requires": { - "loose-envify": "1.3.1" + "loose-envify": "^1.0.0" } }, "js-tokens": { @@ -249,7 +327,7 @@ "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", "dev": true, "requires": { - "js-tokens": "3.0.2" + "js-tokens": "^3.0.0" } }, "ms": { @@ -270,7 +348,7 @@ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "supports-color": { @@ -293,11 +371,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" }, "dependencies": { "ansi-regex": { @@ -318,9 +396,9 @@ "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-helper-function-name": { @@ -329,11 +407,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": { @@ -342,8 +420,8 @@ "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-messages": { @@ -352,7 +430,7 @@ "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", "dev": true, "requires": { - "babel-runtime": "6.26.0" + "babel-runtime": "^6.22.0" } }, "babel-runtime": { @@ -361,8 +439,8 @@ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "core-js": "2.5.4", - "regenerator-runtime": "0.11.1" + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" } }, "babel-template": { @@ -371,11 +449,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.5" + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" } }, "babel-traverse": { @@ -384,15 +462,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.4", - "lodash": "4.17.5" + "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" } }, "babel-types": { @@ -401,10 +479,10 @@ "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", "dev": true, "requires": { - "babel-runtime": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.5", - "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" } }, "babylon": { @@ -419,11 +497,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" } }, "core-js": { @@ -465,7 +543,7 @@ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "invariant": { @@ -474,7 +552,7 @@ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "dev": true, "requires": { - "loose-envify": "1.3.1" + "loose-envify": "^1.0.0" } }, "js-tokens": { @@ -495,7 +573,7 @@ "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", "dev": true, "requires": { - "js-tokens": "3.0.2" + "js-tokens": "^3.0.0" } }, "ms": { @@ -516,7 +594,7 @@ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "supports-color": { @@ -533,13 +611,32 @@ } } }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, "babel-plugin-external-helpers": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-external-helpers/-/babel-plugin-external-helpers-6.22.0.tgz", "integrity": "sha1-IoX0iwK9Xe3oUXXK+MYuhq3M76E=", "dev": true, "requires": { - "babel-runtime": "6.26.0" + "babel-runtime": "^6.22.0" }, "dependencies": { "babel-runtime": { @@ -548,8 +645,8 @@ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "core-js": "2.5.4", - "regenerator-runtime": "0.11.1" + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" } }, "core-js": { @@ -590,9 +687,9 @@ "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" }, "dependencies": { "babel-runtime": { @@ -601,8 +698,8 @@ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "core-js": "2.5.4", - "regenerator-runtime": "0.11.1" + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" } }, "core-js": { @@ -625,9 +722,9 @@ "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" }, "dependencies": { "babel-runtime": { @@ -636,8 +733,8 @@ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "core-js": "2.5.4", - "regenerator-runtime": "0.11.1" + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" } }, "core-js": { @@ -660,36 +757,36 @@ "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.11.3", - "invariant": "2.2.4", - "semver": "5.5.0" + "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": "^2.1.2", + "invariant": "^2.2.2", + "semver": "^5.3.0" }, "dependencies": { "ansi-regex": { @@ -710,9 +807,9 @@ "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-helper-call-delegate": { @@ -721,10 +818,10 @@ "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": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" } }, "babel-helper-define-map": { @@ -733,10 +830,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.5" + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" } }, "babel-helper-function-name": { @@ -745,11 +842,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": { @@ -758,8 +855,8 @@ "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": { @@ -768,8 +865,8 @@ "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", "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-optimise-call-expression": { @@ -778,8 +875,8 @@ "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": { @@ -788,9 +885,9 @@ "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", "dev": true, "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "lodash": "4.17.5" + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" } }, "babel-helper-replace-supers": { @@ -799,12 +896,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-messages": { @@ -813,7 +910,7 @@ "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", "dev": true, "requires": { - "babel-runtime": "6.26.0" + "babel-runtime": "^6.22.0" } }, "babel-plugin-check-es2015-constants": { @@ -822,7 +919,7 @@ "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", "dev": true, "requires": { - "babel-runtime": "6.26.0" + "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-arrow-functions": { @@ -831,7 +928,7 @@ "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", "dev": true, "requires": { - "babel-runtime": "6.26.0" + "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-block-scoped-functions": { @@ -840,7 +937,7 @@ "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", "dev": true, "requires": { - "babel-runtime": "6.26.0" + "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-block-scoping": { @@ -849,11 +946,11 @@ "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.5" + "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": { @@ -862,15 +959,15 @@ "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": { @@ -879,8 +976,8 @@ "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", "dev": true, "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" } }, "babel-plugin-transform-es2015-destructuring": { @@ -889,7 +986,7 @@ "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", "dev": true, "requires": { - "babel-runtime": "6.26.0" + "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-duplicate-keys": { @@ -898,8 +995,8 @@ "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", "dev": true, "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" } }, "babel-plugin-transform-es2015-for-of": { @@ -908,7 +1005,7 @@ "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", "dev": true, "requires": { - "babel-runtime": "6.26.0" + "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-function-name": { @@ -917,9 +1014,9 @@ "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-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" } }, "babel-plugin-transform-es2015-literals": { @@ -928,7 +1025,7 @@ "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", "dev": true, "requires": { - "babel-runtime": "6.26.0" + "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-modules-amd": { @@ -937,9 +1034,9 @@ "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": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" } }, "babel-plugin-transform-es2015-modules-commonjs": { @@ -948,10 +1045,10 @@ "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-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": { @@ -960,9 +1057,9 @@ "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", "dev": true, "requires": { - "babel-helper-hoist-variables": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" } }, "babel-plugin-transform-es2015-modules-umd": { @@ -971,9 +1068,9 @@ "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-modules-amd": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" } }, "babel-plugin-transform-es2015-object-super": { @@ -982,8 +1079,8 @@ "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", "dev": true, "requires": { - "babel-helper-replace-supers": "6.24.1", - "babel-runtime": "6.26.0" + "babel-helper-replace-supers": "^6.24.1", + "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-parameters": { @@ -992,12 +1089,12 @@ "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-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": { @@ -1006,8 +1103,8 @@ "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", "dev": true, "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" } }, "babel-plugin-transform-es2015-spread": { @@ -1016,7 +1113,7 @@ "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", "dev": true, "requires": { - "babel-runtime": "6.26.0" + "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-sticky-regex": { @@ -1025,9 +1122,9 @@ "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", "dev": true, "requires": { - "babel-helper-regex": "6.26.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" } }, "babel-plugin-transform-es2015-template-literals": { @@ -1036,7 +1133,7 @@ "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", "dev": true, "requires": { - "babel-runtime": "6.26.0" + "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-typeof-symbol": { @@ -1045,7 +1142,7 @@ "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", "dev": true, "requires": { - "babel-runtime": "6.26.0" + "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-unicode-regex": { @@ -1054,9 +1151,9 @@ "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", "dev": true, "requires": { - "babel-helper-regex": "6.26.0", - "babel-runtime": "6.26.0", - "regexpu-core": "2.0.0" + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "regexpu-core": "^2.0.0" } }, "babel-plugin-transform-regenerator": { @@ -1065,7 +1162,7 @@ "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", "dev": true, "requires": { - "regenerator-transform": "0.10.1" + "regenerator-transform": "^0.10.0" } }, "babel-plugin-transform-strict-mode": { @@ -1074,8 +1171,8 @@ "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", "dev": true, "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" } }, "babel-runtime": { @@ -1084,8 +1181,8 @@ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "core-js": "2.5.4", - "regenerator-runtime": "0.11.1" + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" } }, "babel-template": { @@ -1094,11 +1191,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.5" + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" } }, "babel-traverse": { @@ -1107,15 +1204,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.4", - "lodash": "4.17.5" + "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" } }, "babel-types": { @@ -1124,10 +1221,10 @@ "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", "dev": true, "requires": { - "babel-runtime": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.5", - "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" } }, "babylon": { @@ -1142,11 +1239,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" } }, "core-js": { @@ -1188,7 +1285,7 @@ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "invariant": { @@ -1197,7 +1294,7 @@ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "dev": true, "requires": { - "loose-envify": "1.3.1" + "loose-envify": "^1.0.0" } }, "js-tokens": { @@ -1224,7 +1321,7 @@ "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", "dev": true, "requires": { - "js-tokens": "3.0.2" + "js-tokens": "^3.0.0" } }, "ms": { @@ -1257,9 +1354,9 @@ "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", "dev": true, "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "private": "0.1.8" + "babel-runtime": "^6.18.0", + "babel-types": "^6.19.0", + "private": "^0.1.6" } }, "regexpu-core": { @@ -1268,9 +1365,9 @@ "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", "dev": true, "requires": { - "regenerate": "1.3.3", - "regjsgen": "0.2.0", - "regjsparser": "0.1.5" + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" } }, "regjsgen": { @@ -1285,7 +1382,7 @@ "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", "dev": true, "requires": { - "jsesc": "0.5.0" + "jsesc": "~0.5.0" } }, "strip-ansi": { @@ -1294,7 +1391,7 @@ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "supports-color": { @@ -1317,576 +1414,263 @@ "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", "dev": true, "requires": { - "babel-core": "6.26.0", - "babel-runtime": "6.26.0", - "core-js": "2.5.4", - "home-or-tmp": "2.0.0", - "lodash": "4.17.5", - "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" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "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-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "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.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "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" + } + }, + "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", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browserslist": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", + "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000792", + "electron-to-chromium": "^1.3.30" + } + }, + "buffer-from": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", + "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==", + "optional": true + }, + "caniuse-lite": { + "version": "1.0.30000821", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000821.tgz", + "integrity": "sha512-qyYay02wr/5k7PO86W+LKFaEUZfWIvT65PaXuPP16jkSpgZGIsSstHKiYAPVLjTj98j2WnWwZg8CjXPx7UIPYg==", + "dev": true + }, + "cfb": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.0.5.tgz", + "integrity": "sha512-z1BN+JkopTE4vYu0sx25da2ZFurcN8gUKcBpT2ThCDlNFtWBozId8AHMs4OS7jTPLCJYK30Ud7QgcioyGkkkbg==", + "optional": true, + "requires": { + "commander": "^2.14.1", + "printj": "~1.1.2" }, "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - } - }, - "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=", - "dev": true, - "requires": { - "babel-code-frame": "6.26.0", - "babel-generator": "6.26.1", - "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.5", - "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.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.5", - "source-map": "0.5.7", - "trim-right": "1.0.1" - } - }, - "babel-helpers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, - "requires": { - "core-js": "2.5.4", - "regenerator-runtime": "0.11.1" - } - }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "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.5" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "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.4", - "lodash": "4.17.5" - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.5", - "to-fast-properties": "1.0.3" - } + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "optional": true + } + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "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": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.3.8.tgz", + "integrity": "sha1-Ty5dfAl13ij4hJgFjcta/KtqX3E=", + "optional": true, + "requires": { + "commander": "^2.15.1", + "concat-stream": "^1.6.2", + "voc": "^1.1.0" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "optional": true + } + } + }, + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "optional": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "optional": 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 + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, - "balanced-match": { + "isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "optional": true }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "optional": true }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, + "readable-stream": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz", + "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==", + "optional": 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" + "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.0.3", + "util-deprecate": "~1.0.1" } }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "convert-source-map": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", - "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", - "dev": true - }, - "core-js": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.4.tgz", - "integrity": "sha1-8si/GB8qgLkvNgEhQpzmOi8K6uA=", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "optional": true, "requires": { - "ms": "2.0.0" + "safe-buffer": "~5.1.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.1" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "home-or-tmp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", - "dev": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "1.3.1" - } - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", - "dev": true - }, - "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.5", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", - "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==", - "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" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-tmpdir": { + "util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "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" - } - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "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 - }, - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", - "dev": true, - "requires": { - "source-map": "0.5.7" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "dev": true - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "optional": true } } }, - "browserslist": { - "version": "2.11.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", - "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", "dev": true, "requires": { - "caniuse-lite": "1.0.30000821", - "electron-to-chromium": "1.3.41" + "safe-buffer": "~5.1.1" } }, - "buffer-from": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", - "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==", - "optional": true - }, - "caniuse-lite": { - "version": "1.0.30000821", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000821.tgz", - "integrity": "sha512-qyYay02wr/5k7PO86W+LKFaEUZfWIvT65PaXuPP16jkSpgZGIsSstHKiYAPVLjTj98j2WnWwZg8CjXPx7UIPYg==", + "core-js": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==", "dev": true }, - "cfb": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.0.5.tgz", - "integrity": "sha512-z1BN+JkopTE4vYu0sx25da2ZFurcN8gUKcBpT2ThCDlNFtWBozId8AHMs4OS7jTPLCJYK30Ud7QgcioyGkkkbg==", - "optional": true, - "requires": { - "commander": "2.15.1", - "printj": "1.1.2" - }, - "dependencies": { - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "optional": true - } - } - }, - "codepage": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.3.8.tgz", - "integrity": "sha1-Ty5dfAl13ij4hJgFjcta/KtqX3E=", - "optional": true, - "requires": { - "commander": "2.15.1", - "concat-stream": "1.6.2", - "voc": "1.1.0" - }, - "dependencies": { - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "optional": true - } - } - }, - "colors": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", - "optional": true - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "optional": true, - "requires": { - "buffer-from": "1.0.0", - "inherits": "2.0.3", - "readable-stream": "2.3.5", - "typedarray": "0.0.6" - }, - "dependencies": { - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "optional": true - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "optional": true - }, - "readable-stream": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz", - "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==", - "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.1", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "optional": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "optional": true - } - } - }, "crc-32": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", "optional": true, "requires": { - "exit-on-epipe": "1.0.1", - "printj": "1.1.2" + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" } }, "d3": { @@ -1894,12 +1678,48 @@ "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz", "integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g=" }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "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.41", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.41.tgz", "integrity": "sha1-fjNkPgDNhe39F+BBlPbQDnNzcjU=", "dev": true }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "estree-walker": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.2.1.tgz", + "integrity": "sha1-va/oCVOD2EFNXcLs9MkXO225QS4=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, "exit-on-epipe": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", @@ -1911,19 +1731,68 @@ "integrity": "sha1-V3Z3t/3L5vr3xGHxgB00E3zaQ1Q=", "optional": true }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.1" + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, "js-xlsx": { "version": "0.8.22", "resolved": "https://registry.npmjs.org/js-xlsx/-/js-xlsx-0.8.22.tgz", "integrity": "sha512-3N4a9RBHTr777rxxlvwJVpC+er/neRC+40sm2M/g3RIpWiCJG0iyaGJa8Za1K3NvjhZcKn9Sz5n36TY9ti5RMQ==", "optional": true, "requires": { - "adler-32": "1.2.0", - "cfb": "1.0.5", - "codepage": "1.3.8", - "commander": "2.15.1", - "crc-32": "1.2.0", + "adler-32": "^1.2.0", + "cfb": ">=0.10.0", + "codepage": "~1.3.6", + "commander": "^2.15.1", + "crc-32": "^1.2.0", "jszip": "2.4.0", - "ssf": "0.8.2" + "ssf": "~0.8.1" }, "dependencies": { "commander": { @@ -1934,21 +1803,102 @@ } } }, + "jsesc": { + "version": "1.3.0", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "http://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.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" } }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, "pako": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", "optional": true }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, "prettier": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.11.1.tgz", @@ -1960,584 +1910,54 @@ "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==" }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "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.0" + } + }, "rollup": { - "version": "0.50.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.50.1.tgz", - "integrity": "sha512-XwrnqjSTk+yR8GbP6hiJuVe83MVmBw/gm4P3qP34A10fRXvv6ppl0ZUg1+Pj1tIZSR/aw5ZaILLEiVxwXIAdAw==", - "dev": true + "version": "0.66.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.66.6.tgz", + "integrity": "sha512-J7/SWanrcb83vfIHqa8+aVVGzy457GcjA6GVZEnD0x2u4OnOd0Q1pCrEoNe8yLwM6z6LZP02zBT2uW0yh5TqOw==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "@types/node": "*" + } }, "rollup-plugin-babel": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-3.0.3.tgz", - "integrity": "sha512-5kzM/Rr4jQSRPLc2eN5NuD+CI/6AAy7S1O18Ogu4U3nq1Q42VJn0C9EMtqnvxtfwf1XrezOtdA9ro1VZI5B0mA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-3.0.7.tgz", + "integrity": "sha512-bVe2y0z/V5Ax1qU8NX/0idmzIwJPdUGu8Xx3vXH73h0yGjxfv2gkFI82MBVg49SlsFlLTBadBHb67zy4TWM3hA==", "dev": true, "requires": { - "rollup-pluginutils": "1.5.2" - }, - "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "estree-walker": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.2.1.tgz", - "integrity": "sha1-va/oCVOD2EFNXcLs9MkXO225QS4=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "rollup-pluginutils": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz", - "integrity": "sha1-HhVud4+UtyVb+hs9AXi+j1xVJAg=", - "dev": true, - "requires": { - "estree-walker": "0.2.1", - "minimatch": "3.0.4" - } - } + "rollup-pluginutils": "^1.5.0" } }, - "rollup-watch": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/rollup-watch/-/rollup-watch-4.3.1.tgz", - "integrity": "sha512-6yjnIwfjpSrqA8IafyIu7fsEyeImNR4aDjA1bQ7KWeVuiA+Clfsx8+PGQkyABWIQzmauQ//tIJ5wAxLXsXs8qQ==", + "rollup-pluginutils": { + "version": "1.5.2", + "resolved": "http://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz", + "integrity": "sha1-HhVud4+UtyVb+hs9AXi+j1xVJAg=", "dev": true, "requires": { - "chokidar": "1.7.0", - "require-relative": "0.8.7", - "rollup-pluginutils": "2.0.1" - }, - "dependencies": { - "anymatch": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", - "dev": true, - "requires": { - "micromatch": "2.3.11", - "normalize-path": "2.1.1" - } - }, - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "1.1.0" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "async-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "binary-extensions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", - "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "dev": true, - "requires": { - "expand-range": "1.8.2", - "preserve": "0.2.0", - "repeat-element": "1.1.2" - } - }, - "chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", - "dev": true, - "requires": { - "anymatch": "1.3.2", - "async-each": "1.0.1", - "glob-parent": "2.0.0", - "inherits": "2.0.3", - "is-binary-path": "1.0.1", - "is-glob": "2.0.1", - "path-is-absolute": "1.0.1", - "readdirp": "2.1.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "estree-walker": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.3.1.tgz", - "integrity": "sha1-5rGlHPcpJSTnI3wxLl/mZgwc4ao=", - "dev": true - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true, - "requires": { - "is-posix-bracket": "0.1.1" - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true, - "requires": { - "fill-range": "2.2.3" - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "dev": true, - "requires": { - "is-extglob": "1.0.0" - } - }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", - "dev": true - }, - "fill-range": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", - "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", - "dev": true, - "requires": { - "is-number": "2.1.0", - "isobject": "2.1.0", - "randomatic": "1.1.7", - "repeat-element": "1.1.2", - "repeat-string": "1.6.1" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "1.0.2" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "dev": true, - "requires": { - "glob-parent": "2.0.0", - "is-glob": "2.0.1" - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "dev": true, - "requires": { - "is-glob": "2.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "1.11.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "dev": true, - "requires": { - "is-primitive": "2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "1.0.0" - } - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - } - }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", - "dev": true - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - }, - "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true, - "requires": { - "arr-diff": "2.0.0", - "array-unique": "0.2.1", - "braces": "1.8.5", - "expand-brackets": "0.1.5", - "extglob": "0.3.2", - "filename-regex": "2.0.1", - "is-extglob": "1.0.0", - "is-glob": "2.0.1", - "kind-of": "3.2.2", - "normalize-path": "2.1.1", - "object.omit": "2.0.1", - "parse-glob": "3.0.4", - "regex-cache": "0.4.4" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "1.1.0" - } - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "dev": true, - "requires": { - "for-own": "0.1.5", - "is-extendable": "0.1.1" - } - }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "dev": true, - "requires": { - "glob-base": "0.3.0", - "is-dotfile": "1.0.3", - "is-extglob": "1.0.0", - "is-glob": "2.0.1" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "randomatic": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", - "dev": true, - "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "readable-stream": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz", - "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==", - "dev": 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.1", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" - } - }, - "readdirp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "minimatch": "3.0.4", - "readable-stream": "2.3.5", - "set-immediate-shim": "1.0.1" - } - }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "dev": true, - "requires": { - "is-equal-shallow": "0.1.3" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "require-relative": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", - "integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=", - "dev": true - }, - "rollup-pluginutils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.0.1.tgz", - "integrity": "sha1-fslbNXP2VDpGpkYb2afFRFJdD8A=", - "dev": true, - "requires": { - "estree-walker": "0.3.1", - "micromatch": "2.3.11" - } - }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - } + "estree-walker": "^0.2.1", + "minimatch": "^3.0.2" } }, "safe-buffer": { @@ -2551,6 +1971,27 @@ "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", "dev": true }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "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 + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "^0.5.6" + } + }, "ssf": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.8.2.tgz", @@ -2559,9 +2000,36 @@ "requires": { "colors": "0.6.2", "frac": "0.3.1", - "voc": "1.1.0" + "voc": "^1.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" } }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -2578,8 +2046,8 @@ "resolved": "https://registry.npmjs.org/webcharts/-/webcharts-1.10.0.tgz", "integrity": "sha512-YeHYeYcsWNa2SO0BWSiUl4xGDQB4i3zyIdZTFeAGbxK8mcObCdKudOj9umA7IgSIRM2yUp7XEWj2x17zPgKB7w==", "requires": { - "d3": "3.5.17", - "js-xlsx": "0.8.22" + "d3": "^3", + "js-xlsx": "^0.8.22" } } } diff --git a/package.json b/package.json index c91f342..2be6937 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "query-overview", - "version": "1.2.3", + "version": "2.0.0", "description": "Interactive bar chart for exploration of query data from clinical trials", + "module": "./src/index.js", "main": "./build/queryOverview.js", - "module": "./src/wrapper.js", "author": "Rho, Inc.", "license": "MIT", "dependencies": { @@ -11,22 +11,25 @@ "webcharts": "~1" }, "scripts": { - "build": "npm run bundle && npm run format && npm run build-md && npm run test-page", - "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-src": "prettier --print-width=100 --tab-width=4 --single-quote --write \"./src/**/!(*styles).js\"", "format-bundle": "prettier --print-width=100 --tab-width=4 --single-quote --write ./build/*.js", - "test-page": "start chrome ./build/test-page/index.html && start firefox ./build/test-page/index.html && start iexplore file://%CD%/build/test-page/index.html", + "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-register": "^6.26.0", "prettier": "^1.7.4", - "rollup": "^0.50.0", - "rollup-plugin-babel": "^3.0.2", - "rollup-watch": "^4.3.1" + "rollup": "^0.66.6", + "rollup-plugin-babel": "^3.0.2" } } diff --git a/scripts/configuration-markdown.js b/scripts/build-configuration-wiki.js similarity index 73% rename from scripts/configuration-markdown.js rename to scripts/build-configuration-wiki.js index b96f82d..e974dbf 100644 --- a/scripts/configuration-markdown.js +++ b/scripts/build-configuration-wiki.js @@ -1,8 +1,12 @@ -var pkg = require('../package'), - schema = require('../settings-schema'), - properties = schema.properties, - markdown = [], - fs = require('fs'); +require('babel-register'); +const fs = require('fs'); +const pkg = require('../package'); +const schema = require('../settings-schema'); +const properties = schema.properties; +const settings = require('../src/configuration/rendererSettings.js').default; +const chartSettings = require('../src/configuration/chartSettings.js').default; +const listingSettings = require('../src/configuration/listingSettings.js').default; +const markdown = []; function setDefault(setting) { let settingDefault = '**default:** '; @@ -51,7 +55,7 @@ function setDefault(setting) { markdown.push(`## settings.${property}`); markdown.push(`\`${setting.type}\``); markdown.push(``); - markdown.push(`${setting.description}`); + markdown.push(`${setting.description || setting.title}`); if (setting.type !== 'object') { markdown.push(``); @@ -64,7 +68,7 @@ function setDefault(setting) { markdown.push(`### settings.${property}.${subProperty}`); markdown.push(`\`${subSetting.type}\``); markdown.push(``); - markdown.push(`${subSetting.title}`); + markdown.push(`${subSetting.description || subSetting.title}`); markdown.push(``); markdown.push(setDefault(subSetting)); }); @@ -78,7 +82,7 @@ function setDefault(setting) { markdown.push(`### settings.${property}[].${subProperty}`); markdown.push(`\`${subSetting.type}\``); markdown.push(``); - markdown.push(`${subSetting.title}`); + markdown.push(`${subSetting.description || subSetting.title}`); markdown.push(``); markdown.push(setDefault(subSetting)); }); @@ -95,29 +99,33 @@ function setDefault(setting) { Webcharts settings \------------------------------------------------------------------------------------------------*/ - var webchartsSettingsFlag = 0, - webchartsSettings = fs.readFileSync('./src/configuration/webchartsSettings.js', 'utf8') - .split('\n') - 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('```'); + markdown.push(`The objects below contain Webcharts settings for each display as of version ${schema.version} of the ${pkg.name.split('-').map(str => str.substring(0,1).toUpperCase() + str.substring(1).toLowerCase()).join(' ')}.`); + + [ + chartSettings, + listingSettings, + ].forEach(settingsFx => { + const settings = JSON.stringify(settingsFx(), null, 4); + const display = settingsFx.name.replace('Settings', ''); + + markdown.push(``); + markdown.push(`## ${display.substring(0,1).toUpperCase()}${display.substring(1)}`); + markdown.push('```'); + markdown.push(settings); + markdown.push('```'); + }); /*------------------------------------------------------------------------------------------------\ Configuration markdown \------------------------------------------------------------------------------------------------*/ fs.writeFile( - './scripts/configuration.md', + './scripts/configuration-wiki.md', markdown.join('\n'), (err) => { if (err) console.log(err); - console.log('The configuration markdown file was built!'); + 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..e81263a --- /dev/null +++ b/scripts/build-data-guidelines-wiki.js @@ -0,0 +1,80 @@ +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(/name of variable that (captures )?/, '')}|${ + 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 + 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..4b72155 --- /dev/null +++ b/scripts/check-settings-schema.js @@ -0,0 +1,26 @@ +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`); +} diff --git a/scripts/configuration.md b/scripts/configuration-wiki.md similarity index 50% rename from scripts/configuration.md rename to scripts/configuration-wiki.md index 422d741..5bcd89b 100644 --- a/scripts/configuration.md +++ b/scripts/configuration-wiki.md @@ -3,7 +3,7 @@ The most straightforward way to customize query-overview is by using a configura In addition to the standard Webcharts settings several custom settings not available in the base Webcharts library have been added to query-overview 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 query-overview setting as of version 1.2.3. +The sections below describe each query-overview setting as of version 2.0.0. ## settings.form_col `string` @@ -37,58 +37,55 @@ field variable name field description variable name -**default:** `"fieldname"` +**default:** `"null"` -## settings.marking_group_col +## settings.site_col `string` -query origin variable name +site variable name -**default:** `"markinggroup"` +**default:** `"sitename"` -## settings.visit_col +## settings.marking_group_col `string` -visit/folder variable name +query origin variable name -**default:** `"folderoid"` +**default:** `"markinggroup"` -## settings.open_col +## settings.visit_col `string` -variable name for how long a query has been open +visit/folder variable name -**default:** `"open_time"` +**default:** `"folderoid"` -## settings.open_category_col +## settings.color_by_col `string` -Categorical version of query open time variable +coloring variable name of query categorization: query age, query status, or any custom categorization; note that _queryage_ is derived by the renderer -**default:** `"Query Open Time Category"` +**default:** `"queryage"` -## settings.open_category_order +## settings.age_statuses `array` -an array of query open time that dictates how they are ordered in the chart +an array of query statuses for which query age will be derived **default:** ``` [ - "0-7 days", - "8-14 days", - "15-30 days", - ">30 days" + "Open" ] ``` @@ -97,42 +94,46 @@ an array of query open time that dictates how they are ordered in the chart ## settings.age_col `string` -query age variable name +query age variable measured in days between query open date and data snapshot date, query response date, or query resolution date for open, answered, and resolved queries, respectively **default:** `"qdays"` -## settings.age_category_col -`string` - -Categorical version of query age variable - -**default:** `"Query Age Category"` - - - -## settings.age_category_order +## settings.age_cutoffs `array` -an array of query age categories that dictates how they are ordered in the legend and chart +an array of query age cutoffs for which query age range will be derived -**default:** none +**default:** +``` +[ + 14, + 28, + 56, + 112 +] +``` -## settings.age_category_colors +## settings.age_range_colors `array` -an array of colors that determines the colors for query age categories +an array of colors with which to color query age range categories; only as many colors as there are query age range categories will be used, from darkest to lightest **default:** ``` [ - "#fcae91", - "#fb6a4a", - "#de2d26", - "#a50f15" + "#ffffcc", + "#ffeda0", + "#fed976", + "#feb24c", + "#fd8d3c", + "#fc4e2a", + "#e31a1c", + "#bd0026", + "#800026" ] ``` @@ -172,10 +173,44 @@ an array of colors that determines the colors for query statuses **default:** ``` [ - "#fb9a99", - "#fdbf6f", - "#1f78b4", - "gray" + "#fd8d3c", + "#4daf4a", + "#377eb8", + "#999999" +] +``` + + + +## settings.recency_category_col +`string` + +query recency category variable name; overrides `recency_col` + +**default:** `"open_time"` + + + +## settings.recency_col +`string` + +query recency variable measured in days between query open date and data snapshot date, regardless of query status + +**default:** `"odays"` + + + +## settings.recency_cutoffs +`array` + +an array of query recency cutoffs for which query recency range will be derived + +**default:** +``` +[ + 7, + 14, + 30 ] ``` @@ -184,21 +219,21 @@ an array of colors that determines the colors for query statuses ## settings.groups `array` -an array of group-by variables by which to group queries on the y-axis +an array of variables by which to count queries; each value of the variable is plotted on the y-axis **default:** none ### settings.groups[].value_col `string` -Group-by Variable +group-by variable name **default:** none ### settings.groups[].label `string` -Group-by Label +group-by variable label **default:** none @@ -207,101 +242,101 @@ Group-by Label ## settings.status_groups `array` -an array of Stratification variables by which to stratify the stacked bars +an array of variables with which to stratify each group-by value, each value of which plots as a component of a stacked bar when Bar Arrangement is set to _Stacked_ or as individual bars when Bar Arrangement is set to _grouped_ **default:** none ### settings.status_groups[].value_col `string` -Stratification Variable +Stratification variable name **default:** none ### settings.status_groups[].label `string` -Stratification Label +Stratification variable label **default:** none ### settings.status_groups[].order `array` -Stratification Order +Stratification variable order **default:** none ### settings.status_groups[].colors `array` -Stratification Colors +Stratification variable colors **default:** none -## settings.site_col -`string` - -site variable name - -**default:** `"sitename"` - - - ## settings.filters `array` -an array of filter variables with which to filter the data +an array of variables with which to filter the data **default:** none ### settings.filters[].value_col `string` -Filter Variable +filter variable name **default:** none ### settings.filters[].label `string` -Filter Label +filter variable label **default:** none +## settings.dropdown_size +`number` + +controls the maximum number of options that appear in the multi-select dropdowns before a scrollbar appears + +**default:** `6` + + + ## settings.details `array` -an array of detail variables which will print in the detail listing +an array of variables which will print in the listing; if unspecified all variables in data will appear in listing **default:** none ### settings.details[].value_col `string` -Detail Listing Variable +detail listing variable name **default:** none ### settings.details[].label `string` -Detail Listing Column Header +detail listing column header **default:** none -## settings.dropdown_size -`number` +## settings.bar_arrangement +`string` -controls the maximum number of options that appear in the multi-select dropdowns before a scrollbar appears +controls arrangement of bars, either stacked or grouped side-by-side -**default:** `6` +**default:** `"stacked"` @@ -323,25 +358,69 @@ sort groups on the y-axis alphanumerically; by default groups are sorted by desc -## settings.exportable +## settings.truncate_listing_cells `boolean` -allow the export of data to .csv via a button beneath the detail listing +optionally truncate cell text past a certain number of characters **default:** `true` -## settings.nRowsPerPage +## settings.truncation_cutoff `number` -the number of rows displayed per page. +cell text past this cutoff will be truncated and the full text will be captured in a tooltip -**default:** `10` +**default:** `100` # Webcharts settings -The object below contains each Webcharts setting as of version 1.2.3. +The objects below contain Webcharts settings for each display as of version 2.0.0 of the Query Overview. + +## Chart +``` +{ + "x": { + "label": "# of Queries", + "column": null, + "behavior": "flex" + }, + "y": { + "type": "ordinal", + "column": null, + "label": "Form", + "sort": null + }, + "marks": [ + { + "type": "bar", + "per": [ + null + ], + "split": null, + "arrange": null, + "summarizeX": "count", + "tooltip": null + } + ], + "color_by": null, + "color_dom": null, + "legend": { + "location": "top", + "label": null, + "order": null + }, + "margin": { + "right": "50" + }, + "range_band": 25 +} +``` +## Listing ``` -{ x: { label: '# of Queries', behavior: 'flex' }, y: { type: 'ordinal', column: null, // set in syncSettings() label: 'Form', sort: 'total-descending' }, marks: [ { type: 'bar', per: [null], // set in syncSettings() split: null, // set in syncSettings() arrange: 'stacked', summarizeX: 'count', tooltip: null // set in syncSettings() } ], color_by: null, // set in syncSettings() color_dom: null, // set in syncSettings() legend: { location: 'top', // label: 'Query Status', label: null, order: null // set in syncSettings() }, range_band: 15, margin: { right: '50' } // room for count annotation }; } +{ + "nRowsPerPage": 25, + "exportable": true +} ``` \ 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..1eab96f --- /dev/null +++ b/scripts/data-guidelines-wiki.md @@ -0,0 +1,25 @@ +The Query Overview 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 query data with **one row per query** plus the required variables specified below. + +## Data structure +one record per query + +## Data specification +required and optional variables: + +| Setting | Default | Data Type | Description | Required? | +|:--------|:--------|:----------|:------------|:---------:| +|`form_col`|_formoid_|**character**|form variable name|**Yes**| +|`formDescription_col`|_ecrfpagename_|**character**|form description variable name|**Yes**| +|`field_col`|_fieldname_|**character**|field variable name|**Yes**| +|`fieldDescription_col`||**character**|field description variable name|| +|`site_col`|_sitename_|**character**|site variable name|**Yes**| +|`marking_group_col`|_markinggroup_|**character**|query origin variable name|**Yes**| +|`visit_col`|_folderoid_|**character**|visit/folder variable name|**Yes**| +|`age_col`|_qdays_|**numeric**|query age variable measured in days between query open date and data snapshot date, query response date, or query resolution date for open, answered, and resolved queries, respectively|**Yes**| +|`status_col`|_querystatus_|**character**|query status variable name|**Yes**| +|`recency_category_col`|_open_time_|**character**|query recency category variable name; overrides `recency_col`|| +|`recency_col`|_odays_|**numeric**|query recency variable measured in days between query open date and data snapshot date, regardless of query status|| +|`groups[]`||**either**|an array of variables by which to count queries; each value of the variable is plotted on the y-axis|| +|`status_groups[]`||**either**|an array of variables with which to stratify each group-by value, each value of which plots as a component of a stacked bar when Bar Arrangement is set to _Stacked_ or as individual bars when Bar Arrangement is set to _grouped_|| +|`filters[]`||**either**|an array of variables with which to filter the data|| +|`details[]`||**either**|an array of variables which will print in the listing; if unspecified all variables in data will appear in listing|| \ No newline at end of file diff --git a/settings-schema.json b/settings-schema.json index 1ac0af7..db2a069 100644 --- a/settings-schema.json +++ b/settings-schema.json @@ -2,116 +2,151 @@ "title": "settings", "description": "JSON schema for the configuration of query-overview", "overview": "The most straightforward way to customize query-overview is by using a configuration object whose properties describe the behavior and appearance of the chart. Since query-overview is a Webcharts `chart` object, many default Webcharts settings are set in the [defaultSettings.js file](https://github.com/RhoInc/query-overview/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 query-overview to facilitate data mapping and other custom functionality. These custom settings are described in detail below. All defaults can be overwritten by users.", - "version": "1.2.3", + "version": "2.0.0", "type": "object", + "data-guidelines": "The Query Overview 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 query data with **one row per query** plus the required variables specified below.", + "data-structure": "one record per query", "properties": { "form_col": { "title": "Form Variable", "description": "form variable name", "type": "string", - "default": "formoid" + "default": "formoid", + "data-mapping": true, + "data-type": "character", + "required": true }, "formDescription_col": { "title": "Form Description Variable", "description": "form description variable name", "type": "string", - "default": "ecrfpagename" + "default": "ecrfpagename", + "data-mapping": true, + "data-type": "character", + "required": true }, "field_col": { "title": "Field Variable", "description": "field variable name", "type": "string", - "default": "fieldname" + "default": "fieldname", + "data-mapping": true, + "data-type": "character", + "required": true }, "fieldDescription_col": { "title": "Field Description Variable", "description": "field description variable name", "type": "string", - "default": "fieldname" + "default": null, + "data-mapping": true, + "data-type": "character", + "required": false + }, + "site_col": { + "title": "Site Variable", + "description": "site variable name", + "type": "string", + "default": "sitename", + "data-mapping": true, + "data-type": "character", + "required": true }, "marking_group_col": { "title": "Marking Group Variable", "description": "query origin variable name", "type": "string", - "default": "markinggroup" + "default": "markinggroup", + "data-mapping": true, + "data-type": "character", + "required": true }, "visit_col": { "title": "Visit/Folder Variable", "description": "visit/folder variable name", "type": "string", - "default": "folderoid" - }, - "open_col": { - "title": "Query Open Time Variable", - "description": "variable name for how long a query has been open", - "type": "string", - "default": "open_time" + "default": "folderoid", + "data-mapping": true, + "data-type": "character", + "required": true }, - "open_category_col": { - "title": "Query Open Time Category Variable", - "description": "Categorical version of query open time variable", + "color_by_col": { + "title": "Status Group Variable", + "description": "coloring variable name of query categorization: query age, query status, or any custom categorization; note that _queryage_ is derived by the renderer", "type": "string", - "default": "Query Open Time Category" + "default": "queryage", + "enum": [ + "queryage", + "querystatus" + ] }, - "open_category_order": { - "title": "Query Open Time Order", - "description": "an array of query open time that dictates how they are ordered in the chart", + "age_statuses": { + "title": "Query Statuses Applicable to Query Age", + "description": "an array of query statuses for which query age will be derived", "type": "array", "items": { - "title": "Query Open Time Category", - "description": "query open time category value", + "title": "Query Status", + "description": "query status value", "type": "string" }, "defaultObject": [ - "0-7 days", - "8-14 days", - "15-30 days", - ">30 days" + "Open" ] }, "age_col": { - "title": "Query Age Variable", - "description": "query age variable name", + "title": "Query Age", + "description": "query age variable measured in days between query open date and data snapshot date, query response date, or query resolution date for open, answered, and resolved queries, respectively", "type": "string", - "default": "qdays" + "default": "qdays", + "data-mapping": true, + "data-type": "numeric", + "required": true }, - "age_category_col": { - "title": "Query Age Category Variable", - "description": "Categorical version of query age variable", - "type": "string", - "default": "Query Age Category" - }, - "age_category_order": { - "title": "Query Age Category Order", - "description": "an array of query age categories that dictates how they are ordered in the legend and chart", + "age_cutoffs": { + "title": "Query Age Cutoffs", + "description": "an array of query age cutoffs for which query age range will be derived", "type": "array", "items": { - "title": "Query Age Category", - "description": "query age category value", - "type": "string" - } + "title": "Query Age", + "description": "query age measured in days between query open date and data snapshot date", + "type": "number" + }, + "defaultObject": [ + 14, + 28, + 56, + 112 + ] }, - "age_category_colors": { - "title": "Query Age Category Color", - "description": "an array of colors that determines the colors for query age categories", + "age_range_colors": { + "title": "Query Age Range Colors", + "description": "an array of colors with which to color query age range categories; only as many colors as there are query age range categories will be used, from darkest to lightest", "type": "array", "items": { - "title": "Query Age Category", - "description": "query age category value", + "title": "Query Age Range Color", + "description": "query age range color", "type": "string" }, "defaultObject": [ - "#fcae91", - "#fb6a4a", - "#de2d26", - "#a50f15" + "#ffffcc", + "#ffeda0", + "#fed976", + "#feb24c", + "#fd8d3c", + "#fc4e2a", + "#e31a1c", + "#bd0026", + "#800026" ] }, "status_col": { "title": "Query Status Variable", "description": "query status variable name", "type": "string", - "default": "querystatus" + "default": "querystatus", + "data-mapping": true, + "data-type": "character", + "required": true }, "status_order": { "title": "Query Status Order", @@ -139,15 +174,48 @@ "type": "string" }, "defaultObject": [ - "#fb9a99", - "#fdbf6f", - "#1f78b4", - "gray" + "#fd8d3c", + "#4daf4a", + "#377eb8", + "#999999" + ] + }, + "recency_category_col": { + "title": "Query Recency Category", + "description": "query recency category variable name; overrides `recency_col`", + "type": "string", + "default": "open_time", + "data-mapping": true, + "data-type": "character", + "required": false + }, + "recency_col": { + "title": "Query Recency", + "description": "query recency variable measured in days between query open date and data snapshot date, regardless of query status", + "type": "string", + "default": "odays", + "data-mapping": true, + "data-type": "numeric", + "required": false + }, + "recency_cutoffs": { + "title": "Query Recency Cutoffs", + "description": "an array of query recency cutoffs for which query recency range will be derived", + "type": "array", + "items": { + "title": "Query Recency", + "description": "query recency measured in days between query open date and data snapshot date or today...?", + "type": "number" + }, + "defaultObject": [ + 7, + 14, + 30 ] }, "groups": { "title": "Group-by Variables", - "description": "an array of group-by variables by which to group queries on the y-axis", + "description": "an array of variables by which to count queries; each value of the variable is plotted on the y-axis", "type": "array", "items": { "type": "object", @@ -162,12 +230,15 @@ "description": "group-by variable label", "type": "string" } - } - } + } + }, + "data-mapping": true, + "data-type": "either", + "required": false }, "status_groups": { "title": "Stratification Variables", - "description": "an array of Stratification variables by which to stratify the stacked bars", + "description": "an array of variables with which to stratify each group-by value, each value of which plots as a component of a stacked bar when Bar Arrangement is set to _Stacked_ or as individual bars when Bar Arrangement is set to _grouped_", "type": "array", "items": { "type": "object", @@ -193,17 +264,14 @@ "type": "array" } } - } - }, - "site_col": { - "title": "Site Variable", - "description": "site variable name", - "type": "string", - "default": "sitename" + }, + "data-mapping": true, + "data-type": "either", + "required": false }, "filters": { "title": "Filter Variables", - "description": "an array of filter variables with which to filter the data", + "description": "an array of variables with which to filter the data", "type": "array", "items": { "type": "object", @@ -219,11 +287,20 @@ "type": "string" } } - } + }, + "data-mapping": true, + "data-type": "either", + "required": false + }, + "dropdown_size": { + "title": "Number of Options Display in Multi-select Dropdowns", + "description": "controls the maximum number of options that appear in the multi-select dropdowns before a scrollbar appears", + "type": "number", + "default": 6 }, "details": { "title": "Detail Listing Variables", - "description": "an array of detail variables which will print in the detail listing", + "description": "an array of variables which will print in the listing; if unspecified all variables in data will appear in listing", "type": "array", "items": { "type": "object", @@ -239,13 +316,20 @@ "type": "string" } } - } + }, + "data-mapping": true, + "data-type": "either", + "required": false }, - "dropdown_size": { - "title": "Number of Options Display in Multi-select Dropdowns", - "description": "controls the maximum number of options that appear in the multi-select dropdowns before a scrollbar appears", - "type": "number", - "default": 6 + "bar_arrangement": { + "title": "Arrangement of Bars", + "description": "controls arrangement of bars, either stacked or grouped side-by-side", + "type": "string", + "default": "stacked", + "enum": [ + "stacked", + "grouped" + ] }, "cutoff": { "title": "Number of Groups Displayed Initially", @@ -259,17 +343,17 @@ "type": "boolean", "default": true }, - "exportable": { - "title": "Allow Listing Export?", - "description": "allow the export of data to .csv via a button beneath the detail listing", + "truncate_listing_cells": { + "title": "Truncate Listing Cell Text?", + "description": "optionally truncate cell text past a certain number of characters", "type": "boolean", "default": true }, - "nRowsPerPage": { - "title": "Number of rows displayed per page.", - "description": "the number of rows displayed per page.", + "truncation_cutoff": { + "title": "Maximum Length of Cell Text", + "description": "cell text past this cutoff will be truncated and the full text will be captured in a tooltip", "type": "number", - "default": 10 + "default": 100 } } } diff --git a/src/chart/index.js b/src/chart/index.js new file mode 100644 index 0000000..d0a6a34 --- /dev/null +++ b/src/chart/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/chart/onDraw.js b/src/chart/onDraw.js index 2dd8554..51ed83c 100644 --- a/src/chart/onDraw.js +++ b/src/chart/onDraw.js @@ -1,10 +1,12 @@ import setLeftMargin from './onDraw/setLeftMargin'; +import setXDomain from './onDraw/setXDomain'; import setYDomain from './onDraw/setYDomain'; import setChartHeight from './onDraw/setChartHeight'; import updateXAxisLabel from './onDraw/updateXAxisLabel'; export default function onDraw() { setLeftMargin.call(this); + setXDomain.call(this); setYDomain.call(this); setChartHeight.call(this); updateXAxisLabel.call(this); diff --git a/src/chart/onDraw/setChartHeight.js b/src/chart/onDraw/setChartHeight.js index feb9245..594b895 100644 --- a/src/chart/onDraw/setChartHeight.js +++ b/src/chart/onDraw/setChartHeight.js @@ -1,6 +1,7 @@ export default function setChartHeight() { //Match chart height to number of bars currently displayed. - this.raw_height = - (+this.config.range_band + this.config.range_band * this.config.padding) * - this.y_dom.length; + this.raw_height = this.filtered_data.length + ? (+this.config.range_band + this.config.range_band * this.config.padding) * + this.y_dom.length + : 100; } diff --git a/src/chart/onDraw/setLeftMargin.js b/src/chart/onDraw/setLeftMargin.js index a376ee1..6d10d98 100644 --- a/src/chart/onDraw/setLeftMargin.js +++ b/src/chart/onDraw/setLeftMargin.js @@ -1,5 +1,7 @@ export default function setLeftMargin() { const fontSize = parseInt(this.wrap.style('font-size')); - this.config.margin.left = - Math.max(7, d3.max(this.y_dom, d => d.length)) * fontSize * 0.5 + fontSize * 1.5 * 1.5 + 6; + this.config.margin.left = Math.max( + Math.max(7, d3.max(this.y_dom, d => d.length)) * fontSize * 0.5 + fontSize * 1.5 * 1.5 + 6, + 100 + ); } diff --git a/src/chart/onDraw/setXDomain.js b/src/chart/onDraw/setXDomain.js new file mode 100644 index 0000000..de30f1c --- /dev/null +++ b/src/chart/onDraw/setXDomain.js @@ -0,0 +1,3 @@ +export default function setXDomain() { + if (this.filtered_data.length === 0) this.x_dom = [0, 0]; +} diff --git a/src/chart/onInit.js b/src/chart/onInit.js index 0a86af9..85af7a9 100644 --- a/src/chart/onInit.js +++ b/src/chart/onInit.js @@ -1,7 +1,7 @@ import defineListingSettings from './onInit/defineListingSettings'; import defineNewVariables from './onInit/defineNewVariables'; -import defineQueryStatuses from './onInit/defineQueryStatuses'; -import defineQueryAgeCategories from './onInit/defineQueryAgeCategories'; +import defineQueryStatusSet from './onInit/defineQueryStatusSet'; +import defineQueryRecencySet from './onInit/defineQueryRecencySet'; import removeInvalidControls from './onInit/removeInvalidControls'; export default function onInit() { @@ -9,10 +9,10 @@ export default function onInit() { defineNewVariables.call(this); //Define query statuses. - defineQueryStatuses.call(this); + defineQueryStatusSet.call(this); - //Define query age categories. - defineQueryAgeCategories.call(this); + //Define query recency categories. + defineQueryRecencySet.call(this); //Define detail listing settings. defineListingSettings.call(this); diff --git a/src/chart/onInit/defineNewVariables.js b/src/chart/onInit/defineNewVariables.js index b74f703..b0b0e6d 100644 --- a/src/chart/onInit/defineNewVariables.js +++ b/src/chart/onInit/defineNewVariables.js @@ -1,51 +1,42 @@ export default function defineNewVariables() { + const queryAgeCol = this.config.status_groups.find( + status_group => status_group.label === 'Query Age' + ).value_col; + const queryRecencyCol = this.config.filters.find(filter => filter.label === 'Query Recency') + .value_col; + this.raw_data.forEach(d => { + //Concatenate form and field to avoid duplicates across forms. d['Form: Field'] = d[this.config.form_col] + ': ' + d[this.config.field_col]; - //Define query age category. - if (!this.config.age_category_order) { - const queryAge = - /^ *\d+ *$/.test(d[this.config.age_col]) && - ['Closed', 'Cancelled'].indexOf(d[this.config.status_col]) < 0 - ? +d[this.config.age_col] - : NaN; - switch (true) { - case queryAge <= 14: - d['Query Age Category'] = '0-2 weeks'; - break; - case queryAge <= 28: - d['Query Age Category'] = '2-4 weeks'; - break; - case queryAge <= 56: - d['Query Age Category'] = '4-8 weeks'; - break; - case queryAge <= 112: - d['Query Age Category'] = '8-16 weeks'; - break; - case queryAge > 112: - d['Query Age Category'] = '>16 weeks'; - break; - default: - d['Query Age Category'] = d[this.config.status_col]; - break; - } + //Define query age. + if (this.config.age_statuses.indexOf(d[this.config.status_col]) < 0) + d[queryAgeCol] = d[this.config.status_col]; + else { + const age = +d[this.config.age_col]; + this.config.ageRanges.forEach((ageRange, i) => { + if (i === 0 && ageRange[0] <= age && age <= ageRange[1]) + d[queryAgeCol] = this.config.ageRangeCategories[i]; + else if (i === this.config.ageRanges.length - 1 && ageRange[0] < age) + d[queryAgeCol] = this.config.ageRangeCategories[i]; + else if (ageRange[0] < age && age <= ageRange[1]) + d[queryAgeCol] = this.config.ageRangeCategories[i]; + }); } - //Define query open time category. - const openTime = /^ *\d+ *$/.test(d[this.config.open_col]) ? +d[this.config.open_col] : NaN; - switch (true) { - case openTime <= 7: - d['Query Open Time Category'] = '0-7 days'; - break; - case openTime <= 14: - d['Query Open Time Category'] = '8-14 days'; - break; - case openTime <= 30: - d['Query Open Time Category'] = '15-30 days'; - break; - default: - d['Query Open Time Category'] = '>30 days'; - break; + //Define query recency. + if (d.hasOwnProperty(this.config.recency_category_col)) { + d[queryRecencyCol] = d[this.config.recency_category_col] || 'N/A'; + } else if (d.hasOwnProperty(this.config.recency_col)) { + const recency = +d[this.config.recency_col]; + this.config.recencyRanges.forEach((recencyRange, i) => { + if (i === 0 && recencyRange[0] <= recency && recency <= recencyRange[1]) + d[queryRecencyCol] = this.config.recencyRangeCategories[i]; + else if (i === this.config.recencyRanges.length - 1 && recencyRange[0] < recency) + d[queryRecencyCol] = this.config.recencyRangeCategories[i]; + else if (recencyRange[0] < recency && recency <= recencyRange[1]) + d[queryRecencyCol] = this.config.recencyRangeCategories[i]; + }); } }); } diff --git a/src/chart/onInit/defineQueryAgeCategories.js b/src/chart/onInit/defineQueryAgeCategories.js deleted file mode 100644 index 49a904f..0000000 --- a/src/chart/onInit/defineQueryAgeCategories.js +++ /dev/null @@ -1,31 +0,0 @@ -export default function defineQueryAgeCategories() { - const queryAgeCategoryInput = this.controls.config.inputs.find( - input => input.value_col === this.config.age_category_col - ); - const queryAgeCategoryGroup = this.config.status_groups.find( - age_category_group => age_category_group.value_col === this.config.age_category_col - ); - const queryStatusOrder = this.config.status_groups.find( - status_group => status_group.value_col === this.config.status_col - ).order; - const queryAgeCategoryOrder = Array.isArray(queryAgeCategoryGroup.order) - ? queryAgeCategoryGroup.order.concat( - d3 - .set(this.raw_data.map(d => d[this.config.age_category_col])) - .values() - .filter(value => queryAgeCategoryGroup.order.indexOf(value) < 0) - .sort() - ) - : d3 - .set(this.raw_data.map(d => d[this.config.age_category_col])) - .values() - .sort((a, b) => { - const aIndex = queryStatusOrder.indexOf(a); - const bIndex = queryStatusOrder.indexOf(b); - const diff = aIndex - bIndex; - - return diff ? diff : a < b ? -1 : 1; - }); - queryAgeCategoryInput.order = queryAgeCategoryOrder; - queryAgeCategoryGroup.order = queryAgeCategoryOrder; -} diff --git a/src/chart/onInit/defineQueryRecencySet.js b/src/chart/onInit/defineQueryRecencySet.js new file mode 100644 index 0000000..538f9b2 --- /dev/null +++ b/src/chart/onInit/defineQueryRecencySet.js @@ -0,0 +1,23 @@ +export default function defineQueryRecencySet() { + const queryRecencyInput = this.controls.config.inputs.find( + input => input.value_col === 'queryrecency' + ); + + if (this.raw_data[0].hasOwnProperty(this.config.recency_category_col)) { + queryRecencyInput.values = d3 + .set(this.raw_data.map(d => d.queryrecency)) + .values() + .sort((a, b) => { + const anum = parseFloat(a); + const bnum = parseFloat(b); + const diff = anum - bnum; + return diff ? diff : a < b ? -1 : a > b ? 1 : 0; + }); + } else if (this.raw_data[0].hasOwnProperty(this.config.recency_col)) + queryRecencyInput.values = this.config.recencyRangeCategories; + else + this.controls.config.inputs.splice( + this.controls.config.inputs.findIndex(input => input.value_col === 'queryrecency'), + 1 + ); +} diff --git a/src/chart/onInit/defineQueryStatusSet.js b/src/chart/onInit/defineQueryStatusSet.js new file mode 100644 index 0000000..29bfdcd --- /dev/null +++ b/src/chart/onInit/defineQueryStatusSet.js @@ -0,0 +1,23 @@ +export default function defineQueryStatusSet() { + const queryStatusInput = this.controls.config.inputs.find( + input => input.value_col === this.config.status_col + ); + const queryStatusGroup = this.config.status_groups.find( + status_group => status_group.value_col === this.config.status_col + ); + const queryStatusOrder = + Array.isArray(queryStatusGroup.order) && queryStatusGroup.order.length + ? queryStatusGroup.order.concat( + d3 + .set(this.raw_data.map(d => d[this.config.status_col])) + .values() + .filter(value => queryStatusGroup.order.indexOf(value) < 0) + .sort() + ) + : d3 + .set(this.raw_data.map(d => d[this.config.status_col])) + .values() + .sort(); + queryStatusInput.order = queryStatusOrder; + queryStatusGroup.order = queryStatusOrder; +} diff --git a/src/chart/onInit/defineQueryStatuses.js b/src/chart/onInit/defineQueryStatuses.js deleted file mode 100644 index 9755b20..0000000 --- a/src/chart/onInit/defineQueryStatuses.js +++ /dev/null @@ -1,22 +0,0 @@ -export default function defineQueryStatuses() { - const queryStatusInput = this.controls.config.inputs.find( - input => input.value_col === this.config.status_col - ); - const queryStatusGroup = this.config.status_groups.find( - status_group => status_group.value_col === this.config.status_col - ); - const queryStatusOrder = Array.isArray(queryStatusGroup.order) - ? queryStatusGroup.order.concat( - d3 - .set(this.raw_data.map(d => d[this.config.status_col])) - .values() - .filter(value => queryStatusGroup.order.indexOf(value) < 0) - .sort() - ) - : d3 - .set(this.raw_data.map(d => d[this.config.status_col])) - .values() - .sort(); - queryStatusInput.order = queryStatusOrder; - queryStatusGroup.order = queryStatusOrder; -} diff --git a/src/chart/onLayout.js b/src/chart/onLayout.js index a6b8bef..c331710 100644 --- a/src/chart/onLayout.js +++ b/src/chart/onLayout.js @@ -1,17 +1,53 @@ +import classControls from './onLayout/classControls'; +import groupControls from './onLayout/groupControls'; +import addControlTooltips from './onLayout/addControlTooltips'; import updateGroupByOptions from './onLayout/updateGroupByOptions'; +import addGroupByHighlight from './onLayout/addGroupByHighlight'; +import checkInitialStatusGroup from './onLayout/checkInitialStatusGroup'; +import addStatusGroupHighlight from './onLayout/addStatusGroupHighlight'; import customizeMultiSelects from './onLayout/customizeMultiSelects'; +import addSelectAll from './onLayout/addSelectAll'; +import updateFilterEventListeners from './onLayout/updateFilterEventListeners'; +import sortQueryRecencyOptions from './onLayout/sortQueryRecencyOptions'; import setYAxisDomainLength from './onLayout/setYAxisDomainLength'; import addResetButton from './onLayout/addResetButton'; import clearListingOnChange from './onLayout/clearListingOnChange'; -import addListingInstruction from './onLayout/addListingInstruction'; +import addFootnotes from './onLayout/addFootnotes'; export default function onLayout() { + //Class controls for unique selection. + classControls.call(this); + + //Group controls logically. + groupControls.call(this); + + //Add tooltips to controls. + addControlTooltips.call(this); + //Display group label rather than group column name in Group by control. updateGroupByOptions.call(this); + //Highlight y-axis label when user hovers over Status Group control. + addGroupByHighlight.call(this); + + //Check radio button of initial status group. + checkInitialStatusGroup.call(this); + + //Highlight legend when user hovers over Status Group control. + addStatusGroupHighlight.call(this); + //Customize dropdowns with multiple options. customizeMultiSelects.call(this); + //Add select all checkbox to filters. + addSelectAll.call(this); + + //Update filter event listeners to toggle select all checkbox on change. + updateFilterEventListeners.call(this); + + //Sort query recency categories numerically if possible. + sortQueryRecencyOptions.call(this); + //Handle y-domain length control setYAxisDomainLength.call(this); @@ -21,6 +57,6 @@ export default function onLayout() { //Clear listing when controls change. clearListingOnChange.call(this); - //Add listing instruction. - addListingInstruction.call(this); + //Add chart footnotes. + addFootnotes.call(this); } diff --git a/src/chart/onLayout/addControlTooltips.js b/src/chart/onLayout/addControlTooltips.js new file mode 100644 index 0000000..94e0d9d --- /dev/null +++ b/src/chart/onLayout/addControlTooltips.js @@ -0,0 +1,42 @@ +export default function addControlTooltips() { + const tooltips = { + //other controls + 'Group by': + 'Controls the variable with which the queries are grouped; each group is plotted along the vertical axis of the chart.', + 'Status Group': 'Controls the variable with which the bars are subdivided.', + 'Bar Arrangement': + 'Controls the layout of the status groups.\n- stacked: status groups are plotted side-by-side horizontally\n- grouped: status groups are plotted side-by-side vertically', + 'Show First N Groups': 'Controls the number of groups displayed on the vertical axis.', + 'Order Groups Alphabetically?': + 'Controls the order of the groups; uncheck to sort groups by magnitude (largest to smallest number of queries) instead of alphabetically.', + + //filters + 'Query Age': + 'Open queries are broken down into how long they have been open. All other queries are classified by status (answered, closed, cancelled).', + 'Query Status': + 'Open: site has not responded to the issue\nAnswered: site has responded to issue; DM needs to review\nClosed: issue resolved\nCancelled: query cancelled by DM', + 'Query Recency': + 'Number of days a query has been open, regardless of its current status (applies only to queries opened in the past 30 days)', + Form: + 'CRF page abbreviation; hover over the abbreviation in the chart to see its full name.', + Site: 'Name or ID of site', + 'Marking Group': 'Entity that opened the query', + 'Visit/Folder': + 'Visit/folder abbreviation; hover over the visit/folder abbreviation in the chart to see the full name.' + }; + this.controls.controlGroups.each(function(d) { + const tooltip = + tooltips[d.label] || + `This ${d.type} controls ${d.value_col || d.option || d.options.join(', ')}.`; + if (tooltips[d.label] === undefined) + console.warn( + `The control labeled ${ + d.label + } does not have a curated tooltip. Defaulting to ${tooltip}.` + ); + d3 + .select(this) + .selectAll('.wc-control-label') + .attr('title', tooltip); + }); +} diff --git a/src/chart/onLayout/addFootnotes.js b/src/chart/onLayout/addFootnotes.js new file mode 100644 index 0000000..5d2f2f0 --- /dev/null +++ b/src/chart/onLayout/addFootnotes.js @@ -0,0 +1,12 @@ +export default function addFootnotes() { + this.footnotes = { + barClick: this.wrap + .append('div') + .classed('qo-footnote qo-footnote--bar-click', true) + .text('Click one or more bars to view the underlying data in the listing below.'), + deselectBars: this.wrap + .append('div') + .classed('qo-footnote qo-footnote--deselect-bars', true) + .text('Click in the white area to deselect all bars.') + }; +} diff --git a/src/chart/onLayout/addGroupByHighlight.js b/src/chart/onLayout/addGroupByHighlight.js new file mode 100644 index 0000000..f278063 --- /dev/null +++ b/src/chart/onLayout/addGroupByHighlight.js @@ -0,0 +1,18 @@ +export default function addGroupByHighlight() { + this.controls.otherControls.controlGroups + .filter(d => d.label === 'Group by') + .on('mouseover', () => { + this.svg.selectAll('.y.axis .axis-title').style({ + 'font-weight': 'bold', + 'text-decoration': 'underline', + fill: 'red' + }); + }) + .on('mouseout', () => { + this.svg.selectAll('.y.axis .axis-title').style({ + 'font-weight': 'normal', + 'text-decoration': 'none', + fill: 'black' + }); + }); +} diff --git a/src/chart/onLayout/addListingInstruction.js b/src/chart/onLayout/addListingInstruction.js deleted file mode 100644 index 2df201d..0000000 --- a/src/chart/onLayout/addListingInstruction.js +++ /dev/null @@ -1,6 +0,0 @@ -export default function addListingInstruction() { - this.wrap - .append('em') - .attr('id', 'listing-instruction') - .text('Click a bar to view its underlying data.'); -} diff --git a/src/chart/onLayout/addResetButton.js b/src/chart/onLayout/addResetButton.js index 9851e7d..528274a 100644 --- a/src/chart/onLayout/addResetButton.js +++ b/src/chart/onLayout/addResetButton.js @@ -1,21 +1,21 @@ import clone from '../../util/clone'; export default function addResetButton() { - this.controls.wrap + this.resetButton = d3 + .select(this.div) .insert('button', ':first-child') - .attr('id', 'reset-chart') - .style({ - margin: '5px', - padding: '5px', - float: 'right' - }) + .classed('qo-button qo-button--reset-chart', true) .text('Reset chart') .on('click', () => { - const element = clone(this.div), - settings = clone(this.initialSettings), - data = clone(this.raw_data); + const element = this.element; + const settings = clone(this.initialSettings); + const data = clone(this.raw_data); this.listing.destroy(); this.destroy(); + d3 + .select(this.element) + .selectAll('*') + .remove(); queryOverview(element, settings).init(data); }); } diff --git a/src/chart/onLayout/addSelectAll.js b/src/chart/onLayout/addSelectAll.js new file mode 100644 index 0000000..c105ff1 --- /dev/null +++ b/src/chart/onLayout/addSelectAll.js @@ -0,0 +1,33 @@ +export default function addSelectAll() { + const context = this; + + this.controls.filters.labels.filter(d => d.multiple).each(function(d) { + const label = d3 + .select(this) + .html(`${d.label} `); + const checkbox = label + .select('input') + .datum(d) + .attr('title', `Deselect all ${d.label} options`) + .property('checked', true) + .on('click', function(di) { + const checkbox = d3.select(this); + const checked = this.checked; + + //Update checkbox tooltip. + checkbox.attr( + 'title', + checked ? `Deselect all ${di.label} options` : `Select all ${di.label} options` + ); + + //Update filter object. + const filter = context.filters.find(filter => filter.col === di.value_col); + if (checked) filter.val = filter.choices; + else filter.val = []; + + //Redraw. + context.draw(); + }); + }); + this.controls.filters.checkboxes = this.controls.filters.labels.selectAll('.qo-select-all'); +} diff --git a/src/chart/onLayout/addStatusGroupHighlight.js b/src/chart/onLayout/addStatusGroupHighlight.js new file mode 100644 index 0000000..af2ee40 --- /dev/null +++ b/src/chart/onLayout/addStatusGroupHighlight.js @@ -0,0 +1,16 @@ +export default function addStatusGroupHighlight() { + this.controls.otherControls.controlGroups + .filter(d => d.label === 'Status Group') + .on('mouseover', () => { + this.legend.select('.legend-title').style({ + 'text-decoration': 'underline', + color: 'red' + }); + }) + .on('mouseout', () => { + this.legend.select('.legend-title').style({ + 'text-decoration': 'none', + color: 'black' + }); + }); +} diff --git a/src/chart/onLayout/checkInitialStatusGroup.js b/src/chart/onLayout/checkInitialStatusGroup.js new file mode 100644 index 0000000..cd51d7a --- /dev/null +++ b/src/chart/onLayout/checkInitialStatusGroup.js @@ -0,0 +1,7 @@ +export default function checkInitialStatusGroup() { + this.controls.otherControls.controlGroups + .filter(d => d.label === 'Status Group') + .selectAll('.radio') + .selectAll('.changer') + .property('checked', d => d === this.config.legend.label); +} diff --git a/src/chart/onLayout/classControls.js b/src/chart/onLayout/classControls.js new file mode 100644 index 0000000..7200541 --- /dev/null +++ b/src/chart/onLayout/classControls.js @@ -0,0 +1,11 @@ +export default function classControls() { + this.controls.controlGroups = this.controls.wrap + .selectAll('.control-group') + .attr( + 'class', + d => + `control-group qo-${d.type} qo-${d.type}--${d.label + .toLowerCase() + .replace(/[^_a-zA-Z-]/g, '-')}` + ); +} diff --git a/src/chart/onLayout/clearListingOnChange.js b/src/chart/onLayout/clearListingOnChange.js index fc9d879..9ab2e11 100644 --- a/src/chart/onLayout/clearListingOnChange.js +++ b/src/chart/onLayout/clearListingOnChange.js @@ -16,7 +16,6 @@ export default function clearListingOnChange() { //Reset listing. context.listing.wrap.selectAll('*').remove(); - context.wrap.select('#listing-instruction').style('display', 'block'); context.listing.init(context.filtered_data); }); } diff --git a/src/chart/onLayout/groupControls.js b/src/chart/onLayout/groupControls.js new file mode 100644 index 0000000..de59ad7 --- /dev/null +++ b/src/chart/onLayout/groupControls.js @@ -0,0 +1,50 @@ +export default function groupControls() { + const context = this; + + //Group filters. + this.controls.filters = { + container: this.controls.wrap + .insert('div', '.qo-subsetter') + .classed('qo-control-grouping qo-control-grouping--filters', true) + }; + this.controls.filters.container + .append('div') + .classed('qo-control-grouping--label', true) + .attr( + 'title', + 'Filters subset the data underlying the chart and listing.\nHover over filter labels to view more information about them.' + ) + .text('Filters'); + this.controls.filters.controlGroups = this.controls.wrap.selectAll('.qo-subsetter'); + this.controls.filters.labels = this.controls.filters.controlGroups.selectAll( + '.wc-control-label' + ); + this.controls.filters.selects = this.controls.filters.controlGroups.selectAll('.changer'); + this.controls.filters.controlGroups.each(function(d) { + context.controls.filters.container.node().appendChild(this); + }); + + //Group other controls. + this.controls.otherControls = { + container: this.controls.wrap + .insert('div', ':first-child') + .classed('qo-control-grouping qo-control-grouping--other-controls', true) + }; + this.controls.otherControls.label = this.controls.otherControls.container + .append('div') + .classed('qo-control-grouping--label', true) + .attr( + 'title', + 'Controls alter the display of the chart.\nHover over control labels to view more information about them.' + ) + .text('Controls'); + this.controls.otherControls.controlGroups = this.controls.wrap.selectAll( + '.control-group:not(.qo-subsetter)' + ); + this.controls.otherControls.labels = this.controls.otherControls.controlGroups.selectAll( + '.wc-control-label' + ); + this.controls.otherControls.controlGroups.each(function(d) { + context.controls.otherControls.container.node().appendChild(this); + }); +} diff --git a/src/chart/onLayout/sortQueryRecencyOptions.js b/src/chart/onLayout/sortQueryRecencyOptions.js new file mode 100644 index 0000000..27d12b2 --- /dev/null +++ b/src/chart/onLayout/sortQueryRecencyOptions.js @@ -0,0 +1,11 @@ +export default function sortQueryRecencyOptions() { + this.controls.filters.selects + .filter(d => d.value_col === 'queryrecency') + .selectAll('option') + .sort((a, b) => { + const anum = parseFloat(a); + const bnum = parseFloat(b); + const diff = anum - bnum; + return diff ? diff : a < b ? -1 : a > b ? 1 : 0; + }); +} diff --git a/src/chart/onLayout/tweakMultiSelects.js b/src/chart/onLayout/tweakMultiSelects.js deleted file mode 100644 index 94c67fc..0000000 --- a/src/chart/onLayout/tweakMultiSelects.js +++ /dev/null @@ -1,19 +0,0 @@ -export default function tweakMultiSelects() { - const context = this; - - this.controls.wrap - .selectAll('.control-group') - .filter(d => d.type === 'subsetter' && d.multiple) - .each(function(d) { - d3 - .select(this) - .select('select') - .attr( - 'size', - context.filters.find(filter => filter.col === d.value_col).choices.length - ) - .attr('title', 'Hold the CTRL key to select or deselect a single option.') - .selectAll('option') - .property('selected', true); - }); -} diff --git a/src/chart/onLayout/updateFilterEventListeners.js b/src/chart/onLayout/updateFilterEventListeners.js new file mode 100644 index 0000000..83397db --- /dev/null +++ b/src/chart/onLayout/updateFilterEventListeners.js @@ -0,0 +1,12 @@ +import updateSelectAll from './updateFilterEventListeners/updateSelectAll'; + +export default function updateFilterEventListeners() { + const context = this; + + this.controls.filters.selects.on('change', function(d) { + const select = d3.select(this); + const selectedOptions = select.selectAll('option:checked').data(); + updateSelectAll.call(context, d, selectedOptions); + context.draw(); + }); +} diff --git a/src/chart/onLayout/updateFilterEventListeners/updateSelectAll.js b/src/chart/onLayout/updateFilterEventListeners/updateSelectAll.js new file mode 100644 index 0000000..878a205 --- /dev/null +++ b/src/chart/onLayout/updateFilterEventListeners/updateSelectAll.js @@ -0,0 +1,17 @@ +export default function updateSelectAll(d, selectedOptions) { + //Update filter object. + const filter = this.filters.find(filter => filter.col === d.value_col); + filter.val = d.multiple ? selectedOptions : selectedOptions.pop(); + + //Update checkbox. + if (d.multiple) { + const checked = filter.val.length === filter.choices.length; + const checkbox = this.controls.filters.checkboxes + .filter(di => di.value_col === d.value_col) + .attr( + 'title', + checked ? `Deselect all ${d.label} options` : `Select all ${d.label} options` + ) + .property('checked', checked); + } +} diff --git a/src/chart/onLayout/updateGroupByOptions.js b/src/chart/onLayout/updateGroupByOptions.js index 61382d8..8fd9c5c 100644 --- a/src/chart/onLayout/updateGroupByOptions.js +++ b/src/chart/onLayout/updateGroupByOptions.js @@ -1,7 +1,7 @@ export default function updateGroupByOptions() { const context = this; - const groupByControl = this.controls.wrap + this.controls.wrap .selectAll('.control-group select') .filter(d => d.label === 'Group by') .on('change', function() { diff --git a/src/chart/onPreprocess/updateRangeBand.js b/src/chart/onPreprocess/updateRangeBand.js index bb03ae0..0624fba 100644 --- a/src/chart/onPreprocess/updateRangeBand.js +++ b/src/chart/onPreprocess/updateRangeBand.js @@ -1,7 +1,7 @@ export default function updateRangeBand() { if (this.config.marks[0].arrange === 'stacked') { - this.config.range_band = 15; + this.config.range_band = this.initialSettings.range_band; } else { - this.config.range_band = 15 * this.config.color_dom.length; + this.config.range_band = this.initialSettings.range_band * this.config.color_dom.length; } } diff --git a/src/chart/onPreprocess/updateStratification.js b/src/chart/onPreprocess/updateStratification.js index f7b818f..05c34b1 100644 --- a/src/chart/onPreprocess/updateStratification.js +++ b/src/chart/onPreprocess/updateStratification.js @@ -1,16 +1,21 @@ -import { set } from 'd3'; - export default function updateStratification() { - const stratification = this.config.status_groups.find( - status_group => status_group.value_col === this.config.color_by + const statusGroup = this.controls.wrap + .selectAll('.qo-radio--status-group') + .selectAll('.radio') + .filter(function() { + const label = d3.select(this); + const radio = label.select('.changer'); + return radio.property('checked'); + }) + .text(); + this.config.status_group = this.config.status_groups.find( + status_group => status_group.label === statusGroup ); - this.config.color_dom = - stratification.order || - set(this.raw_data.map(d => d[this.config.color_by])) - .values() - .sort(); - this.config.colors = stratification.colors; - this.config.legend.label = stratification.label; + this.config.marks[0].split = this.config.status_group.value_col; + this.config.color_by = this.config.status_group.value_col; + this.config.color_dom = this.config.status_group.order; + this.config.colors = this.config.status_group.colors; + this.config.legend.label = this.config.status_group.label; this.config.legend.order = this.config.color_dom.slice(); this.config.marks[0].tooltip = `[${this.config.color_by}] - $x queries`; } diff --git a/src/chart/onResize.js b/src/chart/onResize.js index 6820139..1a2f752 100644 --- a/src/chart/onResize.js +++ b/src/chart/onResize.js @@ -5,6 +5,8 @@ import annotateYAxisInfo from './onResize/annotateYAxisInfo'; import hideBars from './onResize/hideBars'; import annotateNumberOfQueries from './onResize/annotateNumberOfQueries'; import addBarClick from './onResize/addBarClick'; +import addBarDeselection from './onResize/addBarDeselection'; +import addNoDataIndicator from './onResize/addNoDataIndicator'; export default function onResize() { //Add filter functionality to legend. @@ -27,4 +29,10 @@ export default function onResize() { //Add bar click-ability. addBarClick.call(this); + + //Add bar deselection. + addBarDeselection.call(this); + + //Add informational text to the chart canvas when filters are in conflict. + addNoDataIndicator.call(this); } diff --git a/src/chart/onResize/addBarClick.js b/src/chart/onResize/addBarClick.js index 196eb7c..66ebd1b 100644 --- a/src/chart/onResize/addBarClick.js +++ b/src/chart/onResize/addBarClick.js @@ -1,70 +1,52 @@ -import flatMap from '../../util/flatMap'; +import mouseoverStyle from './addBarClick/mouseoverStyle'; +import mouseoverAttrib from './addBarClick/mouseoverAttrib'; +import mouseoutStyle from './addBarClick/mouseoutStyle'; +import mouseoutAttrib from './addBarClick/mouseoutAttrib'; +import initListing from './addBarClick/initListing'; export default function addBarClick() { const context = this; - const barGroups = this.svg.selectAll('.bar-group'); - const bars = this.svg.selectAll('.bar'); // will subtract and add to bar to offset increase in stroke-width and prevent bars // from overlapping as much when neighbors are both selected. - const mouseoverAttrib = { - width: function(d) { - return this.getBBox().width - 2.5; - }, - x: function(d) { - return this.getBBox().x + 2.5; - } - }; - const mouseoverStyle = { - 'stroke-width': '5px', - fill: 'black' - }; - const mouseoutAttrib = { - width: function(d) { - return this.getBBox().width + 2.5; - }, - x: function(d) { - return this.getBBox().x - 2.5; - } - }; - const mouseoutStyle = { - 'stroke-width': '1px', - fill: d => context.colorScale(d.key) - }; - bars + this.bars = this.svg + .selectAll('.bar') .style('cursor', 'pointer') .on('mouseover', function() { - if (!d3.select(this).classed('selected')) d3.select(this).style(mouseoverStyle); - if (!d3.select(this).classed('selected')) d3.select(this).attr(mouseoverAttrib); + const bar = d3.select(this); + const selected = bar.classed('selected'); + + //Apply highlight attributes and styles to bar. + mouseoverStyle.call(context, bar, selected); + mouseoverAttrib.call(context, bar, selected); + //moveToFront causes an issue preventing onMouseout from firing in Internet Explorer so only call it in other browsers. if (!/trident/i.test(navigator.userAgent)) d3.select(this).moveToFront(); }) .on('mouseout', function() { - if (!d3.select(this).classed('selected')) d3.select(this).style(mouseoutStyle); - if (!d3.select(this).classed('selected')) d3.select(this).attr(mouseoutAttrib); - bars - .filter(function() { - return d3.select(this).classed('selected'); - }) - .moveToFront(); + const bar = d3.select(this); + const selected = bar.classed('selected'); + + //Apply default attributes and styles to bar. + mouseoutStyle.call(context, bar, selected); + mouseoutAttrib.call(context, bar, selected); + + //moveToFront causes an issue preventing onMouseout from firing in Internet Explorer so only call it in other browsers. + if (!/trident/i.test(navigator.userAgent)) + context.bars + .filter(function() { + return d3.select(this).classed('selected'); + }) + .moveToFront(); }) .on('click', function(d) { - // this doesn't need a style because mouseout isn't applied when the bar is selected - d3.select(this).classed('selected', d3.select(this).classed('selected') ? false : true); - context.listing.wrap.selectAll('*').remove(); - // feed listing data for all selected bars - context.listing.init( - d3 - .selectAll('rect.selected') - .data() - .flatMap(d => d.values.raw) - ); - context.wrap.select('#listing-instruction').style('display', 'none'); // remove bar instructions - // display filtered data if no bars are selected - if (d3.selectAll('rect.selected')[0].length === 0) { - context.listing.wrap.selectAll('*').remove(); - context.wrap.select('#listing-instruction').style('display', 'block'); - context.listing.init(context.filtered_data); - } + const bar = d3.select(this); + const selected = bar.classed('selected'); + + //Update selected class of clicked bar. + bar.classed('selected', !selected); + + //Re-initialize listing. + initListing.call(context); }); } diff --git a/src/chart/onResize/addBarClick/initListing.js b/src/chart/onResize/addBarClick/initListing.js new file mode 100644 index 0000000..1609e2f --- /dev/null +++ b/src/chart/onResize/addBarClick/initListing.js @@ -0,0 +1,14 @@ +export default function initListing() { + //Clear listing container. + this.listing.wrap.selectAll('*').remove(); + + //Capture data from selected bars. + const selectedData = d3 + .selectAll('rect.selected') + .data() + .flatMap(d => d.values.raw); + + //Feed data from selected bars into listing. + if (selectedData.length > 0) this.listing.init(selectedData); + else this.listing.init(this.filtered_data); +} diff --git a/src/chart/onResize/addBarClick/mouseoutAttrib.js b/src/chart/onResize/addBarClick/mouseoutAttrib.js new file mode 100644 index 0000000..8d5ec89 --- /dev/null +++ b/src/chart/onResize/addBarClick/mouseoutAttrib.js @@ -0,0 +1,11 @@ +export default function mouseoutAttrib(bar, selected, clear = false) { + if (!(selected || clear) || (selected && clear)) + bar.attr({ + width: function(d) { + return this.getBBox().width + 2.5; + }, + x: function(d) { + return this.getBBox().x - 2.5; + } + }); +} diff --git a/src/chart/onResize/addBarClick/mouseoutStyle.js b/src/chart/onResize/addBarClick/mouseoutStyle.js new file mode 100644 index 0000000..909d9e8 --- /dev/null +++ b/src/chart/onResize/addBarClick/mouseoutStyle.js @@ -0,0 +1,7 @@ +export default function mouseoutStyle(bar, selected, clear = false) { + if (!(selected || clear) || (selected && clear)) + bar.style({ + 'stroke-width': '1px', + fill: d => this.colorScale(d.key) + }); +} diff --git a/src/chart/onResize/addBarClick/mouseoverAttrib.js b/src/chart/onResize/addBarClick/mouseoverAttrib.js new file mode 100644 index 0000000..cbc86ef --- /dev/null +++ b/src/chart/onResize/addBarClick/mouseoverAttrib.js @@ -0,0 +1,11 @@ +export default function mouseoverAttrib(bar, selected) { + if (!selected) + bar.attr({ + width: function(d) { + return this.getBBox().width - 2.5; + }, + x: function(d) { + return this.getBBox().x + 2.5; + } + }); +} diff --git a/src/chart/onResize/addBarClick/mouseoverStyle.js b/src/chart/onResize/addBarClick/mouseoverStyle.js new file mode 100644 index 0000000..58ccdda --- /dev/null +++ b/src/chart/onResize/addBarClick/mouseoverStyle.js @@ -0,0 +1,7 @@ +export default function mouseoverStyle(bar, selected) { + if (!selected) + bar.style({ + 'stroke-width': '5px', + fill: 'black' + }); +} diff --git a/src/chart/onResize/addBarDeselection.js b/src/chart/onResize/addBarDeselection.js new file mode 100644 index 0000000..9d5a19d --- /dev/null +++ b/src/chart/onResize/addBarDeselection.js @@ -0,0 +1,18 @@ +import mouseoutStyle from './addBarClick/mouseoutStyle'; +import mouseoutAttrib from './addBarClick/mouseoutAttrib'; +import initListing from './addBarClick/initListing'; + +export default function addBarDeselection() { + const context = this; + + this.overlay.on('click', () => { + this.bars.each(function(d) { + const bar = d3.select(this); + const selected = bar.classed('selected'); + mouseoutStyle.call(context, bar, selected, true); + mouseoutAttrib.call(context, bar, selected, true); + bar.classed('selected', false); + }); + initListing.call(this); + }); +} diff --git a/src/chart/onResize/addNoDataIndicator.js b/src/chart/onResize/addNoDataIndicator.js new file mode 100644 index 0000000..141df5e --- /dev/null +++ b/src/chart/onResize/addNoDataIndicator.js @@ -0,0 +1,14 @@ +export default function addNoDataIndicator() { + this.svg.select('.qo-no-data').remove(); + + if (this.filtered_data.length === 0) + this.svg + .append('text') + .classed('qo-no-data', true) + .attr({ + x: this.plot_width / 2, + y: this.plot_height / 2, + 'text-anchor': 'middle' + }) + .text('No queries selected. Verify that no filter selections are in conflict.'); +} diff --git a/src/chart/onResize/addYAxisTickClick.js b/src/chart/onResize/addYAxisTickClick.js index 61a2fcb..de40755 100644 --- a/src/chart/onResize/addYAxisTickClick.js +++ b/src/chart/onResize/addYAxisTickClick.js @@ -1,3 +1,5 @@ +import updateSelectAll from '../onLayout/updateFilterEventListeners/updateSelectAll'; + export default function addYAxisTickClick() { if (this.config.y.column === this.config.form_col) { const yLabels = this.svg @@ -5,27 +7,84 @@ export default function addYAxisTickClick() { .style('fill', 'blue') .style('text-decoration', 'underline'); yLabels.style('cursor', 'pointer').on('click', yLabel => { - this.controls.wrap - .selectAll('.control-group') - .filter(d => d.label === 'Group by') - .selectAll('option') - .property('selected', d => { - return d === 'Form: Field'; + //Update Group by control. + const groupByControl = this.controls.otherControls.controlGroups.filter( + d => d.label === 'Group by' + ); + groupByControl + .select('.wc-control-label') + .style({ + 'font-weight': 'bold', + 'text-decoration': 'underline', + color: 'red' + }) + .transition() + .delay(3000) + .style({ + 'font-weight': 'normal', + 'text-decoration': 'none', + color: 'black' }); + groupByControl.selectAll('option').property('selected', d => { + return d === 'Form: Field'; + }); + + //Update chart settings. this.config.y.column = 'Form: Field'; this.config.y.label = 'Form: Field'; this.config.marks[0].per[0] = 'Form: Field'; - this.controls.wrap - .selectAll('.control-group') - .filter(d => d.label === 'Form') - .selectAll('option') - .property('selected', d => d === yLabel); - this.filters.filter(filter => filter.col === this.config.form_col)[0].val = yLabel; - this.draw(this.filtered_data.filter(d => d[this.config.form_col] === yLabel)); + //Update Form filter. + const formFilter = this.controls.filters.controlGroups.filter(d => d.label === 'Form'); + formFilter + .select('.wc-control-label') + .style({ + 'font-weight': 'bold', + 'text-decoration': 'underline', + color: 'red' + }) + .transition() + .delay(3000) + .style({ + 'font-weight': 'normal', + 'text-decoration': 'none', + color: 'black' + }); + formFilter.selectAll('option').property('selected', d => d === yLabel); + + //Update Form filter object in `chart.filters`. + const filter = this.filters.find(filter => filter.col === this.config.form_col); + filter.val = yLabel; + updateSelectAll.call( + this, + this.controls.filters.controlGroups + .filter(d => d.value_col === this.config.form_col) + .datum(), + [yLabel] + ); + + //Redraw chart and listing. + + //Update Group by control. + this.draw(); this.listing.wrap.selectAll('*').remove(); - this.wrap.select('listing-instruction').style('display', 'block'); this.listing.init(this.filtered_data); + + //Highlight y-axis label. + this.svg + .select('.y.axis .axis-title') + .style({ + 'font-weight': 'bold', + 'text-decoration': 'underline', + fill: 'red' + }) + .transition() + .delay(3000) + .style({ + 'font-weight': 'normal', + 'text-decoration': 'none', + fill: 'black' + }); }); } } diff --git a/src/chart/onResize/legendFilter.js b/src/chart/onResize/legendFilter.js index 88c8bb3..c59bfc8 100644 --- a/src/chart/onResize/legendFilter.js +++ b/src/chart/onResize/legendFilter.js @@ -1,16 +1,20 @@ +import updateSelectAll from '../onLayout/updateFilterEventListeners/updateSelectAll'; + +//TODO: modularize/refactor export default function legendFilter() { const context = this; + //Alter layout of legend. + const legend = this.legend; + legend.style('margin-left', `${this.margin.left}px`); + const legendTitle = legend.select('.legend-title'); + legendTitle.attr('title', 'Add and remove queries by clicking the legend items to the left.'); + legend.node().appendChild(legendTitle.node()); + //Filter data by clicking on legend. const statusFilter = this.filters.find(filter => filter.col === this.config.color_by); const legendItems = this.wrap .selectAll('.legend-item') - .style({ - cursor: 'pointer', - 'border-radius': '4px', - padding: '5px', - 'padding-left': '8px' - }) .classed( 'selected', d => statusFilter.val === 'All' || statusFilter.val.indexOf(d.label) > -1 @@ -22,14 +26,15 @@ export default function legendFilter() { ? 'lightgray' : 'white' ); - const statusOptions = this.controls.wrap + const statusControlGroup = this.controls.wrap .selectAll('.control-group') - .filter(d => d.value_col === context.config.marks[0].split) - .selectAll('.changer option'); // status filter options + .filter(d => d.value_col === context.config.marks[0].split); + const statusOptions = statusControlGroup.selectAll('.changer option'); // status filter options legendItems.selectAll('.legend-mark-text').remove(); // don't need 'em + legendItems.selectAll('.legend-color-block').attr('width', '8px'); legendItems.on('click', function(d) { - const legendItem = d3.select(this), // clicked legend item - selected = !legendItem.classed('selected'); // selected boolean + const legendItem = d3.select(this); // clicked legend item + const selected = !legendItem.classed('selected'); // selected boolean legendItem.classed('selected', selected); // toggle selected class const selectedLegendItems = legendItems .filter(function() { @@ -44,6 +49,7 @@ export default function legendFilter() { .property('selected', false) .filter(d => selectedLegendItems.indexOf(d) > -1) .property('selected', true); // set selected property of status options corresponding to selected statuses to true + updateSelectAll.call(context, statusControlGroup.datum(), selectedLegendItems); const filtered_data = context.raw_data.filter(d => { let filtered = selectedLegendItems.indexOf(d[context.config.marks[0].split]) === -1; @@ -74,9 +80,8 @@ export default function legendFilter() { }); context.draw(); - //Remove listing and display listing instruction. + //Remove listing and display bar click footnote. context.listing.wrap.selectAll('*').remove(); - context.wrap.select('#listing-instruction').style('display', 'block'); context.listing.init(filtered_data); }); } diff --git a/src/configuration/chartSettings.js b/src/configuration/chartSettings.js new file mode 100644 index 0000000..f2becb9 --- /dev/null +++ b/src/configuration/chartSettings.js @@ -0,0 +1,36 @@ +export default function chartSettings() { + return { + x: { + label: '# of Queries', + column: null, + behavior: 'flex' + }, + y: { + type: 'ordinal', + column: null, // set in syncSettings() + label: 'Form', + sort: null // set in syncSettings() + }, + marks: [ + { + type: 'bar', + per: [null], // set in syncSettings() + split: null, // set in syncSettings() + arrange: null, // set in syncSettings() + summarizeX: 'count', + tooltip: null // set in syncSettings() + } + ], + color_by: null, // set in syncSettings() + color_dom: null, // set in syncSettings() + legend: { + location: 'top', + label: null, // set in syncSettings() + order: null // set in syncSettings() + }, + margin: { + right: '50' // room for count annotation + }, + range_band: 25 + }; +} diff --git a/src/configuration/controlInputs.js b/src/configuration/controlInputs.js index 04af664..118b050 100644 --- a/src/configuration/controlInputs.js +++ b/src/configuration/controlInputs.js @@ -3,16 +3,14 @@ export default [ type: 'dropdown', option: 'y.label', label: 'Group by', - description: 'variable toggle', start: null, // set in syncControlInputs() values: null, // set in syncControlInputs() require: true }, { - type: 'dropdown', + type: 'radio', label: 'Status Group', - description: 'stratification', - options: ['marks.0.split', 'color_by'], // will want to change tooltip too + option: 'color_by_col', start: null, // set in syncControlInputs() values: null, // set in syncControlInputs() require: true @@ -32,6 +30,6 @@ export default [ { type: 'checkbox', option: 'alphabetize', - label: 'Alphabetical?' + label: 'Order Groups Alphabetically?' } ]; diff --git a/src/configuration/functions/arrayOfVariablesCheck.js b/src/configuration/functions/arrayOfVariablesCheck.js deleted file mode 100644 index eb33038..0000000 --- a/src/configuration/functions/arrayOfVariablesCheck.js +++ /dev/null @@ -1,28 +0,0 @@ -import { merge } from 'd3'; - -export default function arrayOfVariablesCheck(defaultVariables, userDefinedVariables) { - const validSetting = - userDefinedVariables instanceof Array && userDefinedVariables.length - ? merge([ - defaultVariables, - userDefinedVariables.filter( - item => - !(item instanceof Object && item.hasOwnProperty('value_col') === false) && - defaultVariables.map(d => d.value_col).indexOf(item.value_col || item) === - -1 - ) - ]).map(item => { - const itemObject = {}; - - itemObject.value_col = item instanceof Object ? item.value_col : item; - itemObject.label = - item instanceof Object - ? item.label || itemObject.value_col - : itemObject.value_col; - - return itemObject; - }) - : defaultVariables; - - return validSetting; -} diff --git a/src/configuration/index.js b/src/configuration/index.js index eeeb641..0e4733c 100644 --- a/src/configuration/index.js +++ b/src/configuration/index.js @@ -1,13 +1,15 @@ import rendererSettings from './rendererSettings'; -import webchartsSettings from './webchartsSettings'; +import chartSettings from './chartSettings'; +import listingSettings from './listingSettings'; import syncSettings from './syncSettings'; import controlInputs from './controlInputs'; import syncControlInputs from './syncControlInputs'; export default { rendererSettings, - webchartsSettings, - settings: Object.assign({}, rendererSettings, webchartsSettings), + chartSettings, + listingSettings, + settings: Object.assign({}, rendererSettings(), chartSettings(), listingSettings()), syncSettings, controlInputs, syncControlInputs diff --git a/src/configuration/listingSettings.js b/src/configuration/listingSettings.js new file mode 100644 index 0000000..3cb3df9 --- /dev/null +++ b/src/configuration/listingSettings.js @@ -0,0 +1,6 @@ +export default function listingSettings() { + return { + nRowsPerPage: 25, + exportable: true + }; +} diff --git a/src/configuration/rendererSettings.js b/src/configuration/rendererSettings.js index 37ad869..275b3d7 100644 --- a/src/configuration/rendererSettings.js +++ b/src/configuration/rendererSettings.js @@ -1,36 +1,51 @@ -export default { - //query variables - form_col: 'formoid', - formDescription_col: 'ecrfpagename', - field_col: 'fieldname', - fieldDescription_col: 'fieldname', //there is not a dscriptive column in the test data prescribed by heather - marking_group_col: 'markinggroup', - visit_col: 'folderoid', +export default function rendererSettings() { + return { + //query variables + form_col: 'formoid', + formDescription_col: 'ecrfpagename', + field_col: 'fieldname', + fieldDescription_col: null, + site_col: 'sitename', + marking_group_col: 'markinggroup', + visit_col: 'folderoid', + color_by_col: 'queryage', // options: [ 'queryage' , 'querystatus' ] or any of status_groups[].value_col - //query open time settings - open_col: 'open_time', - open_category_col: 'Query Open Time Category', - open_category_order: ['0-7 days', '8-14 days', '15-30 days', '>30 days'], + //query age + age_statuses: ['Open'], + age_col: 'qdays', + age_cutoffs: [14, 28, 56, 112], + age_range_colors: [ + '#ffffcc', + '#ffeda0', + '#fed976', + '#feb24c', + '#fd8d3c', + '#fc4e2a', + '#e31a1c', + '#bd0026', + '#800026' + ], - //query age settings - age_col: 'qdays', - age_category_col: 'Query Age Category', - age_category_order: null, - age_category_colors: ['#fd8d3c', '#fc4e2a', '#e31a1c', '#bd0026', '#800026', '#1f78b4', 'gray'], + //query status + status_col: 'querystatus', + status_order: ['Open', 'Answered', 'Closed', 'Cancelled'], + status_colors: ['#fd8d3c', '#4daf4a', '#377eb8', '#999999'], - //query status settings - status_col: 'querystatus', - status_order: ['Open', 'Answered', 'Closed', 'Cancelled'], - status_colors: ['#fb9a99', '#fdbf6f', '#1f78b4', 'gray'], + //query recency + recency_category_col: 'open_time', + recency_col: 'odays', + recency_cutoffs: [7, 14, 30], - groups: null, - status_groups: null, - site_col: 'sitename', - filters: null, - details: null, - dropdown_size: 6, - cutoff: 'All', - alphabetize: true, - exportable: true, - nRowsPerPage: 10 -}; + //miscellany + groups: null, + status_groups: null, + filters: null, + dropdown_size: 6, + details: null, + bar_arrangement: 'stacked', + cutoff: 'All', + alphabetize: true, + truncate_listing_cells: true, + truncation_cutoff: 100 + }; +} diff --git a/src/configuration/syncControlInputs.js b/src/configuration/syncControlInputs.js index a7bca1a..2e92273 100644 --- a/src/configuration/syncControlInputs.js +++ b/src/configuration/syncControlInputs.js @@ -1,35 +1,14 @@ -import clone from '../util/clone'; +import syncGroupBy from './syncControlInputs/syncGroupBy'; +import syncStatusGroup from './syncControlInputs/syncStatusGroup'; +import syncFilters from './syncControlInputs/syncFilters'; +import syncShowFirstNGroups from './syncControlInputs/syncShowFirstNGroups'; export default function syncControlInputs(controlInputs, settings) { - const syncedControlInputs = clone(controlInputs); - - //Group by - const groupByControl = syncedControlInputs.find( - controlInput => controlInput.label === 'Group by' - ); - groupByControl.values = settings.groups.map(group => group.label); - - //Status Group - const statusGroupControl = syncedControlInputs.find( - controlInput => controlInput.label === 'Status Group' - ); - statusGroupControl.values = settings.status_groups.map(status_group => status_group.value_col); - - //filters - settings.filters.forEach((filter, i) => { - filter.type = 'subsetter'; - filter.description = 'filter'; - syncedControlInputs.splice(2 + i, 0, filter); - }); - - //Show First N Groups - const nGroupsControl = syncedControlInputs.find( - controlInput => controlInput.label === 'Show First N Groups' - ); - if (nGroupsControl.values.indexOf(settings.cutoff.toString()) === -1) { - nGroupsControl.values.push(settings.cutoff.toString()); - nGroupsControl.values.sort((a, b) => (a === 'All' ? 1 : b === 'All' ? -1 : +a - +b)); - } else settings.cutoff = settings.cutoff.toString() || nGroupsControl.values[0]; + const syncedControlInputs = settings.clone(controlInputs); + syncGroupBy(syncedControlInputs, settings); + syncStatusGroup(syncedControlInputs, settings); + syncFilters(syncedControlInputs, settings); + syncShowFirstNGroups(syncedControlInputs, settings); return syncedControlInputs; } diff --git a/src/configuration/syncControlInputs/syncFilters.js b/src/configuration/syncControlInputs/syncFilters.js new file mode 100644 index 0000000..ae02e3b --- /dev/null +++ b/src/configuration/syncControlInputs/syncFilters.js @@ -0,0 +1,6 @@ +export default function syncFilters(controlInputs, settings) { + settings.filters.forEach((filter, i) => { + filter.type = 'subsetter'; + controlInputs.splice(2 + i, 0, filter); + }); +} diff --git a/src/configuration/syncControlInputs/syncGroupBy.js b/src/configuration/syncControlInputs/syncGroupBy.js new file mode 100644 index 0000000..44b74a5 --- /dev/null +++ b/src/configuration/syncControlInputs/syncGroupBy.js @@ -0,0 +1,4 @@ +export default function syncGroupBy(controlInputs, settings) { + const groupByControl = controlInputs.find(controlInput => controlInput.label === 'Group by'); + groupByControl.values = settings.groups.map(group => group.label); +} diff --git a/src/configuration/syncControlInputs/syncShowFirstNGroups.js b/src/configuration/syncControlInputs/syncShowFirstNGroups.js new file mode 100644 index 0000000..4066bd7 --- /dev/null +++ b/src/configuration/syncControlInputs/syncShowFirstNGroups.js @@ -0,0 +1,10 @@ +export default function syncShowFirstNGroups(controlInputs, settings) { + const nGroupsControl = controlInputs.find( + controlInput => controlInput.label === 'Show First N Groups' + ); + if (nGroupsControl.values.indexOf(settings.cutoff.toString()) === -1) { + settings.cutoff = settings.cutoff.toString(); + nGroupsControl.values.push(settings.cutoff.toString()); + nGroupsControl.values.sort((a, b) => (a === 'All' ? 1 : b === 'All' ? -1 : +a - +b)); + } else settings.cutoff = settings.cutoff.toString() || nGroupsControl.values[0]; +} diff --git a/src/configuration/syncControlInputs/syncStatusGroup.js b/src/configuration/syncControlInputs/syncStatusGroup.js new file mode 100644 index 0000000..5ed0ce0 --- /dev/null +++ b/src/configuration/syncControlInputs/syncStatusGroup.js @@ -0,0 +1,7 @@ +export default function syncStatusGroup(controlInputs, settings) { + const statusGroupControl = controlInputs.find( + controlInput => controlInput.label === 'Status Group' + ); + statusGroupControl.start = settings.color_by; + statusGroupControl.values = settings.status_groups.map(status_group => status_group.label); +} diff --git a/src/configuration/syncSettings.js b/src/configuration/syncSettings.js index 62091d3..0b2b4ae 100644 --- a/src/configuration/syncSettings.js +++ b/src/configuration/syncSettings.js @@ -1,86 +1,20 @@ import clone from '../util/clone'; -import arrayOfVariablesCheck from './functions/arrayOfVariablesCheck'; +import arrayOfVariablesCheck from './syncSettings/arrayOfVariablesCheck'; +import syncGroups from './syncSettings/syncGroups'; +import syncStatusGroups from './syncSettings/syncStatusGroups'; +import syncWebchartsSettings from './syncSettings/syncWebchartsSettings'; +import syncFilters from './syncSettings/syncFilters'; +import syncCutoff from './syncSettings/syncCutoff'; +import syncDetails from './syncSettings/syncCutoff'; export default function syncSettings(settings) { - const syncedSettings = clone(settings); + const syncedSettings = Object.assign({}, clone(settings), { clone, arrayOfVariablesCheck }); + syncGroups(syncedSettings); + syncStatusGroups(syncedSettings); + syncWebchartsSettings(syncedSettings); + syncFilters(syncedSettings); + syncCutoff(syncedSettings); + syncDetails(syncedSettings); - //groups - const defaultGroups = [ - { value_col: syncedSettings.form_col, label: 'Form' }, - { value_col: 'Form: Field', label: 'Form: Field' }, - { value_col: syncedSettings.site_col, label: 'Site' }, - { value_col: syncedSettings.marking_group_col, label: 'Marking Group' }, - { value_col: syncedSettings.visit_col, label: 'Visit/Folder' } - ]; - syncedSettings.groups = arrayOfVariablesCheck(defaultGroups, settings.groups); - - //status_groups - const defaultStatusGroups = [ - { - value_col: syncedSettings.age_category_col, - label: 'Query Age Category', - order: syncedSettings.age_category_order, - colors: syncedSettings.age_category_colors, - derive_source: syncedSettings.age_col - }, - { - value_col: syncedSettings.status_col, - label: 'Query Status', - order: syncedSettings.status_order, - colors: syncedSettings.status_colors - } - ]; - syncedSettings.status_groups = arrayOfVariablesCheck( - defaultStatusGroups, - settings.status_groups - ); - - //y-axis - syncedSettings.y.column = syncedSettings.form_col; - - //stratification - syncedSettings.color_by = syncedSettings.status_groups[0].value_col; - syncedSettings.color_dom = syncedSettings.status_groups[0].order - ? syncedSettings.status_groups[0].order.slice() - : null; - syncedSettings.colors = syncedSettings.status_groups[0].colors; - syncedSettings.legend.label = syncedSettings.status_groups[0].label; - syncedSettings.legend.order = syncedSettings.status_groups[0].order - ? syncedSettings.status_groups[0].order.slice() - : null; - - //mark settings - syncedSettings.marks[0].per[0] = syncedSettings.form_col; - syncedSettings.marks[0].split = syncedSettings.color_by; - syncedSettings.marks[0].tooltip = `[${syncedSettings.color_by}] - $x queries`; - - //filters - const defaultFilters = [ - { - value_col: syncedSettings.open_category_col, - derive_source: syncedSettings.open_col, - label: 'Query Open Time', - multiple: true, - order: syncedSettings.open_category_order - }, - { value_col: syncedSettings.form_col, label: 'Form', multiple: true }, - { value_col: syncedSettings.site_col, label: 'Site', multiple: true }, - { value_col: syncedSettings.marking_group_col, label: 'Marking Group', multiple: true }, - { value_col: syncedSettings.visit_col, label: 'Visit/Folder', multiple: true } - ]; - - syncedSettings.status_groups.reverse().forEach(status_group => { - status_group.multiple = true; - defaultFilters.unshift(clone(status_group)); - }); - syncedSettings.filters = arrayOfVariablesCheck(defaultFilters, settings.filters); - - //cutoff - if (!(+syncedSettings.cutoff > 0 || syncedSettings.cutoff === 'All')) - syncedSettings.cutoff = 10; - - //details - syncedSettings.details = arrayOfVariablesCheck([], settings.details); - if (syncedSettings.details.length === 0) delete syncedSettings.details; return syncedSettings; } diff --git a/src/configuration/syncSettings/arrayOfVariablesCheck.js b/src/configuration/syncSettings/arrayOfVariablesCheck.js new file mode 100644 index 0000000..9484485 --- /dev/null +++ b/src/configuration/syncSettings/arrayOfVariablesCheck.js @@ -0,0 +1,32 @@ +export default function arrayOfVariablesCheck(defaultVariables, userDefinedVariables) { + const validSetting = + userDefinedVariables instanceof Array && userDefinedVariables.length + ? d3 + .merge([ + defaultVariables, + userDefinedVariables.filter( + item => + !( + item instanceof Object && + item.hasOwnProperty('value_col') === false + ) && + defaultVariables + .map(d => d.value_col) + .indexOf(item.value_col || item) === -1 + ) + ]) + .map(item => { + const itemObject = item instanceof Object ? Object.assign({}, item) : {}; + + itemObject.value_col = item instanceof Object ? item.value_col : item; + itemObject.label = + item instanceof Object + ? item.label || itemObject.value_col + : itemObject.value_col; + + return itemObject; + }) + : defaultVariables; + + return validSetting; +} diff --git a/src/configuration/syncSettings/syncCutoff.js b/src/configuration/syncSettings/syncCutoff.js new file mode 100644 index 0000000..8fbc677 --- /dev/null +++ b/src/configuration/syncSettings/syncCutoff.js @@ -0,0 +1,3 @@ +export default function syncCutoff(settings) { + if (!(+settings.cutoff > 0 || settings.cutoff === 'All')) settings.cutoff = 10; +} diff --git a/src/configuration/syncSettings/syncDetails.js b/src/configuration/syncSettings/syncDetails.js new file mode 100644 index 0000000..1c7a1bb --- /dev/null +++ b/src/configuration/syncSettings/syncDetails.js @@ -0,0 +1,4 @@ +export default function syncDetails(settings) { + settings.details = arrayOfVariablesCheck([], settings.details); + if (settings.details.length === 0) delete settings.details; +} diff --git a/src/configuration/syncSettings/syncFilters.js b/src/configuration/syncSettings/syncFilters.js new file mode 100644 index 0000000..6734fa0 --- /dev/null +++ b/src/configuration/syncSettings/syncFilters.js @@ -0,0 +1,68 @@ +export default function syncFilters(settings) { + //recency ranges + settings.recencyRanges = settings.recency_cutoffs.map( + (d, i) => (i > 0 ? [settings.recency_cutoffs[i - 1], d] : [0, d]) + ); + settings.recencyRanges.push([ + settings.recency_cutoffs[settings.recency_cutoffs.length - 1], + null + ]); + + //recency range categories + settings.recencyRangeCategories = settings.recency_cutoffs.every( + recency_range => recency_range % 7 === 0 + ) + ? settings.recencyRanges.map( + (recencyRange, i) => + i < settings.recencyRanges.length - 1 + ? `${recencyRange.map(days => days / 7).join('-')} weeks` + : `>${recencyRange[0] / 7} weeks` + ) + : settings.recencyRanges.map( + (recencyRange, i) => + i < settings.recencyRanges.length - 1 + ? `${recencyRange.join('-')} days` + : `>${recencyRange[0]} days` + ); + + //default filters + const defaultFilters = [ + { + value_col: 'queryrecency', + label: 'Query Recency', + multiple: true + }, + { + value_col: settings.form_col, + label: 'Form', + multiple: true + }, + { + value_col: settings.site_col, + label: 'Site', + multiple: true + }, + { + value_col: settings.marking_group_col, + label: 'Marking Group', + multiple: true + }, + { + value_col: settings.visit_col, + label: 'Visit/Folder', + multiple: true + } + ]; + + //add status group variables to list of filters + settings.status_groups + .slice() + .reverse() + .forEach(status_group => { + status_group.multiple = true; + defaultFilters.unshift(settings.clone(status_group)); + }); + + //add custom filters + settings.filters = settings.arrayOfVariablesCheck(defaultFilters, settings.filters); +} diff --git a/src/configuration/syncSettings/syncGroups.js b/src/configuration/syncSettings/syncGroups.js new file mode 100644 index 0000000..3d804c8 --- /dev/null +++ b/src/configuration/syncSettings/syncGroups.js @@ -0,0 +1,10 @@ +export default function syncGroups(settings) { + const defaultGroups = [ + { value_col: settings.form_col, label: 'Form' }, + { value_col: 'Form: Field', label: 'Form: Field' }, + { value_col: settings.site_col, label: 'Site' }, + { value_col: settings.marking_group_col, label: 'Marking Group' }, + { value_col: settings.visit_col, label: 'Visit/Folder' } + ]; + settings.groups = settings.arrayOfVariablesCheck(defaultGroups, settings.groups); +} diff --git a/src/configuration/syncSettings/syncStatusGroups.js b/src/configuration/syncSettings/syncStatusGroups.js new file mode 100644 index 0000000..dd2c205 --- /dev/null +++ b/src/configuration/syncSettings/syncStatusGroups.js @@ -0,0 +1,65 @@ +export default function syncStatusGroups(settings) { + //age ranges + settings.ageRanges = settings.age_cutoffs.map( + (d, i) => (i > 0 ? [settings.age_cutoffs[i - 1], d] : [0, d]) + ); + settings.ageRanges.push([settings.age_cutoffs[settings.age_cutoffs.length - 1], null]); + + //age range categories + settings.ageRangeCategories = settings.age_cutoffs.every(age_range => age_range % 7 === 0) + ? settings.ageRanges.map( + (ageRange, i) => + i < settings.ageRanges.length - 1 + ? `${ageRange.map(days => days / 7).join('-')} wks` + : `>${ageRange[0] / 7} wks` + ) + : settings.ageRanges.map( + (ageRange, i) => + i < settings.ageRanges.length - 1 + ? `${ageRange.join('-')} days` + : `>${ageRange[0]} days` + ); + + //age range colors + let ageRangeColors = settings.age_range_colors.slice( + settings.age_range_colors.length - settings.ageRanges.length + ); + settings.status_order.forEach((status, i) => { + if (settings.age_statuses.indexOf(status) < 0) + ageRangeColors.push(settings.status_colors[i]); + }); + + //reconcile settings.status_order with settings.status_colors to ensure equal length + if (settings.status_order.length !== settings.status_colors.length) { + console.warn('The number of query statuses does not match the number of query colors:'); + console.log(settings.status_order); + console.log(settings.status_colors); + } + + //default status groups + const defaultStatusGroups = [ + { + value_col: 'queryage', // derived in ../chart/onInit/defineNewVariables + label: 'Query Age', + order: settings.ageRangeCategories.concat( + settings.status_order.filter(status => settings.age_statuses.indexOf(status) < 0) + ), + colors: ageRangeColors + }, + { + value_col: settings.status_col, + label: 'Query Status', + order: settings.status_order, + colors: settings.status_colors + } + ]; + + //add custom status groups + settings.status_groups = settings.arrayOfVariablesCheck( + defaultStatusGroups, + settings.status_groups + ); + settings.status_group = settings.status_groups.find( + status_group => status_group.value_col === settings.color_by_col + ); +} diff --git a/src/configuration/syncSettings/syncWebchartsSettings.js b/src/configuration/syncSettings/syncWebchartsSettings.js new file mode 100644 index 0000000..261bc83 --- /dev/null +++ b/src/configuration/syncSettings/syncWebchartsSettings.js @@ -0,0 +1,23 @@ +export default function syncWebchartsSettings(settings) { + //y-axis + settings.y.column = settings.form_col; + settings.y.sort = settings.alphabetize ? 'alphabetical-ascending' : 'total-descending'; + settings.y.range_band = settings.range_band || 25; + + //mark settings + settings.marks[0].per[0] = settings.form_col; + settings.marks[0].split = settings.status_group.value_col; + settings.marks[0].arrange = settings.bar_arrangement; + settings.marks[0].tooltip = `[${settings.status_group.value_col}] - $x queries`; + + //stratification + settings.color_by = settings.status_group.value_col; + settings.color_dom = settings.status_group.order ? settings.status_group.order.slice() : null; + settings.colors = settings.status_group.colors; + + //legend + settings.legend.label = settings.status_group.label; + settings.legend.order = settings.status_group.order + ? settings.status_group.order.slice() + : null; +} diff --git a/src/configuration/webchartsSettings.js b/src/configuration/webchartsSettings.js deleted file mode 100644 index dfe7df9..0000000 --- a/src/configuration/webchartsSettings.js +++ /dev/null @@ -1,32 +0,0 @@ -export default { - x: { - label: '# of Queries', - behavior: 'flex' - }, - y: { - type: 'ordinal', - column: null, // set in syncSettings() - label: 'Form', - sort: 'total-descending' - }, - marks: [ - { - type: 'bar', - per: [null], // set in syncSettings() - split: null, // set in syncSettings() - arrange: 'stacked', - summarizeX: 'count', - tooltip: null // set in syncSettings() - } - ], - color_by: null, // set in syncSettings() - color_dom: null, // set in syncSettings() - legend: { - location: 'top', - // label: 'Query Status', - label: null, - order: null // set in syncSettings() - }, - range_band: 15, - margin: { right: '50' } // room for count annotation -}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..00831c8 --- /dev/null +++ b/src/index.js @@ -0,0 +1,51 @@ +import './util/polyfills'; +import clone from './util/clone'; +import configuration from './configuration/index'; +import layout from './layout'; +import styles from './styles'; +import { createControls, createChart, createTable } from 'webcharts'; +import chartCallbacks from './chart/index'; +import listingCallbacks from './listing/index'; + +export default function queryOverview(element, settings) { + //Settings + const mergedSettings = Object.assign({}, configuration.settings, settings); + const syncedSettings = configuration.syncSettings(mergedSettings); + const syncedControlInputs = configuration.syncControlInputs( + configuration.controlInputs, + syncedSettings + ); + + //Layout and styles + const containers = layout(element); + styles(); + + //Controls + const controls = createControls(containers.controls.node(), { + location: 'top', + inputs: syncedControlInputs + }); + controls.element = element; + + //Chart + const chart = createChart(containers.chart.node(), syncedSettings, controls); + chart.element = element; + chart.initialSettings = clone(mergedSettings); + for (const callback in chartCallbacks) + chart.on(callback.substring(2).toLowerCase(), chartCallbacks[callback]); + + //Listing + const listing = createTable(containers.listing.node(), { + sortable: false, + exportable: syncedSettings.exportable + }); + listing.element = element; + for (const callback in listingCallbacks) + listing.on(callback.substring(2).toLowerCase(), listingCallbacks[callback]); + + //Intertwine + chart.listing = listing; + listing.chart = chart; + + return chart; +} diff --git a/src/layout.js b/src/layout.js new file mode 100644 index 0000000..f34c686 --- /dev/null +++ b/src/layout.js @@ -0,0 +1,22 @@ +export default function layout(element) { + const containers = { + main: d3 + .select(element) + .append('div') + .classed('.query-overview', true) + }; + + containers.topRow = containers.main.append('div').classed('qo-row qo-row--top', true); + containers.controls = containers.topRow + .append('div') + .classed('qo-component qo-component--controls', true); + containers.chart = containers.controls + .append('div') + .classed('qo-component qo-component--chart', true); + containers.bottomRow = containers.main.append('div').classed('qo-row qo-row--bottom', true); + containers.listing = containers.bottomRow + .append('div') + .classed('qo-component qo-component--listing', true); + + return containers; +} diff --git a/src/listing/defineStyles.js b/src/listing/defineStyles.js deleted file mode 100644 index 619e40c..0000000 --- a/src/listing/defineStyles.js +++ /dev/null @@ -1,20 +0,0 @@ -export default function defineStyles() { - const styles = [ - '.query-table-container {' + - ' overflow-x: auto;' + - ' width : 100%;' + - ' transform: rotate(180deg);' + - ' -webkit-transform:rotate(180deg); ' + - '}', - '.query-table {' + - ' transform: rotate(180deg);' + - ' -webkit-transform:rotate(180deg); ' + - '}' - ]; - - //Attach styles to DOM. - this.style = document.createElement('style'); - this.style.type = 'text/css'; - this.style.innerHTML = styles.join('\n'); - document.getElementsByTagName('head')[0].appendChild(this.style); -} diff --git a/src/listing/index.js b/src/listing/index.js new file mode 100644 index 0000000..27840fd --- /dev/null +++ b/src/listing/index.js @@ -0,0 +1,13 @@ +import onInit from './onInit'; +import onLayout from './onLayout'; +import onPreprocess from './onPreprocess'; +import onDraw from './onDraw'; +import onDestroy from './onDestroy'; + +export default { + onInit, + onLayout, + onPreprocess, + onDraw, + onDestroy +}; diff --git a/src/listing/onDraw.js b/src/listing/onDraw.js index ce38e44..face692 100644 --- a/src/listing/onDraw.js +++ b/src/listing/onDraw.js @@ -1,8 +1,13 @@ -import onClick from './onDraw/onClick'; +import updateColumnSorting from './onDraw/updateColumnSorting'; +import truncateCellText from './onDraw/truncateCellText'; import moveScrollBarLeft from './onDraw/moveScrollBarLeft'; export default function onDraw() { - onClick.call(this); + //Update default Webcharts column sorting. + updateColumnSorting.call(this); + + //Truncate cells with length greater than `settings.truncation_cutoff`. + truncateCellText.call(this); //Move table scrollbar all the way to the left. moveScrollBarLeft.call(this); diff --git a/src/listing/onDraw/truncateCellText.js b/src/listing/onDraw/truncateCellText.js new file mode 100644 index 0000000..58e5fb8 --- /dev/null +++ b/src/listing/onDraw/truncateCellText.js @@ -0,0 +1,8 @@ +export default function truncateCellText() { + if (this.data.raw.length) + this.tbody + .selectAll('td') + .attr('title', d => d.text) + .filter(d => d.text.length > this.chart.initialSettings.truncation_cutoff) + .text(d => `${d.text.substring(0, this.chart.initialSettings.truncation_cutoff)}...`); +} diff --git a/src/listing/onDraw/onClick.js b/src/listing/onDraw/updateColumnSorting.js similarity index 95% rename from src/listing/onDraw/onClick.js rename to src/listing/onDraw/updateColumnSorting.js index dbb6134..6268e9c 100644 --- a/src/listing/onDraw/onClick.js +++ b/src/listing/onDraw/updateColumnSorting.js @@ -1,6 +1,6 @@ -import manualSort from './onClick/manualSort'; +import manualSort from './updateColumnSorting/manualSort'; -export default function onClick() { +export default function updateColumnSorting() { const context = this; this.thead_cells.on('click', function(d) { diff --git a/src/listing/onDraw/onClick/manualSort.js b/src/listing/onDraw/updateColumnSorting/manualSort.js similarity index 100% rename from src/listing/onDraw/onClick/manualSort.js rename to src/listing/onDraw/updateColumnSorting/manualSort.js diff --git a/src/listing/onLayout.js b/src/listing/onLayout.js index 7b87207..3961374 100644 --- a/src/listing/onLayout.js +++ b/src/listing/onLayout.js @@ -1,8 +1,8 @@ -import resetListing from './onLayout/resetListing'; +import addResetButton from './onLayout/addResetButton'; import addTableContainer from './onLayout/addTableContainer'; export default function onLayout() { - resetListing.call(this); + addResetButton.call(this); addTableContainer.call(this); this.wrap.select('.sortable-container').classed('hidden', false); this.table.style('width', '100%').style('display', 'table'); diff --git a/src/listing/onLayout/resetListing.js b/src/listing/onLayout/addResetButton.js similarity index 72% rename from src/listing/onLayout/resetListing.js rename to src/listing/onLayout/addResetButton.js index a1872ca..f146075 100644 --- a/src/listing/onLayout/resetListing.js +++ b/src/listing/onLayout/addResetButton.js @@ -1,13 +1,7 @@ -export default function resetListing() { - this.wrap +export default function addResetButton() { + this.resetButton = this.wrap .insert('button', ':first-child') - .attr('id', 'clear-listing') - .style({ - margin: '5px', - padding: '5px', - float: 'left', - display: 'block' - }) + .classed('qo-button qo-button--reset-listing', true) .text('Reset listing') .on('click', () => { this.wrap.selectAll('*').remove(); @@ -28,6 +22,5 @@ export default function resetListing() { fill: d => this.chart.colorScale(d.key) }); this.chart.listing.init(this.chart.filtered_data); - this.chart.wrap.select('#listing-instruction').style('display', 'block'); }); } diff --git a/src/listing/onLayout/addTableContainer.js b/src/listing/onLayout/addTableContainer.js index b877873..f34c4b0 100644 --- a/src/listing/onLayout/addTableContainer.js +++ b/src/listing/onLayout/addTableContainer.js @@ -4,10 +4,10 @@ export default function addTableContainer() { const table = this.table.node(); this.tableContainer = this.wrap .append('div') - .classed('query-table-container', true) + .classed('qo-table-container', true) .node(); - this.wrap.select('table').classed('query-table', true); // I want to ensure that no other webcharts tables get flipped upside down + this.wrap.select('table').classed('qo-table', true); // I want to ensure that no other webcharts tables get flipped upside down table.parentNode.insertBefore(this.tableContainer, table); this.tableContainer.appendChild(table); diff --git a/src/listing/onPreprocess.js b/src/listing/onPreprocess.js new file mode 100644 index 0000000..c0c0cf1 --- /dev/null +++ b/src/listing/onPreprocess.js @@ -0,0 +1 @@ +export default function onPreprocess() {} diff --git a/src/styles.js b/src/styles.js new file mode 100644 index 0000000..b7bbe27 --- /dev/null +++ b/src/styles.js @@ -0,0 +1,208 @@ +export default function styles() { + const styles = [ + + /***--------------------------------------------------------------------------------------\ + Framework + \--------------------------------------------------------------------------------------***/ + + '.query-overview {' + + ' width: 100%;' + + ' display: inline-block;' + + '}', + '.qo-row {' + + ' width: 100%;' + + ' display: inline-block;' + + '}', + '.qo-component {' + + '}', + '.qo-row--top {' + + '}', + '.qo-row--bottom {' + + '}', + + /***--------------------------------------------------------------------------------------\ + Controls + \--------------------------------------------------------------------------------------***/ + + '.qo-component--controls {' + + ' width: 100%;' + + '}', + '.qo-component--controls .wc-controls {' + + ' margin-bottom: 0;' + + '}', + '.qo-control-grouping {' + + ' display: inline-block;' + + '}', + '.qo-button {' + + ' float: left;' + + ' display: block;' + + '}', + '.qo-control-grouping--label,' + + '.wc-control-label {' + + ' cursor: help;' + + ' margin-bottom: 3px;' + + '}', + '.qo-control-grouping--label {' + + ' text-align: center;' + + ' width: 100%;' + + ' font-size: 24px;' + + ' border-bottom: 2px solid #aaa;' + + '}', + '.span-description {' + + ' display: none !important;' + + '}', + + /****---------------------------------------------------------------------------------\ + Other controls + \---------------------------------------------------------------------------------****/ + + '.qo-control-grouping--other-controls {' + + ' width: 20%;' + + ' float: right;' + + '}', + '.qo-control-grouping--other-controls .control-group {' + + ' width: 100%;' + + ' margin-bottom: 15px;' + + '}', + '.qo-control-grouping--other-controls .control-group:nth-child(n+3) {' + + ' border-top: 1px solid #aaa;' + + '}', + '.qo-control-grouping--other-controls .control-group .wc-control-label {' + + ' text-align: center;' + + ' font-size: 110%;' + + '}', + + //dropdowns + '.qo-dropdown {' + + '}', + '.qo-dropdown .wc-control-label {' + + '}', + '.qo-dropdown .changer {' + + ' margin: 0 auto;' + + '}', + + //radio buttons + '.qo-radio {' + + ' display: flex !important;' + + ' justify-content: center;' + + ' flex-wrap: wrap;' + + '}', + '.qo-radio .wc-control-label {' + + ' width: 100%;' + + '}', + '.qo-radio .radio {' + + ' margin-top: 0 !important;' + + '}', + + //checkboxes + '.qo-checkbox {' + + ' display: flex !important;' + + ' justify-content: center;' + + '}', + '.qo-checkbox .wc-control-label {' + + ' margin-right: 5px;' + + '}', + '.qo-checkbox .changer {' + + ' margin-top: 5px !important;' + + '}', + + /****---------------------------------------------------------------------------------\ + Filters + \---------------------------------------------------------------------------------****/ + + '.qo-control-grouping--filters {' + + ' width: 20%;' + + ' float: left;' + + ' display: flex;' + + ' flex-wrap: wrap;' + + ' justify-content: space-evenly;' + + '}', + '.qo-subsetter {' + + ' margin: 5px 2px !important;' + + ' border-top: 1px solid #aaa;' + + ' padding-top: 5px;' + + '}', + '.qo-subsetter .wc-control-label {' + + ' margin: 0 5px 3px 0;' + + ' text-align: center;' + + '}', + '.qo-select-all {' + + '}', + '.qo-subsetter .changer {' + + ' margin: 0 auto;' + + '}', + + /***--------------------------------------------------------------------------------------\ + Chart + \--------------------------------------------------------------------------------------***/ + + '.qo-component--chart {' + + ' width: 58%;' + + ' margin: 0 auto;' + + ' position: relative;' + + '}', + '.qo-button--reset-chart {' + + ' position: absolute;' + + ' top: 0;' + + ' left: 0;' + + ' z-index: 2;' + + ' width: 91px;' + + ' padding: 3px 0;' + + '}', + '.qo-button--undo {' + + ' position: absolute;' + + ' bottom: 0;' + + ' left: 0;' + + ' z-index: 2;' + + ' width: 91px;' + + ' padding: 3px 0;' + + '}', + '.qo-component--chart .wc-chart {' + + ' z-index: 1;' + + '}', + '.qo-component--chart .legend-title {' + + ' cursor: help;' + + '}', + '.qo-component--chart .legend-item {' + + ' cursor: pointer;' + + ' border-radius: 4px;' + + ' padding: 5px;' + + ' padding-left: 8px;' + + ' margin-right: 5px !important;' + + '}', + '.qo-footnote {' + + ' width: 100%;' + + ' text-align: center;' + + ' font-style: italic;' + + '}', + + /***--------------------------------------------------------------------------------------\ + Listing + \--------------------------------------------------------------------------------------***/ + + '.qo-component--listing {' + + ' width: 100%;' + + '}', + '.qo-button--reset-listing {' + + ' padding: 3px;' + + ' margin: 10px 5px 10px 0;' + + '}', + '.qo-table-container {' + + ' overflow-x: auto;' + + ' width: 100%;' + + ' transform: rotate(180deg);' + + ' -webkit-transform: rotate(180deg); ' + + '}', + '.qo-table {' + + ' width: 100%;' + + ' transform: rotate(180deg);' + + ' -webkit-transform: rotate(180deg); ' + + '}', + ]; + + //Attach styles to DOM. + const style = document.createElement('style'); + style.type = 'text/css'; + style.innerHTML = styles.join('\n'); + document.getElementsByTagName('head')[0].appendChild(style); +} diff --git a/src/util/d3-selection-moveToBack.js b/src/util/d3-selection-moveToBack.js deleted file mode 100644 index 1cad002..0000000 --- a/src/util/d3-selection-moveToBack.js +++ /dev/null @@ -1,6 +0,0 @@ -d3.selection.prototype.moveToBack = function() { - return this.each(function() { - var firstChild = this.parentNode.firstChild; - if (firstChild) this.parentNode.insertBefore(this, firstChild); - }); -}; diff --git a/src/util/d3-selection-moveToFront.js b/src/util/d3-selection-moveToFront.js deleted file mode 100644 index 41170f0..0000000 --- a/src/util/d3-selection-moveToFront.js +++ /dev/null @@ -1,5 +0,0 @@ -d3.selection.prototype.moveToFront = function() { - return this.each(function() { - this.parentNode.appendChild(this); - }); -}; diff --git a/src/util/flatMap.js b/src/util/flatMap.js deleted file mode 100644 index f1e61fb..0000000 --- a/src/util/flatMap.js +++ /dev/null @@ -1,5 +0,0 @@ -// from https://gist.github.com/samgiles/762ee337dff48623e729 - -export default (Array.prototype.flatMap = function(lambda) { - return Array.prototype.concat.apply([], this.map(lambda)); -}); diff --git a/src/util/polyfills.js b/src/util/polyfills.js index 17f4054..b1e9523 100644 --- a/src/util/polyfills.js +++ b/src/util/polyfills.js @@ -1,4 +1,3 @@ -//[].find if (!Array.prototype.find) { Object.defineProperty(Array.prototype, 'find', { value: function(predicate) { @@ -43,7 +42,6 @@ if (!Array.prototype.find) { }); } -//{}.assign if (typeof Object.assign != 'function') { Object.defineProperty(Object, 'assign', { value: function assign(target, varArgs) { @@ -78,7 +76,6 @@ if (typeof Object.assign != 'function') { }); } -//[].findIndex if (!Array.prototype.findIndex) { Object.defineProperty(Array.prototype, 'findIndex', { value: function(predicate) { @@ -122,3 +119,20 @@ if (!Array.prototype.findIndex) { } }); } + +Array.prototype.flatMap = function(lambda) { + return Array.prototype.concat.apply([], this.map(lambda)); +}; + +d3.selection.prototype.moveToBack = function() { + return this.each(function() { + var firstChild = this.parentNode.firstChild; + if (firstChild) this.parentNode.insertBefore(this, firstChild); + }); +}; + +d3.selection.prototype.moveToFront = function() { + return this.each(function() { + this.parentNode.appendChild(this); + }); +}; diff --git a/src/wrapper.js b/src/wrapper.js deleted file mode 100644 index 7a0ee73..0000000 --- a/src/wrapper.js +++ /dev/null @@ -1,63 +0,0 @@ -import './util/polyfills'; -import './util/d3-selection-moveToFront'; -import './util/d3-selection-moveToBack'; -import clone from './util/clone'; - -import configuration from './configuration/index'; - -import { createControls, createChart, createTable } from 'webcharts'; - -//chart callbacks -import onInit from './chart/onInit'; -import onLayout from './chart/onLayout'; -import onPreprocess from './chart/onPreprocess'; -import onDataTransform from './chart/onDataTransform'; -import onDraw from './chart/onDraw'; -import onResize from './chart/onResize'; -import onDestroy from './chart/onDestroy'; - -//listing callbacks -import onInitL from './listing/onInit'; -import onLayoutL from './listing/onLayout'; -import onDrawL from './listing/onDraw'; -import onDestroyL from './listing/onDestroy'; -import defineStyles from './listing/defineStyles'; - -export default function queryOverview(element, settings) { - const mergedSettings = Object.assign({}, configuration.settings, settings); - const syncedSettings = configuration.syncSettings(mergedSettings); - const syncedControlInputs = configuration.syncControlInputs( - configuration.controlInputs, - syncedSettings - ); - const controls = createControls(element, { - location: 'top', - inputs: syncedControlInputs - }); - const chart = createChart(element, syncedSettings, controls); - const listing = createTable(element, { - sortable: false, - exportable: syncedSettings.exportable - }); - - chart.initialSettings = clone(mergedSettings); - chart.on('init', onInit); - chart.on('layout', onLayout); - chart.on('preprocess', onPreprocess); - chart.on('datatransform', onDataTransform); - chart.on('draw', onDraw); - chart.on('resize', onResize); - - listing.on('init', onInitL); - listing.on('layout', onLayoutL); - listing.on('draw', onDrawL); - listing.on('destroy', onDestroyL); - - chart.listing = listing; - listing.chart = chart; - - //add Table stylesheet - defineStyles.call(listing); - - return chart; -} diff --git a/build/test-page/index.css b/test-page/index.css similarity index 76% rename from build/test-page/index.css rename to test-page/index.css index 5ba669a..9f2b3f0 100644 --- a/build/test-page/index.css +++ b/test-page/index.css @@ -6,20 +6,20 @@ } #title { width: 96%; - padding: 0 0 1% 0; + padding: 0 0 12px 0; border-bottom: 2px solid lightgray; - margin: 2% 2% 1% 2%; + margin: 24px 2% 12px 2%; font-size: 32px; font-weight: normal; } #subtitle { width: 96%; - margin: 0 2% 1% 2%; + margin: 0 2% 12px 2%; font-size: 24px; font-weight: lighter; } #container { - width: 50%; - margin: 1% 2%; + width: 96%; + margin: 12px 2%; display: inline-block; } diff --git a/build/test-page/index.html b/test-page/index.html similarity index 66% rename from build/test-page/index.html rename to test-page/index.html index 489537f..c45695d 100644 --- a/build/test-page/index.html +++ b/test-page/index.html @@ -5,10 +5,10 @@ - - + + - + diff --git a/test-page/index.js b/test-page/index.js new file mode 100644 index 0000000..daafd75 --- /dev/null +++ b/test-page/index.js @@ -0,0 +1,16 @@ +d3.csv( + //'https://raw.githubusercontent.com/RhoInc/viz-library/master/data/queries/queries.csv', + '../../viz-library/data/dataCleaning/queries/queries.csv', + function(error,data) { + if (error) + console.log(error); + + var settings = { + }; + var instance = queryOverview( + '#container', + settings + ); + instance.init(data); + } +);