1 /*
  2  * Copyright (c) 2013-2017 LabKey Corporation
  3  *
  4  * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
  5  */
  6 if(!LABKEY.vis) {
  7     LABKEY.vis = {};
  8 }
  9 
 10 /**
 11  * @namespace Namespace used to encapsulate functions related to creating Generic Charts (Box, Scatter, etc.). Used in the
 12  * Generic Chart Wizard and when exporting Generic Charts as Scripts.
 13  */
 14 LABKEY.vis.GenericChartHelper = new function(){
 15 
 16     var getRenderTypes = function() {
 17         return [
 18             {
 19                 name: 'bar_chart',
 20                 title: 'Bar',
 21                 imgUrl: LABKEY.contextPath + '/visualization/images/barchart.png',
 22                 fields: [
 23                     {name: 'x', label: 'X Axis Categories', required: true, nonNumericOnly: true},
 24                     {name: 'xSub', label: 'Split Categories By', required: false, nonNumericOnly: true},
 25                     {name: 'y', label: 'Y Axis', numericOnly: true}
 26                 ],
 27                 layoutOptions: {line: true, opacity: true, axisBased: true}
 28             },
 29             {
 30                 name: 'box_plot',
 31                 title: 'Box',
 32                 imgUrl: LABKEY.contextPath + '/visualization/images/boxplot.png',
 33                 fields: [
 34                     {name: 'x', label: 'X Axis Categories'},
 35                     {name: 'y', label: 'Y Axis', required: true, numericOnly: true},
 36                     {name: 'color', label: 'Color', nonNumericOnly: true},
 37                     {name: 'shape', label: 'Shape', nonNumericOnly: true}
 38                 ],
 39                 layoutOptions: {point: true, box: true, line: true, opacity: true, axisBased: true}
 40             },
 41             {
 42                 name: 'line_plot',
 43                 title: 'Line',
 44                 imgUrl: LABKEY.contextPath + '/visualization/images/timechart.png',
 45                 fields: [
 46                     {name: 'x', label: 'X Axis', required: true, numericOrDateOnly: true},
 47                     {name: 'y', label: 'Y Axis', required: true, numericOnly: true},
 48                     {name: 'series', label: 'Series', nonNumericOnly: true}
 49                 ],
 50                 layoutOptions: {opacity: true, axisBased: true, series: true}
 51             },
 52             {
 53                 name: 'pie_chart',
 54                 title: 'Pie',
 55                 imgUrl: LABKEY.contextPath + '/visualization/images/piechart.png',
 56                 fields: [
 57                     {name: 'x', label: 'Categories', required: true, nonNumericOnly: true},
 58                     // Issue #29046  'Remove "measure" option from pie chart'
 59                     // {name: 'y', label: 'Measure', numericOnly: true}
 60                 ],
 61                 layoutOptions: {pie: true}
 62             },
 63             {
 64                 name: 'scatter_plot',
 65                 title: 'Scatter',
 66                 imgUrl: LABKEY.contextPath + '/visualization/images/scatterplot.png',
 67                 fields: [
 68                     {name: 'x', label: 'X Axis', required: true},
 69                     {name: 'y', label: 'Y Axis', required: true, numericOnly: true},
 70                     {name: 'color', label: 'Color', nonNumericOnly: true},
 71                     {name: 'shape', label: 'Shape', nonNumericOnly: true}
 72                 ],
 73                 layoutOptions: {point: true, opacity: true, axisBased: true, binnable: true}
 74             },
 75             {
 76                 name: 'time_chart',
 77                 title: 'Time',
 78                 hidden: _getStudyTimepointType() == null,
 79                 imgUrl: LABKEY.contextPath + '/visualization/images/timechart.png',
 80                 fields: [
 81                     {name: 'x', label: 'X Axis', required: true, altSelectionOnly: true, altFieldType: 'LABKEY.vis.TimeChartXAxisField'},
 82                     {name: 'y', label: 'Y Axis', required: true, numericOnly: true, allowMultiple: true}
 83                 ],
 84                 layoutOptions: {time: true, axisBased: true}
 85             }
 86         ];
 87     };
 88 
 89     /**
 90      * Gets the chart type (i.e. box or scatter).
 91      * @param {String} renderType The selected renderType, this can be SCATTER_PLOT, BOX_PLOT, or BAR_CHART. Determined
 92      * at chart creation time in the Generic Chart Wizard.
 93      * @param {String} xAxisType The datatype of the x-axis, i.e. String, Boolean, Number.
 94      * @returns {String}
 95      */
 96     var getChartType = function(renderType, xAxisType)
 97     {
 98         if (renderType === 'time_chart' || renderType === "bar_chart" || renderType === "pie_chart"
 99             || renderType === "box_plot" || renderType === "scatter_plot" || renderType === "line_plot")
100         {
101             return renderType;
102         }
103 
104         if (!xAxisType)
105         {
106             // On some charts (non-study box plots) we don't require an x-axis, instead we generate one box plot for
107             // all of the data of your y-axis. If there is no xAxisType, then we have a box plot. Scatter plots require
108             // an x-axis measure.
109             return 'box_plot';
110         }
111 
112         return (xAxisType === 'string' || xAxisType === 'boolean') ? 'box_plot' : 'scatter_plot';
113     };
114 
115     /**
116      * Generate a default label for the selected measure for the given renderType.
117      * @param renderType
118      * @param measureName - the chart type's measure name
119      * @param properties - properties for the selected column
120      */
121     var getSelectedMeasureLabel = function(renderType, measureName, properties)
122     {
123         var label = properties ? properties.label || properties.queryName : '';
124 
125         if (label != '' && measureName == 'y' && (renderType == 'bar_chart' || renderType == 'pie_chart')) {
126             if (Ext4.isDefined(properties.aggregate)) {
127                 var aggLabel = Ext4.isObject(properties.aggregate) ? properties.aggregate.name
128                         : Ext4.String.capitalize(properties.aggregate.toLowerCase());
129                 label = aggLabel + ' of ' + label;
130             }
131             else {
132                 label = 'Sum of ' + label;
133             }
134         }
135 
136         return label;
137     };
138 
139     /**
140      * Generate a plot title based on the selected measures array or object.
141      * @param renderType
142      * @param measures
143      * @returns {string}
144      */
145     var getTitleFromMeasures = function(renderType, measures)
146     {
147         var queryLabels = [];
148 
149         if (Ext4.isObject(measures))
150         {
151             if (Ext4.isArray(measures.y))
152             {
153                 Ext4.each(measures.y, function(m)
154                 {
155                     var measureQueryLabel = m.queryLabel || m.queryName;
156                     if (queryLabels.indexOf(measureQueryLabel) == -1)
157                         queryLabels.push(measureQueryLabel);
158                 });
159             }
160             else
161             {
162                 var m = measures.x || measures.y;
163                 queryLabels.push(m.queryLabel || m.queryName);
164             }
165         }
166 
167         return queryLabels.join(', ');
168     };
169 
170     /**
171      * Get the sorted set of column metadata for the given schema/query/view.
172      * @param queryConfig
173      * @param successCallback
174      * @param callbackScope
175      */
176     var getQueryColumns = function(queryConfig, successCallback, callbackScope)
177     {
178         LABKEY.Ajax.request({
179             url: LABKEY.ActionURL.buildURL('visualization', 'getGenericReportColumns.api'),
180             method: 'GET',
181             params: {
182                 schemaName: queryConfig.schemaName,
183                 queryName: queryConfig.queryName,
184                 viewName: queryConfig.viewName,
185                 dataRegionName: queryConfig.dataRegionName,
186                 includeCohort: true,
187                 includeParticipantCategory : true
188             },
189             success : function(response){
190                 var columnList = LABKEY.Utils.decode(response.responseText);
191                 _queryColumnMetadata(queryConfig, columnList, successCallback, callbackScope)
192             },
193             scope   : this
194         });
195     };
196 
197     var _queryColumnMetadata = function(queryConfig, columnList, successCallback, callbackScope)
198     {
199         LABKEY.Query.selectRows({
200             maxRows: 0, // use maxRows 0 so that we just get the query metadata
201             schemaName: queryConfig.schemaName,
202             queryName: queryConfig.queryName,
203             viewName: queryConfig.viewName,
204             parameters: queryConfig.parameters,
205             requiredVersion: 9.1,
206             columns: columnList.columns.all,
207             method: 'POST', // Issue 31744: use POST as the columns list can be very long and cause a 400 error
208             success: function(response){
209                 var columnMetadata = _updateAndSortQueryFields(queryConfig, columnList, response.metaData.fields);
210                 successCallback.call(callbackScope, columnMetadata);
211             },
212             failure : function(response) {
213                 // this likely means that the query no longer exists
214                 successCallback.call(callbackScope, columnList, []);
215             },
216             scope   : this
217         });
218     };
219 
220     var _updateAndSortQueryFields = function(queryConfig, columnList, columnMetadata)
221     {
222         var queryFields = [],
223             queryFieldKeys = [],
224             columnTypes = Ext4.isDefined(columnList.columns) ? columnList.columns : {};
225 
226         Ext4.each(columnMetadata, function(column)
227         {
228             var f = Ext4.clone(column);
229             f.schemaName = queryConfig.schemaName;
230             f.queryName = queryConfig.queryName;
231             f.isCohortColumn = false;
232             f.isSubjectGroupColumn = false;
233 
234             // issue 23224: distinguish cohort and subject group fields in the list of query columns
235             if (columnTypes['cohort'] && columnTypes['cohort'].indexOf(f.fieldKey) > -1)
236             {
237                 f.shortCaption = 'Study: ' + f.shortCaption;
238                 f.isCohortColumn = true;
239             }
240             else if (columnTypes['subjectGroup'] && columnTypes['subjectGroup'].indexOf(f.fieldKey) > -1)
241             {
242                 f.shortCaption = columnList.subject.nounSingular + ' Group: ' + f.shortCaption;
243                 f.isSubjectGroupColumn = true;
244             }
245 
246             // Issue 31672: keep track of the distinct query field keys so we don't get duplicates
247             if (f.fieldKey.toLowerCase() != 'lsid' && queryFieldKeys.indexOf(f.fieldKey) == -1) {
248                 queryFields.push(f);
249                 queryFieldKeys.push(f.fieldKey);
250             }
251         }, this);
252 
253         // Sorts fields by their shortCaption, but put subject groups/categories/cohort at the end.
254         queryFields.sort(function(a, b)
255         {
256             if (a.isSubjectGroupColumn != b.isSubjectGroupColumn)
257                 return a.isSubjectGroupColumn ? 1 : -1;
258             else if (a.isCohortColumn != b.isCohortColumn)
259                 return a.isCohortColumn ? 1 : -1;
260             else if (a.shortCaption != b.shortCaption)
261                 return a.shortCaption < b.shortCaption ? -1 : 1;
262 
263             return 0;
264         });
265 
266         return queryFields;
267     };
268 
269     /**
270      * Determine a reasonable width for the chart based on the chart type and selected measures / data.
271      * @param chartType
272      * @param measures
273      * @param measureStore
274      * @param defaultWidth
275      * @returns {int}
276      */
277     var getChartTypeBasedWidth = function(chartType, measures, measureStore, defaultWidth) {
278         var width = defaultWidth;
279 
280         if (chartType == 'bar_chart' && Ext4.isObject(measures.x)) {
281             // 15px per bar + 15px between bars + 300 for default margins
282             var xBarCount = measureStore.members(measures.x.name).length;
283             width = Math.max((xBarCount * 15 * 2) + 300, defaultWidth);
284 
285             if (Ext4.isObject(measures.xSub)) {
286                 // 15px per bar per group + 200px between groups + 600 for default margins
287                 var xSubCount = measureStore.members(measures.xSub.name).length;
288                 width = (xBarCount * xSubCount * 15) + (xSubCount * 200) + 600;
289             }
290         }
291         else if (chartType == 'box_plot' && Ext4.isObject(measures.x)) {
292             // 20px per box + 20px between boxes + 300 for default margins
293             var xBoxCount = measureStore.members(measures.x.name).length;
294             width = Math.max((xBoxCount * 20 * 2) + 300, defaultWidth);
295         }
296 
297         return width;
298     };
299 
300     /**
301      * Given the saved labels object we convert it to include all label types (main, x, and y). Each label type defaults
302      * to empty string ('').
303      * @param {Object} labels The saved labels object.
304      * @returns {Object}
305      */
306     var generateLabels = function(labels) {
307         return {
308             main: {
309                 value: labels.main ? labels.main : ''
310             },
311             subtitle: {
312                 value: labels.subtitle ? labels.subtitle : ''
313             },
314             footer: {
315                 value: labels.footer ? labels.footer : ''
316             },
317             x: {
318                 value: labels.x ? labels.x : ''
319             },
320             y: {
321                 value: labels.y ? labels.y : ''
322             }
323         };
324     };
325 
326     /**
327      * Generates an object containing {@link LABKEY.vis.Scale} objects used for the chart.
328      * @param {String} chartType The chartType from getChartType.
329      * @param {Object} measures The measures from generateMeasures.
330      * @param {Object} savedScales The scales object from the saved chart config.
331      * @param {Object} aes The aesthetic map object from genereateAes.
332      * @param {Object} measureStore The MeasureStore data using a selectRows API call.
333      * @param {Function} defaultFormatFn used to format values for tick marks.
334      * @returns {Object}
335      */
336     var generateScales = function(chartType, measures, savedScales, aes, measureStore, defaultFormatFn) {
337         var scales = {};
338         var data = Ext4.isArray(measureStore.rows) ? measureStore.rows : measureStore.records();
339         var fields = Ext4.isObject(measureStore.metaData) ? measureStore.metaData.fields : measureStore.getResponseMetadata().fields;
340         var subjectColumn = _getStudySubjectInfo().columnName;
341         var valExponentialDigits = 6;
342 
343         if (chartType === "box_plot")
344         {
345             scales.x = {
346                 scaleType: 'discrete', // Force discrete x-axis scale for box plots.
347                 sortFn: LABKEY.vis.discreteSortFn,
348                 tickLabelMax: 25
349             };
350 
351             var yMin = d3.min(data, aes.y);
352             var yMax = d3.max(data, aes.y);
353             var yPadding = ((yMax - yMin) * .1);
354             if (savedScales.y && savedScales.y.trans == "log")
355             {
356                 // When subtracting padding we have to make sure we still produce valid values for a log scale.
357                 // log([value less than 0]) = NaN.
358                 // log(0) = -Infinity.
359                 if (yMin - yPadding > 0)
360                 {
361                     yMin = yMin - yPadding;
362                 }
363             }
364             else
365             {
366                 yMin = yMin - yPadding;
367             }
368 
369             scales.y = {
370                 min: yMin,
371                 max: yMax + yPadding,
372                 scaleType: 'continuous',
373                 trans: savedScales.y ? savedScales.y.trans : 'linear'
374             };
375         }
376         else
377         {
378             var xMeasureType = getMeasureType(measures.x);
379 
380             // Force discrete x-axis scale for bar plots.
381             var useContinuousScale = chartType != 'bar_chart' && isNumericType(xMeasureType);
382 
383             if (useContinuousScale)
384             {
385                 scales.x = {
386                     scaleType: 'continuous',
387                     trans: savedScales.x ? savedScales.x.trans : 'linear'
388                 };
389             }
390             else
391             {
392                 scales.x = {
393                     scaleType: 'discrete',
394                     sortFn: LABKEY.vis.discreteSortFn,
395                     tickLabelMax: 25
396                 };
397 
398                 //bar chart x-axis subcategories support
399                 if (Ext4.isDefined(measures.xSub)) {
400                     scales.xSub = {
401                         scaleType: 'discrete',
402                         sortFn: LABKEY.vis.discreteSortFn,
403                         tickLabelMax: 25
404                     };
405                 }
406             }
407 
408             scales.y = {
409                 scaleType: 'continuous',
410                 trans: savedScales.y ? savedScales.y.trans : 'linear'
411             };
412         }
413 
414         // if we have no data, show a default y-axis domain
415         if (scales.x && data.length == 0 && scales.x.scaleType == 'continuous')
416             scales.x.domain = [0,1];
417         if (scales.y && data.length == 0)
418             scales.y.domain = [0,1];
419 
420         for (var i = 0; i < fields.length; i++) {
421             var type = fields[i].displayFieldJsonType ? fields[i].displayFieldJsonType : fields[i].type;
422             var isMeasureXMatch = measures.x && (fields[i].fieldKey == measures.x.name || fields[i].fieldKey == measures.x.fieldKey);
423             var isMeasureYMatch = measures.y && (fields[i].fieldKey == measures.y.name || fields[i].fieldKey == measures.y.fieldKey);
424             var isConvertedYMeasure = isMeasureYMatch && measures.y.converted;
425 
426             if (isNumericType(type) || isConvertedYMeasure) {
427                 if (isMeasureXMatch) {
428                     if (fields[i].extFormatFn) {
429                         scales.x.tickFormat = eval(fields[i].extFormatFn);
430                     }
431                     else if (defaultFormatFn) {
432                         scales.x.tickFormat = defaultFormatFn;
433                     }
434                 }
435 
436                 if (isMeasureYMatch) {
437                     var tickFormatFn;
438 
439                     if (fields[i].extFormatFn) {
440                         tickFormatFn = eval(fields[i].extFormatFn);
441                     }
442                     else if (defaultFormatFn) {
443                         tickFormatFn = defaultFormatFn;
444                     }
445 
446                     scales.y.tickFormat = function(value) {
447                         if (Ext4.isNumber(value) && Math.abs(Math.round(value)).toString().length >= valExponentialDigits) {
448                             return value.toExponential();
449                         }
450                         else if (Ext4.isFunction(tickFormatFn)) {
451                             return tickFormatFn(value);
452                         }
453                         return value;
454                     };
455                 }
456             }
457             else if (isMeasureXMatch && measures.x.name == subjectColumn && LABKEY.demoMode) {
458                     scales.x.tickFormat = function(){return '******'};
459             }
460         }
461 
462         if (savedScales.x && (savedScales.x.min != null || savedScales.x.max != null)) {
463             scales.x.domain = [savedScales.x.min, savedScales.x.max];
464         }
465 
466         if (Ext4.isDefined(measures.xSub) && savedScales.xSub && (savedScales.xSub.min != null || savedScales.xSub.max != null)) {
467             scales.xSub.domain = [savedScales.xSub.min, savedScales.xSub.max];
468         }
469 
470         if (savedScales.y && (savedScales.y.min != null || savedScales.y.max != null)) {
471             scales.y.domain = [savedScales.y.min, savedScales.y.max];
472         }
473 
474         return scales;
475     };
476 
477     /**
478      * Generates the aesthetic map object needed by the visualization API to render the chart. See {@link LABKEY.vis.Plot}
479      * and {@link LABKEY.vis.Layer}.
480      * @param {String} chartType The chartType from getChartType.
481      * @param {Object} measures The measures from getMeasures.
482      * @param {String} schemaName The schemaName from the saved queryConfig.
483      * @param {String} queryName The queryName from the saved queryConfig.
484      * @returns {Object}
485      */
486     var generateAes = function(chartType, measures, schemaName, queryName) {
487         var aes = {},
488             xMeasureType = getMeasureType(measures.x);
489 
490         if (chartType == "box_plot" && !measures.x)
491         {
492             aes.x = generateMeasurelessAcc(queryName);
493         }
494         else if (isNumericType(xMeasureType) || (chartType == 'scatter_plot' && measures.x.measure))
495         {
496             var xMeasureName = measures.x.converted ? measures.x.convertedName : measures.x.name;
497             aes.x = generateContinuousAcc(xMeasureName);
498         }
499         else
500         {
501             var xMeasureName = measures.x.converted ? measures.x.convertedName : measures.x.name;
502             aes.x = generateDiscreteAcc(xMeasureName, measures.x.label);
503         }
504 
505         if (measures.y)
506         {
507             var yMeasureName = measures.y.converted ? measures.y.convertedName : measures.y.name;
508             aes.y = generateContinuousAcc(yMeasureName);
509         }
510 
511         if (chartType === "scatter_plot" || chartType === "line_plot")
512         {
513             aes.hoverText = generatePointHover(measures);
514         }
515 
516         if (chartType === "box_plot")
517         {
518             if (measures.color) {
519                 aes.outlierColor = generateGroupingAcc(measures.color.name);
520             }
521 
522             if (measures.shape) {
523                 aes.outlierShape = generateGroupingAcc(measures.shape.name);
524             }
525 
526             aes.hoverText = generateBoxplotHover();
527             aes.outlierHoverText = generatePointHover(measures);
528         }
529         else if (chartType === 'bar_chart')
530         {
531             var xSubMeasureType = measures.xSub ? getMeasureType(measures.xSub) : null;
532             if (xSubMeasureType)
533             {
534                 if (isNumericType(xSubMeasureType))
535                     aes.xSub = generateContinuousAcc(measures.xSub.name);
536                 else
537                     aes.xSub = generateDiscreteAcc(measures.xSub.name, measures.xSub.label);
538             }
539         }
540 
541         // color/shape aes are not dependent on chart type. If we have a box plot with all points enabled, then we
542         // create a second layer for points. So we'll need this no matter what.
543         if (measures.color) {
544             aes.color = generateGroupingAcc(measures.color.name);
545         }
546 
547         if (measures.shape) {
548             aes.shape = generateGroupingAcc(measures.shape.name);
549         }
550 
551         // also add the color and shape for the line plot series.
552         if (measures.series) {
553             aes.color = generateGroupingAcc(measures.series.name);
554             aes.shape = generateGroupingAcc(measures.series.name);
555         }
556 
557         if (measures.pointClickFn) {
558             aes.pointClickFn = generatePointClickFn(
559                     measures,
560                     schemaName,
561                     queryName,
562                     measures.pointClickFn
563             );
564         }
565 
566         return aes;
567     };
568 
569     /**
570      * Generates a function that returns the text used for point hovers.
571      * @param {Object} measures The measures object from the saved chart config.
572      * @returns {Function}
573      */
574     var generatePointHover = function(measures)
575     {
576         return function(row) {
577             var hover = '', sep = '', distinctNames = [];
578 
579             Ext4.Object.each(measures, function(key, measure) {
580                 if (Ext4.isObject(measure) && distinctNames.indexOf(measure.name) == -1) {
581                     hover += sep + measure.label + ': ' + _getRowValue(row, measure.name);
582                     sep = ', \n';
583 
584                     distinctNames.push(measure.name);
585                 }
586             });
587 
588             return hover;
589         };
590     };
591 
592     /**
593      * Backwards compatibility for function that has been moved to LABKEY.vis.getAggregateData.
594      */
595     var generateAggregateData = function(data, dimensionName, measureName, aggregate, nullDisplayValue) {
596         return LABKEY.vis.getAggregateData(data, dimensionName, null, measureName, aggregate, nullDisplayValue, false);
597     };
598 
599     var _getRowValue = function(row, propName, valueName)
600     {
601         if (row.hasOwnProperty(propName)) {
602             // backwards compatibility for response row that is not a LABKEY.Query.Row
603             if (!(row instanceof LABKEY.Query.Row)) {
604                 return row[propName].displayValue || row[propName].value;
605             }
606 
607             var propValue = row.get(propName);
608             if (valueName != undefined && propValue.hasOwnProperty(valueName)) {
609                 return propValue[valueName];
610             }
611             else if (propValue.hasOwnProperty('displayValue')) {
612                 return propValue['displayValue'];
613             }
614             return row.getValue(propName);
615         }
616 
617         return undefined;
618     };
619 
620     /**
621      * Returns a function used to generate the hover text for box plots.
622      * @returns {Function}
623      */
624     var generateBoxplotHover = function() {
625         return function(xValue, stats) {
626             return xValue + ':\nMin: ' + stats.min + '\nMax: ' + stats.max + '\nQ1: ' + stats.Q1 + '\nQ2: ' + stats.Q2 +
627                     '\nQ3: ' + stats.Q3;
628         };
629     };
630 
631     /**
632      * Generates an accessor function that returns a discrete value from a row of data for a given measure and label.
633      * Used when an axis has a discrete measure (i.e. string).
634      * @param {String} measureName The name of the measure.
635      * @param {String} measureLabel The label of the measure.
636      * @returns {Function}
637      */
638     var generateDiscreteAcc = function(measureName, measureLabel)
639     {
640         return function(row)
641         {
642             var value = _getRowValue(row, measureName);
643             if (value === null)
644                 value = "Not in " + measureLabel;
645 
646             return value;
647         };
648     };
649 
650     /**
651      * Generates an accessor function that returns a value from a row of data for a given measure.
652      * @param {String} measureName The name of the measure.
653      * @returns {Function}
654      */
655     var generateContinuousAcc = function(measureName)
656     {
657         return function(row)
658         {
659             var value = _getRowValue(row, measureName, 'value');
660 
661             if (value !== undefined)
662             {
663                 if (Math.abs(value) === Infinity)
664                     value = null;
665 
666                 if (value === false || value === true)
667                     value = value.toString();
668 
669                 return value;
670             }
671 
672             return undefined;
673         }
674     };
675 
676     /**
677      * Generates an accesssor function for shape and color measures.
678      * @param {String} measureName The name of the measure.
679      * @returns {Function}
680      */
681     var generateGroupingAcc = function(measureName)
682     {
683         return function(row)
684         {
685             var value = null;
686             if (Ext4.isArray(row) && row.length > 0) {
687                 value = _getRowValue(row[0], measureName);
688             }
689             else {
690                 value = _getRowValue(row, measureName);
691             }
692 
693             if (value === null || value === undefined)
694                 value = "n/a";
695 
696             return value;
697         };
698     };
699 
700     /**
701      * Generates an accessor for boxplots that do not have an x-axis measure. Generally the measureName passed in is the
702      * queryName.
703      * @param {String} measureName The name of the measure. In this case it is generally the query name.
704      * @returns {Function}
705      */
706     var generateMeasurelessAcc = function(measureName) {
707         // Used for box plots that do not have an x-axis measure. Instead we just return the queryName for every row.
708         return function(row) {
709             return measureName;
710         }
711     };
712 
713     /**
714      * Generates the function to be executed when a user clicks a point.
715      * @param {Object} measures The measures from the saved chart config.
716      * @param {String} schemaName The schema name from the saved query config.
717      * @param {String} queryName The query name from the saved query config.
718      * @param {String} fnString The string value of the user-provided function to be executed when a point is clicked.
719      * @returns {Function}
720      */
721     var generatePointClickFn = function(measures, schemaName, queryName, fnString){
722         var measureInfo = {
723             schemaName: schemaName,
724             queryName: queryName
725         };
726 
727         if (measures.y)
728             measureInfo.yAxis = measures.y.name;
729         if (measures.x)
730             measureInfo.xAxis = measures.x.name;
731         Ext4.each(['color', 'shape', 'series'], function(name) {
732             if (measures[name]) {
733                 measureInfo[name + 'Name'] = measures[name].name;
734             }
735         }, this);
736 
737         // using new Function is quicker than eval(), even in IE.
738         var pointClickFn = new Function('return ' + fnString)();
739         return function(clickEvent, data){
740             pointClickFn(data, measureInfo, clickEvent);
741         };
742     };
743 
744     /**
745      * Generates the Point Geom used for scatter plots and box plots with all points visible.
746      * @param {Object} chartOptions The saved chartOptions object from the chart config.
747      * @returns {LABKEY.vis.Geom.Point}
748      */
749     var generatePointGeom = function(chartOptions){
750         return new LABKEY.vis.Geom.Point({
751             opacity: chartOptions.opacity,
752             size: chartOptions.pointSize,
753             color: '#' + chartOptions.pointFillColor,
754             position: chartOptions.position
755         });
756     };
757 
758     /**
759      * Generates the Boxplot Geom used for box plots.
760      * @param {Object} chartOptions The saved chartOptions object from the chart config.
761      * @returns {LABKEY.vis.Geom.Boxplot}
762      */
763     var generateBoxplotGeom = function(chartOptions){
764         return new LABKEY.vis.Geom.Boxplot({
765             lineWidth: chartOptions.lineWidth,
766             outlierOpacity: chartOptions.opacity,
767             outlierFill: '#' + chartOptions.pointFillColor,
768             outlierSize: chartOptions.pointSize,
769             color: '#' + chartOptions.lineColor,
770             fill: chartOptions.boxFillColor == 'none' ? chartOptions.boxFillColor : '#' + chartOptions.boxFillColor,
771             position: chartOptions.position,
772             showOutliers: chartOptions.showOutliers
773         });
774     };
775 
776     /**
777      * Generates the Barplot Geom used for bar charts.
778      * @param {Object} chartOptions The saved chartOptions object from the chart config.
779      * @returns {LABKEY.vis.Geom.BarPlot}
780      */
781     var generateBarGeom = function(chartOptions){
782         return new LABKEY.vis.Geom.BarPlot({
783             opacity: chartOptions.opacity,
784             color: '#' + chartOptions.lineColor,
785             fill: '#' + chartOptions.boxFillColor,
786             lineWidth: chartOptions.lineWidth
787         });
788     };
789 
790     /**
791      * Generates the Bin Geom used to bin a set of points.
792      * @param {Object} chartOptions The saved chartOptions object from the chart config.
793      * @returns {LABKEY.vis.Geom.Bin}
794      */
795     var generateBinGeom = function(chartOptions) {
796         var colorRange = ["#e6e6e6", "#085D90"]; //light-gray and labkey blue is default
797         if (chartOptions.binColorGroup == 'SingleColor') {
798             var color = '#' + chartOptions.binSingleColor;
799             colorRange = ["#FFFFFF", color];
800         }
801         else if (chartOptions.binColorGroup == 'Heat') {
802             colorRange = ["#fff6bc", "#e23202"];
803         }
804 
805         return new LABKEY.vis.Geom.Bin({
806             shape: chartOptions.binShape,
807             colorRange: colorRange,
808             size: chartOptions.binShape == 'square' ? 10 : 5
809         })
810     };
811 
812     /**
813      * Generates a Geom based on the chartType.
814      * @param {String} chartType The chart type from getChartType.
815      * @param {Object} chartOptions The chartOptions object from the saved chart config.
816      * @returns {LABKEY.vis.Geom}
817      */
818     var generateGeom = function(chartType, chartOptions) {
819         if (chartType == "box_plot")
820             return generateBoxplotGeom(chartOptions);
821         else if (chartType == "scatter_plot" || chartType == "line_plot")
822             return chartOptions.binned ? generateBinGeom(chartOptions) : generatePointGeom(chartOptions);
823         else if (chartType == "bar_chart")
824             return generateBarGeom(chartOptions);
825     };
826 
827     /**
828      * Generate the plot config for the given chart renderType and config options.
829      * @param renderTo
830      * @param chartConfig
831      * @param labels
832      * @param aes
833      * @param scales
834      * @param geom
835      * @param data
836      * @returns {Object}
837      */
838     var generatePlotConfig = function(renderTo, chartConfig, labels, aes, scales, geom, data)
839     {
840         var renderType = chartConfig.renderType,
841             layers = [], clipRect,
842             plotConfig = {
843                 renderTo: renderTo,
844                 rendererType: 'd3',
845                 width: chartConfig.width,
846                 height: chartConfig.height
847             };
848 
849         if (renderType == 'pie_chart')
850             return _generatePieChartConfig(plotConfig, chartConfig, labels, data);
851 
852         clipRect = (scales.x && Ext4.isArray(scales.x.domain)) || (scales.y && Ext4.isArray(scales.y.domain));
853 
854         if (renderType == 'bar_chart')
855         {
856             aes = { x: 'label', y: 'value' };
857 
858             if (Ext4.isDefined(chartConfig.measures.xSub))
859             {
860                 aes.xSub = 'subLabel';
861                 aes.color = 'label';
862             }
863 
864             if (!scales.y) {
865                 scales.y = {};
866             }
867 
868             if (!scales.y.domain) {
869                 var values = Ext4.Array.pluck(data, 'value'),
870                     min = Math.min(0, Ext4.Array.min(values)),
871                     max = Math.max(0, Ext4.Array.max(values));
872 
873                 scales.y.domain = [min, max];
874             }
875         }
876         else if (renderType == 'box_plot' && chartConfig.pointType == 'all')
877         {
878             layers.push(
879                 new LABKEY.vis.Layer({
880                     data: data,
881                     geom: LABKEY.vis.GenericChartHelper.generatePointGeom(chartConfig.geomOptions),
882                     aes: {hoverText: LABKEY.vis.GenericChartHelper.generatePointHover(chartConfig.measures)}
883                 })
884             );
885         }
886         else if (renderType == 'line_plot') {
887             var xName = chartConfig.measures.x.name,
888                 isDate = isDateType(getMeasureType(chartConfig.measures.x)),
889                     pathAes = {};
890 
891             pathAes.sortFn = function(a, b) {
892                 // No need to handle the case for a or b or a.getValue() or b.getValue() null as they are
893                 // not currently included in this plot.
894                 if (isDate){
895                     return new Date(a.getValue(xName)) - new Date(b.getValue(xName));
896                 }
897                 return a.getValue(xName) - b.getValue(xName);
898             };
899 
900             if (chartConfig.measures.series) {
901                 pathAes.pathColor = generateGroupingAcc(chartConfig.measures.series.name);
902                 pathAes.group = generateGroupingAcc(chartConfig.measures.series.name);
903             }
904 
905             layers.push(
906                 new LABKEY.vis.Layer({
907                     geom: new LABKEY.vis.Geom.Path({
908                         color: '#' + chartConfig.geomOptions.pointFillColor,
909                         size: chartConfig.geomOptions.lineWidth?chartConfig.geomOptions.lineWidth:3,
910                         opacity:chartConfig.geomOptions.opacity
911                     }),
912                     aes: pathAes
913                 })
914             );
915         }
916 
917         var margins = _getPlotMargins(renderType, aes, data, plotConfig);
918         if (Ext4.isObject(margins)) {
919             plotConfig.margins = margins;
920         }
921 
922         if (chartConfig.measures.color)
923         {
924             scales.color = {
925                 colorType: chartConfig.geomOptions.colorPaletteScale,
926                 scaleType: 'discrete'
927             }
928         }
929 
930         layers.push(
931             new LABKEY.vis.Layer({
932                 data: data,
933                 geom: geom
934             })
935         );
936 
937         plotConfig = Ext4.apply(plotConfig, {
938             clipRect: clipRect,
939             data: data,
940             labels: labels,
941             aes: aes,
942             scales: scales,
943             layers: layers
944         });
945 
946         return plotConfig;
947     };
948 
949     var _getPlotMargins = function(renderType, aes, data, plotConfig) {
950         // issue 29690: for bar and box plots, set default bottom margin based on the number of labels and the max label length
951         if (Ext4.isArray(data) && ((renderType == 'bar_chart' && !Ext4.isDefined(aes.xSub)) || renderType == 'box_plot')) {
952             var maxLen = 0;
953             Ext4.each(data, function(d) {
954                 var val = Ext4.isFunction(aes.x) ? aes.x(d) : d[aes.x];
955                 if (Ext4.isString(val)) {
956                     maxLen = Math.max(maxLen, val.length);
957                 }
958             });
959 
960             if (data.length * maxLen*5 > plotConfig.width - 150) {
961                 // min bottom margin: 50, max bottom margin: 275
962                 var bottomMargin = Math.min(Math.max(50, maxLen*5), 275);
963                 return {bottom: bottomMargin};
964             }
965         }
966 
967         return null;
968     };
969 
970     var _generatePieChartConfig = function(baseConfig, chartConfig, labels, data)
971     {
972         var hasData = data.length > 0;
973 
974         return Ext4.apply(baseConfig, {
975             data: hasData ? data : [{label: '', value: 1}],
976             header: {
977                 title: { text: labels.main.value },
978                 subtitle: { text: labels.subtitle.value },
979                 titleSubtitlePadding: 1
980             },
981             footer: {
982                 text: hasData ? labels.footer.value : 'No data to display',
983                 location: 'bottom-center'
984             },
985             labels: {
986                 mainLabel: { fontSize: 14 },
987                 percentage: {
988                     fontSize: 14,
989                     color: chartConfig.geomOptions.piePercentagesColor != null ? '#' + chartConfig.geomOptions.piePercentagesColor : undefined
990                 },
991                 outer: { pieDistance: 20 },
992                 inner: {
993                     format: hasData && chartConfig.geomOptions.showPiePercentages ? 'percentage' : 'none',
994                     hideWhenLessThanPercentage: chartConfig.geomOptions.pieHideWhenLessThanPercentage
995                 }
996             },
997             size: {
998                 pieInnerRadius: hasData ? chartConfig.geomOptions.pieInnerRadius + '%' : '100%',
999                 pieOuterRadius: hasData ? chartConfig.geomOptions.pieOuterRadius + '%' : '90%'
1000             },
1001             misc: {
1002                 gradient: {
1003                     enabled: chartConfig.geomOptions.gradientPercentage != 0,
1004                     percentage: chartConfig.geomOptions.gradientPercentage,
1005                     color: '#' + chartConfig.geomOptions.gradientColor
1006                 },
1007                 colors: {
1008                     segments: hasData ? LABKEY.vis.Scale[chartConfig.geomOptions.colorPaletteScale]() : ['#333333']
1009                 }
1010             },
1011             effects: { highlightSegmentOnMouseover: false },
1012             tooltips: { enabled: true }
1013         });
1014     };
1015 
1016     /**
1017      * Check if the MeasureStore selectRows API response has data. Return an error string if no data exists.
1018      * @param measureStore
1019      * @param includeFilterMsg true to include a message about removing filters
1020      * @returns {String}
1021      */
1022     var validateResponseHasData = function(measureStore, includeFilterMsg)
1023     {
1024         var dataArray = Ext4.isDefined(measureStore) ? measureStore.rows || measureStore.records() : [];
1025         if (dataArray.length == 0)
1026         {
1027             return 'The response returned 0 rows of data. The query may be empty or the applied filters may be too strict.'
1028                 + (includeFilterMsg ? 'Try removing or adjusting any filters if possible.' : '');
1029         }
1030 
1031         return null;
1032     };
1033 
1034     /**
1035      * Verifies that the axis measure is actually present and has data. Also checks to make sure that data can be used in a log
1036      * scale (if applicable). Returns an object with a success parameter (boolean) and a message parameter (string). If the
1037      * success parameter is false there is a critical error and the chart cannot be rendered. If success is true the chart
1038      * can be rendered. Message will contain an error or warning message if applicable. If message is not null and success
1039      * is true, there is a warning.
1040      * @param {String} chartType The chartType from getChartType.
1041      * @param {Object} chartConfig The saved chartConfig object.
1042      * @param {String} measureName The name of the axis measure property.
1043      * @param {Object} aes The aes object from generateAes.
1044      * @param {Object} scales The scales object from generateScales.
1045      * @param {Array} data The response data from selectRows.
1046      * @param {Boolean} dataConversionHappened Whether we converted any values in the measure data
1047      * @returns {Object}
1048      */
1049     var validateAxisMeasure = function(chartType, chartConfig, measureName, aes, scales, data, dataConversionHappened) {
1050         var dataIsNull = true, measureUndefined = true, invalidLogValues = false, hasZeroes = false, message = null;
1051 
1052         // no need to check measures if we have no data
1053         if (data.length == 0) {
1054             return {success: true, message: message};
1055         }
1056 
1057         for (var i = 0; i < data.length; i ++)
1058         {
1059             var value = aes[measureName](data[i]);
1060 
1061             if (value !== undefined)
1062                 measureUndefined = false;
1063 
1064             if (value !== null)
1065                 dataIsNull = false;
1066 
1067             if (value && value < 0)
1068                 invalidLogValues = true;
1069 
1070             if (value === 0 )
1071                 hasZeroes = true;
1072         }
1073 
1074         if (measureUndefined)
1075         {
1076             message = 'The measure ' + chartConfig.measures[measureName].label + ' was not found. It may have been renamed or removed.';
1077             return {success: false, message: message};
1078         }
1079 
1080         if ((chartType == 'scatter_plot' || chartType == 'line_plot' || measureName == 'y') && dataIsNull && !dataConversionHappened)
1081         {
1082             message = 'All data values for ' + chartConfig.measures[measureName].label + ' are null. Please choose a different measure.';
1083             return {success: false, message: message};
1084         }
1085 
1086         if (scales[measureName] && scales[measureName].trans == "log")
1087         {
1088             if (invalidLogValues)
1089             {
1090                 message = "Unable to use a log scale on the " + measureName + "-axis. All " + measureName
1091                         + "-axis values must be >= 0. Reverting to linear scale on " + measureName + "-axis.";
1092                 scales[measureName].trans = 'linear';
1093             }
1094             else if (hasZeroes)
1095             {
1096                 message = "Some " + measureName + "-axis values are 0. Plotting all " + measureName + "-axis values as value+1.";
1097                 var accFn = aes[measureName];
1098                 aes[measureName] = function(row){return accFn(row) + 1};
1099             }
1100         }
1101 
1102         return {success: true, message: message};
1103     };
1104 
1105     /**
1106      * Deprecated - use validateAxisMeasure
1107      */
1108     var validateXAxis = function(chartType, chartConfig, aes, scales, data){
1109         return this.validateAxisMeasure(chartType, chartConfig, 'x', aes, scales, data);
1110     };
1111     /**
1112      * Deprecated - use validateAxisMeasure
1113      */
1114     var validateYAxis = function(chartType, chartConfig, aes, scales, data){
1115         return this.validateAxisMeasure(chartType, chartConfig, 'y', aes, scales, data);
1116     };
1117 
1118     var getMeasureType = function(measure) {
1119         return measure ? (measure.normalizedType || measure.type) : null;
1120     };
1121 
1122     var isNumericType = function(type)
1123     {
1124         var t = Ext4.isString(type) ? type.toLowerCase() : null;
1125         return t == 'int' || t == 'integer' || t == 'float' || t == 'double';
1126     };
1127 
1128     var isDateType = function(type)
1129     {
1130         var t = Ext4.isString(type) ? type.toLowerCase() : null;
1131         return t == 'date';
1132     };
1133 
1134     var _getStudySubjectInfo = function()
1135     {
1136         var studyCtx = LABKEY.getModuleContext("study") || {};
1137         return Ext4.isObject(studyCtx.subject) ? studyCtx.subject : {
1138             tableName: 'Participant',
1139             columnName: 'ParticipantId',
1140             nounPlural: 'Participants',
1141             nounSingular: 'Participant'
1142         };
1143     };
1144 
1145     var _getStudyTimepointType = function()
1146     {
1147         var studyCtx = LABKEY.getModuleContext("study") || {};
1148         return Ext4.isDefined(studyCtx.timepointType) ? studyCtx.timepointType : null;
1149     };
1150 
1151     var _getMeasureRestrictions = function (chartType, measure)
1152     {
1153         var measureRestrictions = {};
1154         Ext4.each(getRenderTypes(), function (renderType)
1155         {
1156             if (renderType.name === chartType)
1157             {
1158                 Ext4.each(renderType.fields, function (field)
1159                 {
1160                     if (field.name === measure)
1161                     {
1162                         measureRestrictions.numericOnly = field.numericOnly;
1163                         measureRestrictions.nonNumericOnly = field.nonNumericOnly;
1164                         return false;
1165                     }
1166                 });
1167                 return false;
1168             }
1169         });
1170 
1171         return measureRestrictions;
1172     };
1173 
1174     /**
1175      * Converts data values passed in to the appropriate type based on measure/dimension information.
1176      * @param chartConfig Chart configuration object
1177      * @param aes Aesthetic mapping functions for each measure/axis
1178      * @param renderType The type of plot or chart (e.g. scatter_plot, bar_chart)
1179      * @param data The response data from SelectRows
1180      * @returns {{processed: {}, warningMessage: *}}
1181      */
1182     var doValueConversion = function(chartConfig, aes, renderType, data)
1183     {
1184         var measuresForProcessing = {}, measureRestrictions = {}, configMeasure;
1185         for (var measureName in chartConfig.measures) {
1186             if (chartConfig.measures.hasOwnProperty(measureName) && Ext4.isObject(chartConfig.measures[measureName])) {
1187                 configMeasure = chartConfig.measures[measureName];
1188                 Ext4.apply(measureRestrictions, _getMeasureRestrictions(renderType, measureName));
1189 
1190                 var isGroupingMeasure = measureName === 'color' || measureName === 'shape' || measureName === 'series';
1191                 var isXAxis = measureName === 'x' || measureName === 'xSub';
1192                 var isScatterOrLine = renderType === 'scatter_plot' || renderType === 'line_plot';
1193                 var isBarYCount = renderType === 'bar_chart' && configMeasure.aggregate && (configMeasure.aggregate === 'COUNT' || configMeasure.aggregate.value === 'COUNT');
1194 
1195                 if (configMeasure.measure && !isGroupingMeasure && !isBarYCount
1196                         && ((!isXAxis && measureRestrictions.numericOnly ) || isScatterOrLine) && !isNumericType(configMeasure.type)) {
1197                     measuresForProcessing[measureName] = {};
1198                     measuresForProcessing[measureName].name = configMeasure.name;
1199                     measuresForProcessing[measureName].convertedName = configMeasure.name + "_converted";
1200                     measuresForProcessing[measureName].label = configMeasure.label;
1201                     configMeasure.normalizedType = 'float';
1202                     configMeasure.type = 'float';
1203                 }
1204             }
1205         }
1206 
1207         var response = {processed: {}};
1208         if (!Ext4.Object.isEmpty(measuresForProcessing)) {
1209             response = _processMeasureData(data, aes, measuresForProcessing);
1210         }
1211 
1212         //generate error message for dropped values
1213         var warningMessage = '';
1214         for (var measure in response.droppedValues) {
1215             if (response.droppedValues.hasOwnProperty(measure) && response.droppedValues[measure].numDropped) {
1216                 warningMessage += " The "
1217                         + measure + "-axis measure '"
1218                         + response.droppedValues[measure].label + "' had "
1219                         + response.droppedValues[measure].numDropped +
1220                         " value(s) that could not be converted to a number and are not included in the plot.";
1221             }
1222         }
1223 
1224         return {processed: response.processed, warningMessage: warningMessage};
1225     };
1226 
1227     /**
1228      * Does the explicit type conversion for each measure deemed suitable to convert. Currently we only
1229      * attempt to convert strings to numbers for measures.
1230      * @param rows Data from SelectRows
1231      * @param aes Aesthetic mapping function for the measure/dimensions
1232      * @param measuresForProcessing The measures to be converted, if any
1233      * @returns {{droppedValues: {}, processed: {}}}
1234      */
1235     var _processMeasureData = function(rows, aes, measuresForProcessing) {
1236         var droppedValues = {}, processedMeasures = {}, dataIsNull;
1237         rows.forEach(function(row) {
1238             //convert measures if applicable
1239             if (!Ext4.Object.isEmpty(measuresForProcessing)) {
1240                 for (var measure in measuresForProcessing) {
1241                     if (measuresForProcessing.hasOwnProperty(measure)) {
1242                         dataIsNull = true;
1243                         if (!droppedValues[measure]) {
1244                             droppedValues[measure] = {};
1245                             droppedValues[measure].label = measuresForProcessing[measure].label;
1246                             droppedValues[measure].numDropped = 0;
1247                         }
1248 
1249                         if (aes.hasOwnProperty(measure)) {
1250                             var value = aes[measure](row);
1251                             if (value !== null) {
1252                                 dataIsNull = false;
1253                             }
1254                             row[measuresForProcessing[measure].convertedName] = {value: null};
1255                             if (typeof value !== 'number' && value !== null) {
1256 
1257                                 //only try to convert strings to numbers
1258                                 if (typeof value === 'string') {
1259                                     value = value.trim();
1260                                 }
1261                                 else {
1262                                     //dates, objects, booleans etc. to be assigned value: NULL
1263                                     value = '';
1264                                 }
1265 
1266                                 var n = Number(value);
1267                                 // empty strings convert to 0, which we must explicitly deny
1268                                 if (value === '' || isNaN(n)) {
1269                                     droppedValues[measure].numDropped++;
1270                                 }
1271                                 else {
1272                                     row[measuresForProcessing[measure].convertedName].value = n;
1273                                 }
1274                             }
1275                         }
1276 
1277                         if (!processedMeasures[measure]) {
1278                             processedMeasures[measure] = {
1279                                 converted: false,
1280                                 convertedName: measuresForProcessing[measure].convertedName,
1281                                 type: 'float',
1282                                 normalizedType: 'float'
1283                             }
1284                         }
1285 
1286                         processedMeasures[measure].converted = processedMeasures[measure].converted || !dataIsNull;
1287                     }
1288                 }
1289             }
1290         });
1291 
1292         return {droppedValues: droppedValues, processed: processedMeasures};
1293     };
1294 
1295     /**
1296      * removes all traces of String -> Numeric Conversion from the given chart config
1297      * @param chartConfig
1298      * @returns {updated ChartConfig}
1299      */
1300     var removeNumericConversionConfig = function(chartConfig) {
1301         if (chartConfig && chartConfig.measures) {
1302             for (var measureName in chartConfig.measures) {
1303                 if (chartConfig.measures.hasOwnProperty(measureName)) {
1304                     var measure = chartConfig.measures[measureName];
1305                     if (measure && measure.converted && measure.convertedName) {
1306                         measure.converted = null;
1307                         measure.convertedName = null;
1308                         if (LABKEY.vis.GenericChartHelper.isNumericType(measure.type)) {
1309                             measure.type = 'string';
1310                             measure.normalizedType = 'string';
1311                         }
1312                     }
1313                 }
1314             }
1315         }
1316 
1317         return chartConfig;
1318     };
1319 
1320     return {
1321         // NOTE: the @function below is needed or JSDoc will not include the documentation for loadVisDependencies. Don't
1322         // ask me why, I do not know.
1323         /**
1324          * @function
1325          */
1326         getRenderTypes: getRenderTypes,
1327         getChartType: getChartType,
1328         getSelectedMeasureLabel: getSelectedMeasureLabel,
1329         getTitleFromMeasures: getTitleFromMeasures,
1330         getMeasureType: getMeasureType,
1331         getQueryColumns : getQueryColumns,
1332         getChartTypeBasedWidth : getChartTypeBasedWidth,
1333         isNumericType: isNumericType,
1334         generateLabels: generateLabels,
1335         generateScales: generateScales,
1336         generateAes: generateAes,
1337         doValueConversion: doValueConversion,
1338         removeNumericConversionConfig: removeNumericConversionConfig,
1339         generateAggregateData: generateAggregateData,
1340         generatePointHover: generatePointHover,
1341         generateBoxplotHover: generateBoxplotHover,
1342         generateDiscreteAcc: generateDiscreteAcc,
1343         generateContinuousAcc: generateContinuousAcc,
1344         generateGroupingAcc: generateGroupingAcc,
1345         generatePointClickFn: generatePointClickFn,
1346         generateGeom: generateGeom,
1347         generateBoxplotGeom: generateBoxplotGeom,
1348         generatePointGeom: generatePointGeom,
1349         generatePlotConfig: generatePlotConfig,
1350         validateResponseHasData: validateResponseHasData,
1351         validateAxisMeasure: validateAxisMeasure,
1352         validateXAxis: validateXAxis,
1353         validateYAxis: validateYAxis,
1354         /**
1355          * Loads all of the required dependencies for a Generic Chart.
1356          * @param {Function} callback The callback to be executed when all of the visualization dependencies have been loaded.
1357          * @param {Object} scope The scope to be used when executing the callback.
1358          */
1359         loadVisDependencies: LABKEY.requiresVisualization
1360     };
1361 };