|  | 
| 11 | 11 | 
 | 
| 12 | 12 | var isNumeric = require('fast-isnumeric'); | 
| 13 | 13 | var isArrayOrTypedArray = require('./is_array').isArrayOrTypedArray; | 
| 14 |  | -var isPlainObject = require('./is_plain_object'); | 
| 15 |  | -var containerArrayMatch = require('../plot_api/container_array_match'); | 
| 16 | 14 | 
 | 
| 17 | 15 | /** | 
| 18 | 16 |  * convert a string s (such as 'xaxis.range[0]') | 
| @@ -115,44 +113,21 @@ function npGet(cont, parts) { | 
| 115 | 113 | } | 
| 116 | 114 | 
 | 
| 117 | 115 | /* | 
| 118 |  | - * Can this value be deleted? We can delete any empty object (null, undefined, [], {}) | 
| 119 |  | - * EXCEPT empty data arrays, {} inside an array, or anything INSIDE an *args* array. | 
|  | 116 | + * Can this value be deleted? We can delete `undefined`, and `null` except INSIDE an | 
|  | 117 | + * *args* array. | 
| 120 | 118 |  * | 
| 121 |  | - * Info arrays can be safely deleted, but not deleting them has no ill effects other | 
| 122 |  | - * than leaving a trace or layout object with some cruft in it. | 
|  | 119 | + * Previously we also deleted some `{}` and `[]`, in order to try and make set/unset | 
|  | 120 | + * a net noop; but this causes far more complication than it's worth, and still had | 
|  | 121 | + * lots of exceptions. See https://github.com/plotly/plotly.js/issues/1410 | 
| 123 | 122 |  * | 
| 124 |  | - * Deleting data arrays can change the meaning of the object, as `[]` means there is | 
| 125 |  | - * data for this attribute, it's just empty right now while `undefined` means the data | 
| 126 |  | - * should be filled in with defaults to match other data arrays. | 
| 127 |  | - * | 
| 128 |  | - * `{}` inside an array means "the default object" which is clearly different from | 
| 129 |  | - * popping it off the end of the array, or setting it `undefined` inside the array. | 
| 130 |  | - * | 
| 131 |  | - * *args* arrays get passed directly to API methods and we should respect precisely | 
| 132 |  | - * what the user has put there - although if the whole *args* array is empty it's fine | 
| 133 |  | - * to delete that. | 
| 134 |  | - * | 
| 135 |  | - * So we do some simple tests here to find known non-data arrays but don't worry too | 
| 136 |  | - * much about not deleting some arrays that would actually be safe to delete. | 
|  | 123 | + * *args* arrays get passed directly to API methods and we should respect null if | 
|  | 124 | + * the user put it there, but otherwise null is deleted as we use it as code | 
|  | 125 | + * in restyle/relayout/update for "delete this value" whereas undefined means | 
|  | 126 | + * "ignore this edit" | 
| 137 | 127 |  */ | 
| 138 |  | -var INFO_PATTERNS = /(^|\.)((domain|range)(\.[xy])?|args|parallels)$/; | 
| 139 | 128 | var ARGS_PATTERN = /(^|\.)args\[/; | 
| 140 | 129 | function isDeletable(val, propStr) { | 
| 141 |  | -    if(!emptyObj(val) || | 
| 142 |  | -        (isPlainObject(val) && propStr.charAt(propStr.length - 1) === ']') || | 
| 143 |  | -        (propStr.match(ARGS_PATTERN) && val !== undefined) | 
| 144 |  | -    ) { | 
| 145 |  | -        return false; | 
| 146 |  | -    } | 
| 147 |  | -    if(!isArrayOrTypedArray(val)) return true; | 
| 148 |  | - | 
| 149 |  | -    if(propStr.match(INFO_PATTERNS)) return true; | 
| 150 |  | - | 
| 151 |  | -    var match = containerArrayMatch(propStr); | 
| 152 |  | -    // if propStr matches the container array itself, index is an empty string | 
| 153 |  | -    // otherwise we've matched something inside the container array, which may | 
| 154 |  | -    // still be a data array. | 
| 155 |  | -    return match && (match.index === ''); | 
|  | 130 | +    return (val === undefined) || (val === null && !propStr.match(ARGS_PATTERN)); | 
| 156 | 131 | } | 
| 157 | 132 | 
 | 
| 158 | 133 | function npSet(cont, parts, propStr) { | 
| @@ -194,8 +169,18 @@ function npSet(cont, parts, propStr) { | 
| 194 | 169 |         } | 
| 195 | 170 | 
 | 
| 196 | 171 |         if(toDelete) { | 
| 197 |  | -            if(i === parts.length - 1) delete curCont[parts[i]]; | 
| 198 |  | -            pruneContainers(containerLevels); | 
|  | 172 | +            if(i === parts.length - 1) { | 
|  | 173 | +                delete curCont[parts[i]]; | 
|  | 174 | + | 
|  | 175 | +                // The one bit of pruning we still do: drop `undefined` from the end of arrays. | 
|  | 176 | +                // In case someone has already unset previous items, continue until we hit a | 
|  | 177 | +                // non-undefined value. | 
|  | 178 | +                if(Array.isArray(curCont) && +parts[i] === curCont.length - 1) { | 
|  | 179 | +                    while(curCont.length && curCont[curCont.length - 1] === undefined) { | 
|  | 180 | +                        curCont.pop(); | 
|  | 181 | +                    } | 
|  | 182 | +                } | 
|  | 183 | +            } | 
| 199 | 184 |         } | 
| 200 | 185 |         else curCont[parts[i]] = val; | 
| 201 | 186 |     }; | 
| @@ -249,48 +234,6 @@ function checkNewContainer(container, part, nextPart, toDelete) { | 
| 249 | 234 |     return true; | 
| 250 | 235 | } | 
| 251 | 236 | 
 | 
| 252 |  | -function pruneContainers(containerLevels) { | 
| 253 |  | -    var i, | 
| 254 |  | -        j, | 
| 255 |  | -        curCont, | 
| 256 |  | -        propPart, | 
| 257 |  | -        keys, | 
| 258 |  | -        remainingKeys; | 
| 259 |  | -    for(i = containerLevels.length - 1; i >= 0; i--) { | 
| 260 |  | -        curCont = containerLevels[i][0]; | 
| 261 |  | -        propPart = containerLevels[i][1]; | 
| 262 |  | - | 
| 263 |  | -        remainingKeys = false; | 
| 264 |  | -        if(isArrayOrTypedArray(curCont)) { | 
| 265 |  | -            for(j = curCont.length - 1; j >= 0; j--) { | 
| 266 |  | -                if(isDeletable(curCont[j], joinPropStr(propPart, j))) { | 
| 267 |  | -                    if(remainingKeys) curCont[j] = undefined; | 
| 268 |  | -                    else curCont.pop(); | 
| 269 |  | -                } | 
| 270 |  | -                else remainingKeys = true; | 
| 271 |  | -            } | 
| 272 |  | -        } | 
| 273 |  | -        else if(typeof curCont === 'object' && curCont !== null) { | 
| 274 |  | -            keys = Object.keys(curCont); | 
| 275 |  | -            remainingKeys = false; | 
| 276 |  | -            for(j = keys.length - 1; j >= 0; j--) { | 
| 277 |  | -                if(isDeletable(curCont[keys[j]], joinPropStr(propPart, keys[j]))) { | 
| 278 |  | -                    delete curCont[keys[j]]; | 
| 279 |  | -                } | 
| 280 |  | -                else remainingKeys = true; | 
| 281 |  | -            } | 
| 282 |  | -        } | 
| 283 |  | -        if(remainingKeys) return; | 
| 284 |  | -    } | 
| 285 |  | -} | 
| 286 |  | - | 
| 287 |  | -function emptyObj(obj) { | 
| 288 |  | -    if(obj === undefined || obj === null) return true; | 
| 289 |  | -    if(typeof obj !== 'object') return false; // any plain value | 
| 290 |  | -    if(isArrayOrTypedArray(obj)) return !obj.length; // [] | 
| 291 |  | -    return !Object.keys(obj).length; // {} | 
| 292 |  | -} | 
| 293 |  | - | 
| 294 | 237 | function badContainer(container, propStr, propParts) { | 
| 295 | 238 |     return { | 
| 296 | 239 |         set: function() { throw 'bad container'; }, | 
|  | 
0 commit comments