1 /* 2 * Copyright (c) 2012-2016 LabKey Corporation 3 * 4 * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 5 */ 6 7 // Contains helpers that aren't specific to plot, layer, geom, etc. and are used throughout the API. 8 9 if(!LABKEY){ 10 var LABKEY = {}; 11 } 12 13 if(!LABKEY.vis){ 14 /** 15 * @namespace The namespace for the internal LabKey visualization library. Contains classes within 16 * {@link LABKEY.vis.Plot}, {@link LABKEY.vis.Layer}, and {@link LABKEY.vis.Geom}. 17 */ 18 LABKEY.vis = {}; 19 } 20 21 LABKEY.vis.makeLine = function(x1, y1, x2, y2){ 22 //Generates a path between two coordinates. 23 return "M " + x1 + " " + y1 + " L " + x2 + " " + y2; 24 }; 25 26 LABKEY.vis.makePath = function(data, xAccessor, yAccessor){ 27 var pathString = ''; 28 29 for(var i = 0; i < data.length; i++){ 30 var x = xAccessor(data[i]); 31 var y = yAccessor(data[i]); 32 if(!LABKEY.vis.isValid(x) || !LABKEY.vis.isValid(y)){ 33 continue; 34 } 35 36 if(pathString == ''){ 37 pathString = pathString + 'M' + x + ' ' + y; 38 } else { 39 pathString = pathString + ' L' + x + ' ' + y; 40 } 41 } 42 return pathString; 43 }; 44 45 LABKEY.vis.createGetter = function(aes){ 46 if(typeof aes.value === 'function'){ 47 aes.getValue = aes.value; 48 } else { 49 aes.getValue = function(row){ 50 if(row instanceof Array) { 51 /* 52 * For Path geoms we pass in the entire array of values for the path to the aesthetic. So if the user 53 * provides only a string for an Aes value we'll assume they want the first object in the path array to 54 * determing the value. 55 */ 56 if(row.length > 0) { 57 row = row[0]; 58 } else { 59 return null; 60 } 61 } 62 return row[aes.value]; 63 }; 64 } 65 }; 66 67 LABKEY.vis.convertAes = function(aes){ 68 var newAes= {}; 69 for(var aesthetic in aes){ 70 var newAesName = (aesthetic == 'y') ? 'yLeft' : aesthetic; 71 newAes[newAesName] = {}; 72 newAes[newAesName].value = aes[aesthetic]; 73 } 74 return newAes; 75 }; 76 77 LABKEY.vis.mergeAes = function(oldAes, newAes) { 78 newAes = LABKEY.vis.convertAes(newAes); 79 for(var attr in newAes) { 80 if(newAes.hasOwnProperty(attr)) { 81 if (newAes[attr].value != null) { 82 LABKEY.vis.createGetter(newAes[attr]); 83 oldAes[attr] = newAes[attr]; 84 } else { 85 delete oldAes[attr]; 86 } 87 } 88 } 89 }; 90 91 /** 92 * Groups data by the groupAccessor, and subgroupAccessor if provided, passed in. 93 * Ex: A set of rows with participantIds in them, would return an object that has one attribute 94 * per participant id. Each attribute will be an array of all of the rows the participant is in. 95 * @param data Array of data (likely result of selectRows API call) 96 * @param groupAccessor Function defining how to access group data from array rows 97 * @param subgroupAccessor Function defining how to access subgroup data from array rows 98 * @returns {Object} Map of groups, and subgroups, to arrays of data for each 99 */ 100 LABKEY.vis.groupData = function(data, groupAccessor, subgroupAccessor) 101 { 102 var groupedData = {}, 103 hasSubgroupAcc = subgroupAccessor != undefined && subgroupAccessor != null; 104 105 for (var i = 0; i < data.length; i++) 106 { 107 var value = groupAccessor(data[i]); 108 if (!groupedData[value]) 109 groupedData[value] = hasSubgroupAcc ? {} : []; 110 111 if (hasSubgroupAcc) 112 { 113 var subvalue = subgroupAccessor(data[i]); 114 if (!groupedData[value][subvalue]) 115 groupedData[value][subvalue] = []; 116 117 groupedData[value][subvalue].push(data[i]); 118 } 119 else 120 { 121 groupedData[value].push(data[i]); 122 } 123 } 124 return groupedData; 125 }; 126 127 /** 128 * Groups data by the groupAccessor, and subgroupAccessor if provided, passed in and returns the number 129 * of occurrences for that group/subgroup. Most commonly used for processing data for a bar plot. 130 * @param data 131 * @param groupAccessor 132 * @param subgroupAccessor 133 * @param propNameMap 134 * @returns {Array} 135 */ 136 LABKEY.vis.groupCountData = function(data, groupAccessor, subgroupAccessor, propNameMap) 137 { 138 var counts = [], total = 0, 139 nameProp = propNameMap && propNameMap.name ? propNameMap.name : 'name', 140 subnameProp = propNameMap && propNameMap.subname ? propNameMap.subname : 'subname', 141 countProp = propNameMap && propNameMap.count ? propNameMap.count : 'count', 142 totalProp = propNameMap && propNameMap.total ? propNameMap.total : 'total', 143 hasSubgroupAcc = subgroupAccessor != undefined && subgroupAccessor != null, 144 groupedData = LABKEY.vis.groupData(data, groupAccessor, subgroupAccessor); 145 146 for (var groupName in groupedData) 147 { 148 if (groupedData.hasOwnProperty(groupName)) 149 { 150 if (hasSubgroupAcc) 151 { 152 for (var subgroupName in groupedData[groupName]) 153 { 154 if (groupedData[groupName].hasOwnProperty(subgroupName)) 155 { 156 var row = {rawData: groupedData[groupName][subgroupName]}, 157 count = row['rawData'].length; 158 total += count; 159 160 row[nameProp] = groupName; 161 row[subnameProp] = subgroupName; 162 row[countProp] = count; 163 row[totalProp] = total; 164 counts.push(row); 165 } 166 } 167 } 168 else 169 { 170 var row = {rawData: groupedData[groupName]}, 171 count = row['rawData'].length; 172 total += count; 173 174 row[nameProp] = groupName; 175 row[countProp] = count; 176 row[totalProp] = total; 177 counts.push(row); 178 } 179 } 180 } 181 182 return counts; 183 }; 184 185 /** 186 * Generate an array of aggregate values for the given groups/subgroups in the data array. 187 * @param {Array} data The response data from selectRows. 188 * @param {String} dimensionName The grouping variable to get distinct members from. 189 * @param {String} subDimensionName The subgrouping variable to get distinct members from 190 * @param {String} measureName The variable to calculate aggregate values over. Nullable. 191 * @param {String} aggregate MIN/MAX/SUM/COUNT/etc. Defaults to COUNT. 192 * @param {String} nullDisplayValue The display value to use for null dimension values. Defaults to 'null'. 193 * @param {Boolean} includeTotal Whether or not to include the cumulative totals. Defaults to false. 194 * @returns {Array} An array of results for each group/subgroup/aggregate 195 */ 196 LABKEY.vis.getAggregateData = function(data, dimensionName, subDimensionName, measureName, aggregate, nullDisplayValue, includeTotal) 197 { 198 var results = [], subgroupAccessor, 199 groupAccessor = typeof dimensionName === 'function' ? dimensionName : function(row){ return LABKEY.vis.getValue(row[dimensionName]);}, 200 hasSubgroup = subDimensionName != undefined && subDimensionName != null, 201 hasMeasure = measureName != undefined && measureName != null, 202 measureAccessor = hasMeasure ? function(row){ return LABKEY.vis.getValue(row[measureName]); } : null; 203 204 if (hasSubgroup) { 205 if (typeof subDimensionName === 'function') { 206 subgroupAccessor = subDimensionName; 207 } else { 208 subgroupAccessor = function (row) { return LABKEY.vis.getValue(row[subDimensionName]); } 209 } 210 } 211 212 var groupData = LABKEY.vis.groupCountData(data, groupAccessor, subgroupAccessor); 213 214 for (var i = 0; i < groupData.length; i++) 215 { 216 var row = {label: groupData[i]['name']}; 217 if (row['label'] == null || row['label'] == 'null') 218 row['label'] = nullDisplayValue || 'null'; 219 220 if (hasSubgroup) 221 { 222 row['subLabel'] = groupData[i]['subname']; 223 if (row['subLabel'] == null || row['subLabel'] == 'null') 224 row['subLabel'] = nullDisplayValue || 'null'; 225 } 226 if (includeTotal) { 227 row['total'] = groupData[i]['total']; 228 } 229 230 var values = measureAccessor != undefined && measureAccessor != null 231 ? LABKEY.vis.Stat.sortNumericAscending(groupData[i].rawData, measureAccessor) 232 : null; 233 234 if (aggregate == undefined || aggregate == null || aggregate == 'COUNT') 235 { 236 row['value'] = values != null ? values.length : groupData[i]['count']; 237 } 238 else if (typeof LABKEY.vis.Stat[aggregate] == 'function') 239 { 240 try { 241 row.value = LABKEY.vis.Stat[aggregate](values); 242 } catch (e) { 243 row.value = null; 244 } 245 } 246 else 247 { 248 throw 'Aggregate ' + aggregate + ' is not yet supported.'; 249 } 250 251 results.push(row); 252 } 253 254 return results; 255 }; 256 257 LABKEY.vis.getColumnAlias = function(aliasArray, measureInfo) { 258 /* 259 Lookup the column alias (from the getData response) by the specified measure information 260 aliasArray: columnAlias array from the getData API response 261 measureInfo: 1. a string with the name of the column to lookup 262 2. an object with a measure alias OR measureName 263 3. an object with both measureName AND pivotValue 264 */ 265 if (!aliasArray) 266 aliasArray = []; 267 268 if (typeof measureInfo != "object") 269 measureInfo = {measureName: measureInfo}; 270 for (var i = 0; i < aliasArray.length; i++) 271 { 272 var arrVal = aliasArray[i]; 273 274 if (measureInfo.measureName && measureInfo.pivotValue) 275 { 276 if (arrVal.measureName == measureInfo.measureName && arrVal.pivotValue == measureInfo.pivotValue) 277 return arrVal.columnName; 278 } 279 else if (measureInfo.alias) 280 { 281 if (arrVal.alias == measureInfo.alias) 282 return arrVal.columnName; 283 } 284 else if (measureInfo.measureName && arrVal.measureName == measureInfo.measureName) 285 return arrVal.columnName; 286 } 287 return null; 288 }; 289 290 LABKEY.vis.isValid = function(value) { 291 return !(value == undefined || value == null || (typeof value == "number" && !isFinite(value))); 292 }; 293 294 LABKEY.vis.arrayObjectIndexOf = function(myArray, searchTerm, property) { 295 for (var i = 0; i < myArray.length; i++) { 296 if (myArray[i][property] === searchTerm) return i; 297 } 298 return -1; 299 }; 300 301 LABKEY.vis.discreteSortFn = function(a,b) { 302 // Issue 23015: sort categorical x-axis alphabetically with special case for "Not in X" and "[Blank]" 303 var aIsEmptyCategory = a && (a.indexOf("Not in ") == 0 || a == '[Blank]'), 304 bIsEmptyCategory = b && (b.indexOf("Not in ") == 0 || b == '[Blank]'); 305 306 if (aIsEmptyCategory) 307 return 1; 308 else if (bIsEmptyCategory) 309 return -1; 310 else if (a != b) 311 return LABKEY.vis.naturalSortFn(a,b); 312 313 return 0; 314 }; 315 316 LABKEY.vis.naturalSortFn = function(aso, bso) { 317 // http://stackoverflow.com/questions/19247495/alphanumeric-sorting-an-array-in-javascript 318 var a, b, a1, b1, i= 0, n, L, 319 rx=/(\.\d+)|(\d+(\.\d+)?)|([^\d.]+)|(\.\D+)|(\.$)/g; 320 if (aso === bso) return 0; 321 a = aso.toLowerCase().match(rx); 322 b = bso.toLowerCase().match(rx); 323 324 L = a.length; 325 while (i < L) { 326 if (!b[i]) return 1; 327 a1 = a[i]; b1 = b[i++]; 328 if (a1 !== b1) { 329 n = a1 - b1; 330 if (!isNaN(n)) return n; 331 return a1 > b1 ? 1 : -1; 332 } 333 } 334 return b[i] ? -1 : 0; 335 }; 336 337 LABKEY.vis.getValue = function(obj) { 338 if (typeof obj == 'object') 339 return obj.hasOwnProperty('displayValue') ? obj.displayValue : obj.value; 340 341 return obj; 342 }; 343