1 /*
  2  * Copyright (c) 2013-2016 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 Categories', required: true, nonNumericOnly: true},
 24                     {name: 'y', label: 'Y Axis', numericOnly: true}
 25                 ],
 26                 layoutOptions: {line: true, opacity: true, axisBased: true}
 27             },
 28             {
 29                 name: 'box_plot',
 30                 title: 'Box',
 31                 imgUrl: LABKEY.contextPath + '/visualization/images/boxplot.png',
 32                 fields: [
 33                     {name: 'x', label: 'X Axis Grouping'},
 34                     {name: 'y', label: 'Y Axis', required: true, numericOnly: true},
 35                     {name: 'color', label: 'Color', nonNumericOnly: true},
 36                     {name: 'shape', label: 'Shape', nonNumericOnly: true}
 37                 ],
 38                 layoutOptions: {point: true, box: true, line: true, opacity: true, axisBased: true}
 39             },
 40             {
 41                 name: 'pie_chart',
 42                 title: 'Pie',
 43                 imgUrl: LABKEY.contextPath + '/visualization/images/piechart.png',
 44                 fields: [
 45                     {name: 'x', label: 'Categories', required: true, nonNumericOnly: true},
 46                     {name: 'y', label: 'Measure', numericOnly: true}
 47                 ],
 48                 layoutOptions: {pie: true}
 49             },
 50             {
 51                 name: 'scatter_plot',
 52                 title: 'Scatter',
 53                 imgUrl: LABKEY.contextPath + '/visualization/images/scatterplot.png',
 54                 fields: [
 55                     {name: 'x', label: 'X Axis', required: true},
 56                     {name: 'y', label: 'Y Axis', required: true, numericOnly: true},
 57                     {name: 'color', label: 'Color', nonNumericOnly: true},
 58                     {name: 'shape', label: 'Shape', nonNumericOnly: true}
 59                 ],
 60                 layoutOptions: {point: true, box: false, line: false, opacity: true, axisBased: true, binnable: true}
 61             }
 62         ];
 63     };
 64 
 65     /**
 66      * Gets the chart type (i.e. box or scatter).
 67      * @param {String} renderType The selected renderType, this can be SCATTER_PLOT, BOX_PLOT, or BAR_CHART. Determined
 68      * at chart creation time in the Generic Chart Wizard.
 69      * @param {String} xAxisType The datatype of the x-axis, i.e. String, Boolean, Number.
 70      * @returns {String}
 71      */
 72     var getChartType = function(renderType, xAxisType) {
 73         if (renderType === "bar_chart" || renderType === "pie_chart"
 74             || renderType === "box_plot" || renderType === "scatter_plot") {
 75             return renderType;
 76         }
 77 
 78         if(!xAxisType) {
 79             // On some charts (non-study box plots) we don't require an x-axis, instead we generate one box plot for
 80             // all of the data of your y-axis. If there is no xAxisType, then we have a box plot. Scatter plots require
 81             // an x-axis measure.
 82             return 'box_plot';
 83         }
 84 
 85 
 86 
 87         return (xAxisType === 'string' || xAxisType === 'boolean') ? 'box_plot' : 'scatter_plot';
 88     };
 89 
 90     /**
 91      * Generate a default label for the selected measure for the given renderType.
 92      * @param renderType
 93      * @param measureName - the chart type's measure name
 94      * @param properties - properties for the selected column
 95      */
 96     var getDefaultLabel = function(renderType, measureName, properties)
 97     {
 98         var label = properties ? properties.label || properties.queryName : '';
 99 
100         if ((renderType == 'bar_chart' || renderType == 'pie_chart') && measureName == 'y')
101             label = 'Sum of ' + label;
102 
103         return label;
104     };
105 
106     /**
107      * Given the saved labels object we convert it to include all label types (main, x, and y). Each label type defaults
108      * to empty string ('').
109      * @param {Object} labels The saved labels object.
110      * @returns {Object}
111      */
112     var generateLabels = function(labels) {
113         return {
114             main: {
115                 value: labels.main ? labels.main : ''
116             },
117             subtitle: {
118                 value: labels.subtitle ? labels.subtitle : ''
119             },
120             footer: {
121                 value: labels.footer ? labels.footer : ''
122             },
123             x: {
124                 value: labels.x ? labels.x : ''
125             },
126             y: {
127                 value: labels.y ? labels.y : ''
128             }
129         };
130     };
131 
132     /**
133      * Generates an object containing {@link LABKEY.vis.Scale} objects used for the chart.
134      * @param {String} chartType The chartType from getChartType.
135      * @param {Object} measures The measures from generateMeasures.
136      * @param {Object} savedScales The scales object from the saved chart config.
137      * @param {Object} aes The aesthetic map object from genereateAes.
138      * @param {Object} responseData The data from selectRows.
139      * @param {Function} defaultFormatFn used to format values for tick marks.
140      * @returns {Object}
141      */
142     var generateScales = function(chartType, measures, savedScales, aes, responseData, defaultFormatFn) {
143         var scales = {};
144         var data = responseData.rows;
145         var fields = responseData.metaData.fields;
146         var subjectColumn = 'ParticipantId';
147 
148         if (LABKEY.moduleContext.study && LABKEY.moduleContext.study.subject)
149             subjectColumn = LABKEY.moduleContext.study.subject.columnName;
150 
151         if (chartType === "box_plot")
152         {
153             scales.x = {
154                 scaleType: 'discrete', // Force discrete x-axis scale for box plots.
155                 sortFn: LABKEY.vis.discreteSortFn,
156                 tickLabelMax: 25
157             };
158 
159             var yMin = d3.min(data, aes.y);
160             var yMax = d3.max(data, aes.y);
161             var yPadding = ((yMax - yMin) * .1);
162             if (savedScales.y && savedScales.y.trans == "log")
163             {
164                 // When subtracting padding we have to make sure we still produce valid values for a log scale.
165                 // log([value less than 0]) = NaN.
166                 // log(0) = -Infinity.
167                 if (yMin - yPadding > 0)
168                 {
169                     yMin = yMin - yPadding;
170                 }
171             }
172             else
173             {
174                 yMin = yMin - yPadding;
175             }
176 
177             scales.y = {
178                 min: yMin,
179                 max: yMax + yPadding,
180                 scaleType: 'continuous',
181                 trans: savedScales.y ? savedScales.y.trans : 'linear'
182             };
183         }
184         else
185         {
186             var xMeasureType = _getMeasureType(measures.x);
187             if (xMeasureType == "float" || xMeasureType == "int")
188             {
189                 scales.x = {
190                     scaleType: 'continuous',
191                     trans: savedScales.x ? savedScales.x.trans : 'linear'
192                 };
193             } else
194             {
195                 scales.x = {
196                     scaleType: 'discrete',
197                     sortFn: LABKEY.vis.discreteSortFn,
198                     tickLabelMax: 25
199                 };
200             }
201 
202             scales.y = {
203                 scaleType: 'continuous',
204                 trans: savedScales.y ? savedScales.y.trans : 'linear'
205             };
206 
207         }
208 
209         for (var i = 0; i < fields.length; i++) {
210             var type = fields[i].displayFieldJsonType ? fields[i].displayFieldJsonType : fields[i].type;
211 
212             if (type == 'int' || type == 'float') {
213                 if (measures.x && fields[i].name == measures.x.name) {
214                     if (fields[i].extFormatFn) {
215                         scales.x.tickFormat = eval(fields[i].extFormatFn);
216                     } else if (defaultFormatFn) {
217                         scales.x.tickFormat = defaultFormatFn;
218                     }
219                 }
220 
221                 if (measures.y && fields[i].name == measures.y.name) {
222                     if (fields[i].extFormatFn) {
223                         scales.y.tickFormat = eval(fields[i].extFormatFn);
224                     } else if (defaultFormatFn) {
225                         scales.y.tickFormat = defaultFormatFn;
226                     }
227                 }
228             } else if (measures.x && fields[i].name == measures.x.name && measures.x.name == subjectColumn && LABKEY.demoMode) {
229                     scales.x.tickFormat = function(){return '******'};
230             }
231         }
232 
233         if (savedScales.x && (savedScales.x.min != null || savedScales.x.max != null)) {
234             scales.x.domain = [savedScales.x.min, savedScales.x.max]
235         }
236 
237         if (savedScales.y && (savedScales.y.min != null || savedScales.y.max != null)) {
238             scales.y.domain = [savedScales.y.min, savedScales.y.max]
239         }
240 
241         return scales;
242     };
243 
244     /**
245      * Generates the aesthetic map object needed by the visualization API to render the chart. See {@link LABKEY.vis.Plot}
246      * and {@link LABKEY.vis.Layer}.
247      * @param {String} chartType The chartType from getChartType.
248      * @param {Object} measures The measures from getMeasures.
249      * @param {String} schemaName The schemaName from the saved queryConfig.
250      * @param {String} queryName The queryName from the saved queryConfig.
251      * @returns {Object}
252      */
253     var generateAes = function(chartType, measures, schemaName, queryName) {
254         var aes = {},
255             xMeasureType = _getMeasureType(measures.x),
256             yMeasureType = _getMeasureType(measures.y);
257 
258         if(chartType == "box_plot" && !measures.x) {
259             aes.x = generateMeasurelessAcc(queryName);
260         } else if (xMeasureType == "float" || xMeasureType == "int") {
261             aes.x = generateContinuousAcc(measures.x.name);
262         } else {
263             aes.x = generateDiscreteAcc(measures.x.name, measures.x.label);
264         }
265 
266         if (measures.y)
267         {
268             if (yMeasureType == "float" || yMeasureType == "int")
269                 aes.y = generateContinuousAcc(measures.y.name);
270             else
271                 aes.y = generateDiscreteAcc(measures.y.name, measures.y.label);
272         }
273 
274         if (chartType === "scatter_plot") {
275             aes.hoverText = generatePointHover(measures);
276         } else if (chartType === "box_plot") {
277             if (measures.color) {
278                 aes.outlierColor = generateGroupingAcc(measures.color.name);
279             }
280 
281             if (measures.shape) {
282                 aes.outlierShape = generateGroupingAcc(measures.shape.name);
283             }
284 
285             aes.hoverText = generateBoxplotHover();
286             aes.outlierHoverText = generatePointHover(measures);
287         }
288 
289         // color/shape aes are not dependent on chart type. If we have a box plot with all points enabled, then we
290         // create a second layer for points. So we'll need this no matter what.
291         if (measures.color) {
292             aes.color = generateGroupingAcc(measures.color.name);
293         }
294 
295         if (measures.shape) {
296             aes.shape = generateGroupingAcc(measures.shape.name);
297         }
298 
299         if (measures.pointClickFn) {
300             aes.pointClickFn = generatePointClickFn(
301                     measures,
302                     schemaName,
303                     queryName,
304                     measures.pointClickFn
305             );
306         }
307 
308         return aes;
309     };
310 
311     /**
312      * Generates a function that returns the text used for point hovers.
313      * @param {Object} measures The measures object from the saved chart config.
314      * @returns {Function}
315      */
316     var generatePointHover = function(measures){
317         return function(row) {
318             var hover;
319 
320             if(measures.x) {
321                 hover = measures.x.label + ': ';
322 
323                 if(row[measures.x.name].displayValue){
324                     hover = hover + row[measures.x.name].displayValue;
325                 } else {
326                     hover = hover + row[measures.x.name].value;
327                 }
328             }
329 
330             hover = hover + ', \n' + measures.y.label + ': ' + row[measures.y.name].value;
331 
332             if(measures.color){
333                 hover = hover +  ', \n' + measures.color.label + ': ';
334                 if(row[measures.color.name]){
335                     if(row[measures.color.name].displayValue){
336                         hover = hover + row[measures.color.name].displayValue;
337                     } else {
338                         hover = hover + row[measures.color.name].value;
339                     }
340                 }
341             }
342 
343             if(measures.shape && !(measures.color && measures.color.name == measures.shape.name)){
344                 hover = hover +  ', \n' + measures.shape.label + ': ';
345                 if(row[measures.shape.name]){
346                     if(row[measures.shape.name].displayValue){
347                         hover = hover + row[measures.shape.name].displayValue;
348                     } else {
349                         hover = hover + row[measures.shape.name].value;
350                     }
351                 }
352             }
353             return hover;
354         };
355     };
356 
357     /**
358      * Returns a function used to generate the hover text for box plots.
359      * @returns {Function}
360      */
361     var generateBoxplotHover = function() {
362         return function(xValue, stats) {
363             return xValue + ':\nMin: ' + stats.min + '\nMax: ' + stats.max + '\nQ1: ' + stats.Q1 + '\nQ2: ' + stats.Q2 +
364                     '\nQ3: ' + stats.Q3;
365         };
366     };
367 
368     /**
369      * Generates an accessor function that returns a discrete value from a row of data for a given measure and label.
370      * Used when an axis has a discrete measure (i.e. string).
371      * @param {String} measureName The name of the measure.
372      * @param {String} measureLabel The label of the measure.
373      * @returns {Function}
374      */
375     var generateDiscreteAcc = function(measureName, measureLabel) {
376         return function(row){
377             var valueObj = row[measureName];
378             var value = null;
379 
380             if(valueObj){
381                 value = valueObj.displayValue ? valueObj.displayValue : valueObj.value;
382             } else {
383                 return undefined;
384             }
385 
386             if(value === null){
387                 value = "Not in " + measureLabel;
388             }
389 
390             return value;
391         };
392     };
393 
394     /**
395      * Generates an accessor function that returns a value from a row of data for a given measure.
396      * @param {String} measureName The name of the measure.
397      * @returns {Function}
398      */
399     var generateContinuousAcc = function(measureName){
400         return function(row){
401             var value = null;
402 
403             if(row[measureName]){
404                 value = row[measureName].value;
405 
406                 if(Math.abs(value) === Infinity){
407                     value = null;
408                 }
409 
410                 if(value === false || value === true){
411                     value = value.toString();
412                 }
413 
414                 return value;
415             } else {
416                 return undefined;
417             }
418         }
419     };
420 
421     /**
422      * Generates an accesssor function for shape and color measures.
423      * @param {String} measureName The name of the measure.
424      * @returns {Function}
425      */
426     var generateGroupingAcc = function(measureName){
427         return function(row) {
428             var measureObj = row[measureName];
429             var value = null;
430 
431             if(measureObj){
432                 value = measureObj.displayValue ? measureObj.displayValue : measureObj.value;
433             }
434 
435             if(value === null || value === undefined){
436                 value = "n/a";
437             }
438 
439             return value;
440         };
441     };
442 
443     /**
444      * Generates an accessor for boxplots that do not have an x-axis measure. Generally the measureName passed in is the
445      * queryName.
446      * @param {String} measureName The name of the measure. In this case it is generally the query name.
447      * @returns {Function}
448      */
449     var generateMeasurelessAcc = function(measureName) {
450         // Used for boxplots that do not have an x-axis measure. Instead we just return the
451         // queryName for every row.
452         return function(row) {
453             return measureName;
454         }
455     };
456 
457     /**
458      * Generates the function to be executed when a user clicks a point.
459      * @param {Object} measures The measures from the saved chart config.
460      * @param {String} schemaName The schema name from the saved query config.
461      * @param {String} queryName The query name from the saved query config.
462      * @param {String} fnString The string value of the user-provided function to be executed when a point is clicked.
463      * @returns {Function}
464      */
465     var generatePointClickFn = function(measures, schemaName, queryName, fnString){
466         var measureInfo = {
467             schemaName: schemaName,
468             queryName: queryName
469         };
470 
471         if (measures.y)
472             measureInfo.yAxis = measures.y.name;
473         if (measures.x)
474             measureInfo.xAxis = measures.x.name;
475         if (measures.shape)
476             measureInfo.shapeName = measures.shape.name;
477         if (measures.color)
478             measureInfo.pointName = measures.color.name;
479 
480         // using new Function is quicker than eval(), even in IE.
481         var pointClickFn = new Function('return ' + fnString)();
482         return function(clickEvent, data){
483             pointClickFn(data, measureInfo, clickEvent);
484         };
485     };
486 
487     /**
488      * Generates the Point Geom used for scatter plots and box plots with all points visible.
489      * @param {Object} chartOptions The saved chartOptions object from the chart config.
490      * @returns {LABKEY.vis.Geom.Point}
491      */
492     var generatePointGeom = function(chartOptions){
493         return new LABKEY.vis.Geom.Point({
494             opacity: chartOptions.opacity,
495             size: chartOptions.pointSize,
496             color: '#' + chartOptions.pointFillColor,
497             position: chartOptions.position
498         });
499     };
500 
501     /**
502      * Generates the Boxplot Geom used for box plots.
503      * @param {Object} chartOptions The saved chartOptions object from the chart config.
504      * @returns {LABKEY.vis.Geom.Boxplot}
505      */
506     var generateBoxplotGeom = function(chartOptions){
507         return new LABKEY.vis.Geom.Boxplot({
508             lineWidth: chartOptions.lineWidth,
509             outlierOpacity: chartOptions.opacity,
510             outlierFill: '#' + chartOptions.pointFillColor,
511             outlierSize: chartOptions.pointSize,
512             color: '#' + chartOptions.lineColor,
513             fill: chartOptions.boxFillColor == 'none' ? chartOptions.boxFillColor : '#' + chartOptions.boxFillColor,
514             position: chartOptions.position,
515             showOutliers: chartOptions.showOutliers
516         });
517     };
518 
519     /**
520      * Generates the Barplot Geom used for bar charts.
521      * @param {Object} chartOptions The saved chartOptions object from the chart config.
522      * @returns {LABKEY.vis.Geom.BarPlot}
523      */
524     var generateBarGeom = function(chartOptions){
525         return new LABKEY.vis.Geom.BarPlot({
526             opacity: chartOptions.opacity,
527             color: '#' + chartOptions.lineColor,
528             fill: '#' + chartOptions.boxFillColor,
529             lineWidth: chartOptions.lineWidth
530         });
531     };
532 
533     /**
534      * Generates the Bin Geom used to bin a set of points.
535      * @param {Object} chartOptions The saved chartOptions object from the chart config.
536      * @returns {LABKEY.vis.Geom.Bin}
537      */
538     var generateBinGeom = function(chartOptions) {
539         var colorRange = ["#e6e6e6", "#085D90"]; //light-gray and labkey blue is default
540         if (chartOptions.binColorGroup == 'SingleColor') {
541             var color = '#' + chartOptions.binSingleColor;
542             colorRange = ["#FFFFFF", color];
543         }
544         else if (chartOptions.binColorGroup == 'Heat') {
545             colorRange = ["#fff6bc", "#e23202"];
546         }
547 
548         return new LABKEY.vis.Geom.Bin({
549             shape: chartOptions.binShape,
550             colorRange: colorRange,
551             size: chartOptions.binShape == 'square' ? 10 : 5
552         })
553     };
554 
555     /**
556      * Generates a Geom based on the chartType.
557      * @param {String} chartType The chart type from getChartType.
558      * @param {Object} chartOptions The chartOptions object from the saved chart config.
559      * @returns {LABKEY.vis.Geom}
560      */
561     var generateGeom = function(chartType, chartOptions) {
562         if (chartType == "box_plot")
563             return generateBoxplotGeom(chartOptions);
564         else if (chartType == "scatter_plot")
565             return chartOptions.binned ? generateBinGeom(chartOptions) : generatePointGeom(chartOptions);
566         else if (chartType == "bar_chart")
567             return generateBarGeom(chartOptions);
568     };
569 
570     /**
571      *
572      * @param {Array} data The response data from selectRows.
573      * @param {String} dimensionName The grouping variable to get distinct members from.
574      * @param {String} measureName The variable to calculate aggregate values over. Nullable.
575      * @param {String} aggregate MIN/MAX/SUM/COUNT/etc. Defaults to COUNT.
576      * @param {String} nullDisplayValue The display value to use for null dimension values. Defaults to 'null'.
577      */
578     var generateAggregateData = function(data, dimensionName, measureName, aggregate, nullDisplayValue)
579     {
580         var uniqueDimValues = {};
581         for (var i = 0; i < data.length; i++)
582         {
583             var dimVal = null;
584             if (typeof data[i][dimensionName] == 'object')
585                 dimVal = data[i][dimensionName].hasOwnProperty('displayValue') ? data[i][dimensionName].displayValue : data[i][dimensionName].value;
586 
587             var measureVal = null;
588             if (measureName != undefined && measureName != null && typeof data[i][measureName] == 'object')
589                 measureVal = data[i][measureName].value;
590 
591             if (uniqueDimValues[dimVal] == undefined)
592                 uniqueDimValues[dimVal] = {count: 0, sum: 0};
593 
594             uniqueDimValues[dimVal].count++;
595             if (!isNaN(measureVal))
596                 uniqueDimValues[dimVal].sum += measureVal;
597         }
598 
599         var keys = Object.keys(uniqueDimValues), results = [];
600         for (var i = 0; i < keys.length; i++)
601         {
602             var row = {
603                 label: keys[i] == null || keys[i] == 'null' ? nullDisplayValue || 'null' : keys[i]
604             };
605 
606             // TODO add support for more aggregates
607             if (aggregate == undefined || aggregate == null || aggregate == 'COUNT')
608                 row.value = uniqueDimValues[keys[i]].count;
609             else if (aggregate == 'SUM')
610                 row.value = uniqueDimValues[keys[i]].sum;
611             else
612                 throw 'Aggregate ' + aggregate + ' is not yet supported.';
613 
614             results.push(row);
615         }
616         return results;
617     };
618 
619     /**
620      * Generate the plot config for the given chart renderType and config options.
621      * @param renderTo
622      * @param chartConfig
623      * @param labels
624      * @param aes
625      * @param scales
626      * @param data
627      * @returns {Object}
628      */
629     var generatePlotConfig = function(renderTo, chartConfig, labels, aes, scales, geom, data)
630     {
631         var renderType = chartConfig.renderType,
632             layers = [], clipRect,
633             plotConfig = {
634                 renderTo: renderTo,
635                 rendererType: 'd3',
636                 width: chartConfig.width,
637                 height: chartConfig.height
638             };
639 
640         if (renderType == 'pie_chart')
641             return _generatePieChartConfig(plotConfig, chartConfig, labels, data);
642 
643         clipRect = (scales.x && Ext4.isArray(scales.x.domain)) || (scales.y && Ext4.isArray(scales.y.domain));
644 
645         if (renderType == 'bar_chart')
646         {
647             aes = { x: 'label', y: 'value' };
648 
649             if (scales.y.domain) {
650                 scales.y = { domain: scales.y.domain };
651             } else {
652                 var values = Ext4.Array.pluck(data, 'value'),
653                         min = Math.min(0, Ext4.Array.min(values)),
654                         max = Math.max(0, Ext4.Array.max(values));
655                 scales.y = { domain: [min, max] };
656             }
657         }
658         else if (renderType == 'box_plot' && chartConfig.pointType == 'all')
659         {
660             layers.push(
661                 new LABKEY.vis.Layer({
662                     data: data,
663                     geom: LABKEY.vis.GenericChartHelper.generatePointGeom(chartConfig.geomOptions),
664                     aes: {hoverText: LABKEY.vis.GenericChartHelper.generatePointHover(chartConfig.measures)}
665                 })
666             );
667         }
668 
669         layers.push(
670             new LABKEY.vis.Layer({
671                 data: data,
672                 geom: geom
673             })
674         );
675 
676         plotConfig = Ext4.apply(plotConfig, {
677             clipRect: clipRect,
678             data: data,
679             labels: labels,
680             aes: aes,
681             scales: scales,
682             layers: layers
683         });
684 
685         return plotConfig;
686     };
687 
688     var _generatePieChartConfig = function(baseConfig, chartConfig, labels, data)
689     {
690         return Ext4.apply(baseConfig, {
691             data: data,
692             header: {
693                 title: { text: labels.main.value },
694                 subtitle: { text: labels.subtitle.value },
695                 titleSubtitlePadding: 1
696             },
697             footer: {
698                 text: labels.footer.value,
699                 location: 'bottom-center'
700             },
701             labels: {
702                 mainLabel: { fontSize: 14 },
703                 percentage: {
704                     fontSize: 14,
705                     color: chartConfig.geomOptions.piePercentagesColor != null ? '#' + chartConfig.geomOptions.piePercentagesColor : undefined
706                 },
707                 outer: { pieDistance: 20 },
708                 inner: {
709                     format: chartConfig.geomOptions.showPiePercentages ? 'percentage' : 'none',
710                     hideWhenLessThanPercentage: chartConfig.geomOptions.pieHideWhenLessThanPercentage
711                 }
712             },
713             size: {
714                 pieInnerRadius: chartConfig.geomOptions.pieInnerRadius + '%',
715                 pieOuterRadius: chartConfig.geomOptions.pieOuterRadius + '%'
716             },
717             misc: {
718                 gradient: {
719                     enabled: chartConfig.geomOptions.gradientPercentage != 0,
720                     percentage: chartConfig.geomOptions.gradientPercentage,
721                     color: '#' + chartConfig.geomOptions.gradientColor
722                 },
723                 colors: {
724                     segments: LABKEY.vis.Scale[chartConfig.geomOptions.colorPaletteScale]()
725                 }
726             },
727             effects: { highlightSegmentOnMouseover: false },
728             tooltips: { enabled: true }
729         });
730     };
731 
732     /**
733      * Check if the selectRows API response has data. Return an error string if no data exists.
734      * @param response
735      * @param includeFilterMsg true to include a message about removing filters
736      * @returns {String}
737      */
738     var validateResponseHasData = function(response, includeFilterMsg)
739     {
740         if (!response || !response.rows || response.rows.length == 0)
741         {
742             return 'The response returned 0 rows of data. The query may be empty or the applied filters may be too strict.'
743                 + (includeFilterMsg ? 'Try removing or adjusting any filters if possible.' : '');
744         }
745 
746         return null;
747     };
748 
749     /**
750      * Verifies that the axis measure is actually present and has data. Also checks to make sure that data can be used in a log
751      * scale (if applicable). Returns an object with a success parameter (boolean) and a message parameter (string). If the
752      * success parameter is false there is a critical error and the chart cannot be rendered. If success is true the chart
753      * can be rendered. Message will contain an error or warning message if applicable. If message is not null and success
754      * is true, there is a warning.
755      * @param {String} chartType The chartType from getChartType.
756      * @param {Object} chartConfig The saved chartConfig object.
757      * @param {String} measureName The name of the axis measure property.
758      * @param {Object} aes The aes object from generateAes.
759      * @param {Object} scales The scales object from generateScales.
760      * @param {Array} data The response data from selectRows.
761      * @returns {Object}
762      */
763     var validateAxisMeasure = function(chartType, chartConfig, measureName, aes, scales, data){
764 
765         var dataIsNull = true, measureUndefined = true, invalidLogValues = false, hasZeroes = false, message = null;
766 
767         for (var i = 0; i < data.length; i ++)
768         {
769             var value = aes[measureName](data[i]);
770 
771             if (value !== undefined)
772                 measureUndefined = false;
773 
774             if (value !== null)
775                 dataIsNull = false;
776 
777             if (value && value < 0)
778                 invalidLogValues = true;
779 
780             if (value === 0 )
781                 hasZeroes = true;
782         }
783 
784         if (measureUndefined)
785         {
786             message = 'The measure ' + chartConfig.measures[measureName].label + ' was not found. It may have been renamed or removed.';
787             return {success: false, message: message};
788         }
789 
790         if ((chartType == 'scatter_plot' || measureName == 'y') && dataIsNull)
791         {
792             message = 'All data values for ' + chartConfig.measures[measureName].label + ' are null. Please choose a different measure.';
793             return {success: false, message: message};
794         }
795 
796         if (scales[measureName] && scales[measureName].trans == "log")
797         {
798             if (invalidLogValues)
799             {
800                 message = "Unable to use a log scale on the y-axis. All y-axis values must be >= 0. Reverting to linear scale on y-axis.";
801                 scales[measureName].trans = 'linear';
802             }
803             else if (hasZeroes)
804             {
805                 message = "Some " + measureName + "-axis values are 0. Plotting all " + measureName + "-axis values as value+1.";
806                 var accFn = aes[measureName];
807                 aes[measureName] = function(row){return accFn(row) + 1};
808             }
809         }
810 
811         return {success: true, message: message};
812     };
813 
814     /**
815      * Deprecated - use validateAxisMeasure
816      */
817     var validateXAxis = function(chartType, chartConfig, aes, scales, data){
818         return this.validateAxisMeasure(chartType, chartConfig, 'x', aes, scales, data);
819     };
820     /**
821      * Deprecated - use validateAxisMeasure
822      */
823     var validateYAxis = function(chartType, chartConfig, aes, scales, data){
824         return this.validateAxisMeasure(chartType, chartConfig, 'y', aes, scales, data);
825     };
826 
827     var _getMeasureType = function(measure) {
828         return measure ? (measure.normalizedType || measure.type) : null;
829     };
830 
831     return {
832         // NOTE: the @function below is needed or JSDoc will not include the documentation for loadVisDependencies. Don't
833         // ask me why, I do not know.
834         /**
835          * @function
836          */
837         getRenderTypes: getRenderTypes,
838         getMeasureType: _getMeasureType,
839         getChartType: getChartType,
840         getDefaultLabel: getDefaultLabel,
841         generateLabels: generateLabels,
842         generateScales: generateScales,
843         generateAes: generateAes,
844         generatePointHover: generatePointHover,
845         generateBoxplotHover: generateBoxplotHover,
846         generateDiscreteAcc: generateDiscreteAcc,
847         generateContinuousAcc: generateContinuousAcc,
848         generateGroupingAcc: generateGroupingAcc,
849         generatePointClickFn: generatePointClickFn,
850         generateGeom: generateGeom,
851         generateBoxplotGeom: generateBoxplotGeom,
852         generatePointGeom: generatePointGeom,
853         generateAggregateData: generateAggregateData,
854         generatePlotConfig: generatePlotConfig,
855         validateResponseHasData: validateResponseHasData,
856         validateAxisMeasure: validateAxisMeasure,
857         validateXAxis: validateXAxis,
858         validateYAxis: validateYAxis,
859         /**
860          * Loads all of the required dependencies for a Generic Chart.
861          * @param {Function} callback The callback to be executed when all of the visualization dependencies have been loaded.
862          * @param {Object} scope The scope to be used when executing the callback.
863          */
864         loadVisDependencies: LABKEY.requiresVisualization
865     };
866 };