diff --git a/build/safetyResultsOverTime.js b/build/safetyResultsOverTime.js index 6aa6cd0..bfc1700 100644 --- a/build/safetyResultsOverTime.js +++ b/build/safetyResultsOverTime.js @@ -156,7 +156,7 @@ function assignKey(to, from, key) { var val = from[key]; - if (val === undefined || val === null) { + if (val === undefined) { return; } @@ -208,8 +208,7 @@ return target; } - var defaultSettings = { - //Custom settings for this template + var rendererSettings = { id_col: 'USUBJID', time_settings: { value_col: 'VISIT', @@ -232,10 +231,11 @@ missingValues: ['', 'NA', 'N/A'], visits_without_data: false, unscheduled_visits: false, - unscheduled_visit_values: null, // takes precedence over unscheduled_visit_pattern - unscheduled_visit_pattern: /unscheduled|early termination/i, + unscheduled_visit_pattern: '/unscheduled|early termination/i', + unscheduled_visit_values: null // takes precedence over unscheduled_visit_pattern + }; - //Standard webcharts settings + var webchartsSettings = { x: { column: null, // set in syncSettings() type: 'ordinal', @@ -272,6 +272,8 @@ aspect: 3 }; + var defaultSettings = merge(rendererSettings, webchartsSettings); + // Replicate settings in multiple places in the settings object function syncSettings(settings) { settings.x.column = settings.time_settings.value_col; @@ -293,6 +295,19 @@ settings.marks[0].per = [settings.color_by]; settings.margin = settings.margin || { bottom: settings.time_settings.vertical_space }; + //Convert unscheduled_visit_pattern from string to regular expression. + if ( + typeof settings.unscheduled_visit_pattern === 'string' && + settings.unscheduled_visit_pattern !== '' + ) { + var flags = settings.unscheduled_visit_pattern.replace(/.*?\/([gimy]*)$/, '$1'), + pattern = settings.unscheduled_visit_pattern.replace( + new RegExp('^/(.*?)/' + flags + '$'), + '$1' + ); + settings.unscheduled_visit_regex = new RegExp(pattern, flags); + } + return settings; } @@ -334,18 +349,17 @@ // Map values from settings to control inputs function syncControlInputs(controlInputs, settings) { - var measureControl = controlInputs.filter(function(controlInput) { - return controlInput.label === 'Measure'; - })[0], - groupControl = controlInputs.filter(function(controlInput) { - return controlInput.label === 'Group'; - })[0]; - //Sync measure control. + var measureControl = controlInputs.filter(function(controlInput) { + return controlInput.label === 'Measure'; + })[0]; measureControl.value_col = settings.measure_col; measureControl.start = settings.start_value; //Sync group control. + var groupControl = controlInputs.filter(function(controlInput) { + return controlInput.label === 'Group'; + })[0]; groupControl.start = settings.color_by; settings.groups .filter(function(group) { @@ -380,6 +394,17 @@ }); } + //Remove unscheduled visit conrol if unscheduled visit pattern is unscpecified. + if (!settings.unscheduled_visit_regex) + controlInputs.splice( + controlInputs + .map(function(controlInput) { + return controlInput.label; + }) + .indexOf('Unscheduled visits'), + 1 + ); + return controlInputs; } @@ -437,8 +462,8 @@ ? _this.config.unscheduled_visit_values.indexOf( d[_this.config.time_settings.value_col] ) > -1 - : _this.config.unscheduled_visit_pattern - ? _this.config.unscheduled_visit_pattern.test( + : _this.config.unscheduled_visit_regex + ? _this.config.unscheduled_visit_regex.test( d[_this.config.time_settings.value_col] ) : false; @@ -716,9 +741,9 @@ this.config.x.domain = this.config.x.domain.filter(function(visit) { return _this.config.unscheduled_visit_values.indexOf(visit) < 0; }); - else if (this.config.unscheduled_visit_pattern) + else if (this.config.unscheduled_visit_regex) this.config.x.domain = this.config.x.domain.filter(function(visit) { - return !_this.config.unscheduled_visit_pattern.test(visit); + return !_this.config.unscheduled_visit_regex.test(visit); }); } } diff --git a/package.json b/package.json index 424c64d..9112ebf 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,11 @@ ], "dependencies": { "d3": "^3", - "webcharts": "^1.9" + "webcharts": "^1" }, "scripts": { - "build": "npm run bundle && npm run format", + "build": "npm run bundle && npm run format && npm run build-md", + "build-md": "node ./scripts/configuration-markdown.js", "bundle": "rollup -c", "format": "npm run format-src && npm run format-build", "format-src": "prettier --print-width=100 --tab-width=4 --single-quote --write src/**/*.js", diff --git a/scripts/configuration-markdown.js b/scripts/configuration-markdown.js new file mode 100644 index 0000000..8da28d0 --- /dev/null +++ b/scripts/configuration-markdown.js @@ -0,0 +1,119 @@ +var pkg = require('../package'), + schema = require('../settings-schema'), + properties = schema.properties, + markdown = [], + fs = require('fs'), + webchartsSettingsFlag = 0, + webchartsSettings = fs.readFileSync('./src/defaultSettings.js', 'utf8') + .split('\n') + .filter(line => { + if (line.indexOf('const webchartsSettings') > -1) + webchartsSettingsFlag = 1; + + if (webchartsSettingsFlag === 1 && /};/.test(line)) + webchartsSettingsFlag = 0; + + return webchartsSettingsFlag; + }); + webchartsSettings.splice(0,1,'{\r'); + webchartsSettings.push('}'); + +schema.overview + .split('\n') + .forEach(paragraph => { + markdown.push(paragraph); + markdown.push(''); + }); +markdown.push(`# Renderer-specific settings`); +markdown.push(`The sections below describe each ${pkg.name} setting as of version ${schema.version}.`); +markdown.push(``); + +//Build configuration markdown array. +var keys = Object.keys(properties); + keys.forEach((property,i) => { + var setting = properties[property]; + markdown.push(`## settings.${property}`); + markdown.push(`\`${setting.type}\``); + markdown.push(``); + markdown.push(`${setting.description}`); + if (setting.type !== 'object') + markdown.push(``); + + //Primitive types + if (['object', 'array'].indexOf(setting.type) === -1) + markdown.push(`**default:** ${ + setting.default + ? ('`"' + setting.default + '"`') + : 'none'}`); + //Arrays + else if (setting.type === 'array') { + //of primitive types + if (setting.type === 'array' && ['object', 'array'].indexOf(setting.items.type) === -1) + markdown.push(`**default:** ${ + setting.defaultArray + ? `[${setting.defaultArray.map(item => `"${item}"`).join(', ')}]` + : 'none'}`); + //of objects + else if (setting.items.type === 'object') { + + if (setting.default) { + markdown.push(`**default:**`); + markdown.push(`\`\`\``); + markdown.push(`${JSON.stringify(setting.default, null, 2)}`); + markdown.push(`\`\`\``); + markdown.push(``); + } else + markdown.push(`**default:** none`); + + var subProperties = setting.items.properties; + Object.keys(subProperties).forEach(subProperty => { + var subSetting = subProperties[subProperty]; + markdown.push(``); + markdown.push(`### settings.${property}[].${subProperty}`); + markdown.push(`\`${subSetting.type}\``); + markdown.push(``); + markdown.push(`${subSetting.title}`); + }); + } + } + //Objects + else if (setting.type === 'object') { + var subKeys = Object.keys(setting.properties); + subKeys.forEach((subProperty,i) => { + var subSetting = setting.properties[subProperty]; + markdown.push(``); + markdown.push(`## settings.${property}.${subProperty}`); + markdown.push(`\`${subSetting.type}\``); + markdown.push(``); + markdown.push(`${subSetting.title}`); + markdown.push(``); + markdown.push(`**default:** ${ + subSetting.default + ? ('`"' + subSetting.default + '"`') + : 'none'}`); + }); + } + + if (i < keys.length - 1) { + markdown.push(``); + markdown.push(``); + markdown.push(``); + } + }); + +markdown.push(``); +markdown.push(`# Webcharts-specific 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('```'); + +fs.writeFile( + './scripts/configuration.md', + markdown.join('\n'), + (err) => { + if (err) + console.log(err); + console.log('The configuration markdown file was built!'); + }); diff --git a/scripts/configuration.md b/scripts/configuration.md new file mode 100644 index 0000000..8ba1ff2 --- /dev/null +++ b/scripts/configuration.md @@ -0,0 +1,224 @@ +The most straightforward way to customize the Safety Results Over Time is by using a configuration object whose properties describe the behavior and appearance of the chart. Since the Safety Results Over Time is a Webcharts `chart` object, many default Webcharts settings are set in the [defaultSettings.js file](https://github.com/RhoInc/safety-results-over-time/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. + +In addition to the standard Webcharts settings several custom settings not available in the base Webcharts library have been added to the Safety Results Over Time to facilitate data mapping and other custom functionality. These custom settings are described in detail below. All defaults can be overwritten by users. + +# Renderer-specific settings +The sections below describe each safety-results-over-time setting as of version 2.2.0. + +## settings.id_col +`string` + +unique identifier variable name + +**default:** `"USUBJID"` + + + +## settings.time_settings +`object` + +visit metadata + +## settings.time_settings.value_col +`string` + +Visit variable name + +**default:** `"VISIT"` + +## settings.time_settings.label +`string` + +Visit variable label + +**default:** `"Visit"` + +## settings.time_settings.order_col +`string` + +Visit ordering variable name + +**default:** `"VISITNUM"` + +## settings.time_settings.order +`array` + +Visit order + +**default:** none + +## settings.time_settings.rotate_tick_labels +`boolean` + +Rotate tick labels 45 degrees? + +**default:** `"true"` + +## settings.time_settings.vertical_space +`number` + +Rotated tick label spacing + +**default:** `"100"` + + + +## settings.measure_col +`string` + +measure variable name + +**default:** `"TEST"` + + + +## settings.unit_col +`string` + +measure unit variable name + +**default:** `"STRESU"` + + + +## settings.value_col +`string` + +result variable name + +**default:** `"STRESN"` + + + +## settings.normal_col_low +`string` + +LLN variable name + +**default:** `"STNRLO"` + + + +## settings.normal_col_high +`string` + +ULN variable name + +**default:** `"STNRHI"` + + + +## settings.start_value +`string` + +value of measure to display initially + +**default:** none + + + +## settings.filters +`array` + +an array of filter variables and associated metadata + +**default:** none + +### settings.filters[].value_col +`string` + +Variable name + +### settings.filters[].label +`string` + +Variable label + + + +## settings.groups +`array` + +an array of grouping variables and associated metadata + +**default:** none + +### settings.groups[].value_col +`string` + +Variable name + +### settings.groups[].label +`string` + +Variable label + + + +## settings.boxplots +`boolean` + +controls initial display of box plots + +**default:** `"true"` + + + +## settings.violins +`boolean` + +controls initial display of violin plots + +**default:** none + + + +## settings.missingValues +`array` + +an array of strings that identify missing values in both the measure and result variables + +**default:** ["", "NA", "N/A"] + + + +## settings.visits_without_data +`boolean` + +controls display of visits without data for the current measure + +**default:** none + + + +## settings.unscheduled_visits +`boolean` + +controls display of unscheduled visits + +**default:** none + + + +## settings.unscheduled_visit_pattern +`string` + +a regular expression that identifies unscheduled visits + +**default:** `"/unscheduled|early termination/i"` + + + +## settings.unscheduled_visits_values +`array` + +an array of strings that identify unscheduled visits; overrides unscheduled_visit_pattern + +**default:** none + +# Webcharts-specific settings +The object below contains each Webcharts setting as of version 2.2.0. + +``` +{ x: { column: null, // set in syncSettings() type: 'ordinal', label: null, behavior: 'flex', sort: 'alphabetical-ascending', tickAttr: null }, y: { column: null, // set in syncSettings() type: 'linear', label: null, behavior: 'flex', stat: 'mean', format: '0.2f' }, marks: [ { type: 'line', per: null, // set in syncSettings() attributes: { 'stroke-width': 2, 'stroke-opacity': 1, display: 'none' } } ], legend: { mark: 'square' }, color_by: null, // set in syncSettings() resizable: true, gridlines: 'y', aspect: 3 } +``` \ No newline at end of file diff --git a/scripts/rollup.wrapper.config.js b/scripts/rollup.wrapper.config.js deleted file mode 100644 index 72f4d91..0000000 --- a/scripts/rollup.wrapper.config.js +++ /dev/null @@ -1,28 +0,0 @@ -import babel from 'rollup-plugin-babel'; - -module.exports = { - moduleName: 'safetyResultsOverTime', - entry: './src/wrapper.js', - format: 'iife', - globals: { - webcharts: 'webCharts', - d3: 'd3' - }, - plugins: [ - babel( - { - 'presets': [ - [ - 'es2015', - { - 'modules': false - } - ] - ], - 'plugins': [ - 'external-helpers' - ], - 'exclude': 'node_modules/**' - }) - ] -}; diff --git a/settings-schema.json b/settings-schema.json new file mode 100644 index 0000000..fa186ee --- /dev/null +++ b/settings-schema.json @@ -0,0 +1,176 @@ +{"title": "settings" +,"description": "JSON schema for the configuration of safety-results-over-time" +,"overview": "The most straightforward way to customize the Safety Results Over Time is by using a configuration object whose properties describe the behavior and appearance of the chart. Since the Safety Results Over Time is a Webcharts `chart` object, many default Webcharts settings are set in the [defaultSettings.js file](https://github.com/RhoInc/safety-results-over-time/blob/master/src/defaultSettings.js) as [described below](#Webcharts-Settings). Refer to the [Webcharts documentation](https://github.com/RhoInc/Webcharts/wiki/Chart-Configuration) for more details on these settings.\nIn addition to the standard Webcharts settings several custom settings not available in the base Webcharts library have been added to the Safety Results Over Time to facilitate data mapping and other custom functionality. These custom settings are described in detail below. All defaults can be overwritten by users." +,"version": "2.2.0" +,"type": "object" +,"properties": + {"id_col": + {"title": "ID" + ,"description": "unique identifier variable name" + ,"default": "USUBJID" + ,"type": "string" + } + ,"time_settings": + {"title": "Time settings" + ,"description": "visit metadata" + ,"type": "object" + ,"properties": + {"value_col": + {"title": "Visit variable name" + ,"default": "VISIT" + ,"type": "string" + } + ,"label": + {"title": "Visit variable label" + ,"default": "Visit" + ,"type": "string" + } + ,"order_col": + {"title": "Visit ordering variable name" + ,"default": "VISITNUM" + ,"type": "string" + } + ,"order": + {"title": "Visit order" + ,"type": "array" + ,"items": + {"title": "Visit value" + ,"type": "string" + } + } + ,"rotate_tick_labels": + {"title": "Rotate tick labels 45 degrees?" + ,"default": true + ,"type": "boolean" + } + ,"vertical_space": + {"title": "Rotated tick label spacing" + ,"default": 100 + ,"type": "number" + } + } + } + ,"measure_col": + {"title": "Measure" + ,"description": "measure variable name" + ,"default": "TEST" + ,"type": "string" + } + ,"unit_col": + {"title": "Unit" + ,"description": "measure unit variable name" + ,"default": "STRESU" + ,"type": "string" + } + ,"value_col": + {"title": "Result" + ,"description": "result variable name" + ,"default": "STRESN" + ,"type": "string" + } + ,"normal_col_low": + {"title": "Lower Limit of Normal" + ,"description": "LLN variable name" + ,"default": "STNRLO" + ,"type": "string" + } + ,"normal_col_high": + {"title": "Upper Limit of Normal" + ,"description": "ULN variable name" + ,"default": "STNRHI" + ,"type": "string" + } + ,"start_value": + {"title": "Initially Displayed Measure" + ,"description": "value of measure to display initially" + ,"type": "string" + } + ,"filters": + {"title": "Filters" + ,"description": "an array of filter variables and associated metadata" + ,"type": "array" + ,"items": + {"type": "object" + ,"properties": + {"value_col": + {"title": "Variable name" + ,"type": "string" + } + ,"label": + {"title": "Variable label" + ,"type": "string" + } + } + } + } + ,"groups": + {"title": "Groupings" + ,"description": "an array of grouping variables and associated metadata" + ,"type": "array" + ,"items": + {"type": "object" + ,"properties": + {"value_col": + {"title": "Variable name" + ,"type": "string" + } + ,"label": + {"title": "Variable label" + ,"type": "string" + } + } + } + } + ,"boxplots": + {"title": "Display box plots?" + ,"description": "controls initial display of box plots" + ,"default": true + ,"type": "boolean" + } + ,"violins": + {"title": "Display violin plots?" + ,"description": "controls initial display of violin plots" + ,"default": false + ,"type": "boolean" + } + ,"missingValues": + {"title": "Missing Values" + ,"description": "an array of strings that identify missing values in both the measure and result variables" + ,"defaultArray": + ["" + ,"NA" + ,"N/A" + ] + ,"type": "array" + ,"items": + {"type": "string" + } + } + ,"visits_without_data": + {"title": "Display visits without data?" + ,"description": "controls display of visits without data for the current measure" + ,"default": false + ,"type": "boolean" + } + ,"unscheduled_visits": + {"title": "Display unscheduled visits?" + ,"description": "controls display of unscheduled visits" + ,"default": false + ,"type": "boolean" + } + ,"unscheduled_visit_pattern": + {"title": "Unscheduled Visit Pattern" + ,"description": "a regular expression that identifies unscheduled visits" + ,"default": "/unscheduled|early termination/i" + ,"type": "string" + } + ,"unscheduled_visits_values": + {"title": "Unscheduled Visit List" + ,"description": "an array of strings that identify unscheduled visits; overrides unscheduled_visit_pattern" + ,"type": "array" + ,"items": + {"type": "string" + } + } + } +} diff --git a/src/defaultSettings.js b/src/defaultSettings.js index 7b0cad8..c4bf20d 100644 --- a/src/defaultSettings.js +++ b/src/defaultSettings.js @@ -1,5 +1,6 @@ -const defaultSettings = { - //Custom settings for this template +import merge from './util/merge'; + +export const rendererSettings = { id_col: 'USUBJID', time_settings: { value_col: 'VISIT', @@ -22,10 +23,11 @@ const defaultSettings = { missingValues: ['', 'NA', 'N/A'], visits_without_data: false, unscheduled_visits: false, - unscheduled_visit_values: null, // takes precedence over unscheduled_visit_pattern - unscheduled_visit_pattern: /unscheduled|early termination/i, + unscheduled_visit_pattern: '/unscheduled|early termination/i', + unscheduled_visit_values: null // takes precedence over unscheduled_visit_pattern +}; - //Standard webcharts settings +export const webchartsSettings = { x: { column: null, // set in syncSettings() type: 'ordinal', @@ -62,6 +64,8 @@ const defaultSettings = { aspect: 3 }; +export default merge(rendererSettings, webchartsSettings); + // Replicate settings in multiple places in the settings object export function syncSettings(settings) { settings.x.column = settings.time_settings.value_col; @@ -83,6 +87,19 @@ export function syncSettings(settings) { settings.marks[0].per = [settings.color_by]; settings.margin = settings.margin || { bottom: settings.time_settings.vertical_space }; + //Convert unscheduled_visit_pattern from string to regular expression. + if ( + typeof settings.unscheduled_visit_pattern === 'string' && + settings.unscheduled_visit_pattern !== '' + ) { + const flags = settings.unscheduled_visit_pattern.replace(/.*?\/([gimy]*)$/, '$1'), + pattern = settings.unscheduled_visit_pattern.replace( + new RegExp('^/(.*?)/' + flags + '$'), + '$1' + ); + settings.unscheduled_visit_regex = new RegExp(pattern, flags); + } + return settings; } @@ -114,16 +131,15 @@ export const controlInputs = [ // Map values from settings to control inputs export function syncControlInputs(controlInputs, settings) { - const measureControl = controlInputs.filter( - controlInput => controlInput.label === 'Measure' - )[0], - groupControl = controlInputs.filter(controlInput => controlInput.label === 'Group')[0]; - //Sync measure control. + const measureControl = controlInputs.filter( + controlInput => controlInput.label === 'Measure' + )[0]; measureControl.value_col = settings.measure_col; measureControl.start = settings.start_value; //Sync group control. + const groupControl = controlInputs.filter(controlInput => controlInput.label === 'Group')[0]; groupControl.start = settings.color_by; settings.groups.filter(group => group.value_col !== 'NONE').forEach(group => { groupControl.values.push(group.value_col); @@ -148,7 +164,12 @@ export function syncControlInputs(controlInputs, settings) { }); } + //Remove unscheduled visit control if unscheduled visit pattern is unscpecified. + if (!settings.unscheduled_visit_regex) + controlInputs.splice( + controlInputs.map(controlInput => controlInput.label).indexOf('Unscheduled visits'), + 1 + ); + return controlInputs; } - -export default defaultSettings; diff --git a/src/onInit/addVariables.js b/src/onInit/addVariables.js index 9f1103e..f959867 100644 --- a/src/onInit/addVariables.js +++ b/src/onInit/addVariables.js @@ -4,8 +4,8 @@ export default function addVariables() { d.unscheduled = this.config.unscheduled_visit_values ? this.config.unscheduled_visit_values.indexOf(d[this.config.time_settings.value_col]) > -1 - : this.config.unscheduled_visit_pattern - ? this.config.unscheduled_visit_pattern.test(d[this.config.time_settings.value_col]) + : this.config.unscheduled_visit_regex + ? this.config.unscheduled_visit_regex.test(d[this.config.time_settings.value_col]) : false; }); } diff --git a/src/onInit/checkFilters.js b/src/onInit/checkFilters.js index f916f5e..39aabb4 100644 --- a/src/onInit/checkFilters.js +++ b/src/onInit/checkFilters.js @@ -6,14 +6,18 @@ export default function checkFilters() { return true; } else if (!this.raw_data[0].hasOwnProperty(input.value_col)) { console.warn( - `The [ ${input.label} ] filter has been removed because the variable does not exist.` + `The [ ${ + input.label + } ] filter has been removed because the variable does not exist.` ); } else { const levels = set(this.raw_data.map(d => d[input.value_col])).values(); if (levels.length === 1) console.warn( - `The [ ${input.label} ] filter has been removed because the variable has only one level.` + `The [ ${ + input.label + } ] filter has been removed because the variable has only one level.` ); return levels.length > 1; diff --git a/src/onInit/cleanData.js b/src/onInit/cleanData.js index 906f9dc..f106db8 100644 --- a/src/onInit/cleanData.js +++ b/src/onInit/cleanData.js @@ -11,9 +11,9 @@ export default function cleanData() { //Warn user of removed records. if (nRemoved > 0) console.warn( - `${nRemoved} missing or non-numeric result${nRemoved > 1 - ? 's have' - : ' has'} been removed.` + `${nRemoved} missing or non-numeric result${ + nRemoved > 1 ? 's have' : ' has' + } been removed.` ); this.raw_data = clean; diff --git a/src/onInit/defineVisitOrder.js b/src/onInit/defineVisitOrder.js index dd01ee1..d609261 100644 --- a/src/onInit/defineVisitOrder.js +++ b/src/onInit/defineVisitOrder.js @@ -12,9 +12,9 @@ export default function defineVisitOrder() { visits = set( this.raw_data.map( d => - `${d[this.config.time_settings.order_col]}|${d[ - this.config.time_settings.value_col - ]}` + `${d[this.config.time_settings.order_col]}|${ + d[this.config.time_settings.value_col] + }` ) ).values(); diff --git a/src/onPreprocess/setXdomain/removeUnscheduledVisits.js b/src/onPreprocess/setXdomain/removeUnscheduledVisits.js index 55cb4ed..8a2c0cb 100644 --- a/src/onPreprocess/setXdomain/removeUnscheduledVisits.js +++ b/src/onPreprocess/setXdomain/removeUnscheduledVisits.js @@ -6,9 +6,9 @@ export default function removeUnscheduledVisits() { this.config.x.domain = this.config.x.domain.filter( visit => this.config.unscheduled_visit_values.indexOf(visit) < 0 ); - else if (this.config.unscheduled_visit_pattern) + else if (this.config.unscheduled_visit_regex) this.config.x.domain = this.config.x.domain.filter( - visit => !this.config.unscheduled_visit_pattern.test(visit) + visit => !this.config.unscheduled_visit_regex.test(visit) ); } } diff --git a/src/util/merge.js b/src/util/merge.js index ba773e5..de99e73 100644 --- a/src/util/merge.js +++ b/src/util/merge.js @@ -17,7 +17,7 @@ function isObj(x) { function assignKey(to, from, key) { var val = from[key]; - if (val === undefined || val === null) { + if (val === undefined) { return; }