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