1 /**
  2  * @fileOverview
  3  * @author <a href="https://www.labkey.org">LabKey</a> (<a href="mailto:info@labkey.com">info@labkey.com</a>)
  4  * @license Copyright (c) 2012-2019 LabKey Corporation
  5  * <p/>
  6  * Licensed under the Apache License, Version 2.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at
  9  * <p/>
 10  * http://www.apache.org/licenses/LICENSE-2.0
 11  * <p/>
 12  * Unless required by applicable law or agreed to in writing, software
 13  * distributed under the License is distributed on an "AS IS" BASIS,
 14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing permissions and
 16  * limitations under the License.
 17  * <p/>
 18  */
 19 
 20 
 21 /**
 22  * @name LABKEY.vis.Plot
 23  * @class Plot which allows a user to programmatically create a plot/visualization.
 24  * @description
 25  * @param {Object} config An object that contains the following properties.
 26  * @param {String} config.renderTo The id of the div/span to insert the svg element into.
 27  * @param {Number} config.width The plot width in pixels. This is the width of the entire plot, including margins, the
 28  *      legend, and labels.
 29  * @param {Number} config.height The plot height in pixels. This is the height of the entire plot, including margins and
 30  *      labels.
 31  * @param {Array} [config.data] (Optional) The array of data used while rendering the plot. This array will be used in
 32  *      layers that do not have any data specified. <em>Note:</em> While config.data is optional, if it is not present
 33  *      in the Plot object it must be defined within each {@link LABKEY.vis.Layer}. Data must be array based, with each
 34  *      row of data being an item in the array. The format of each row does not matter, you define how the data is
 35  *      accessed within the <strong>config.aes</strong> object.
 36  * @param {Object} [config.aes] (Optional) An object containing all of the requested aesthetic mappings. Like
 37  *      <em>config.data</em>, config.aes is optional at the plot level because it can be defined at the layer level as
 38  *      well, however, if config.aes is not present at the plot level it must be defined within each
 39  *      {@link LABKEY.vis.Layer}}. The aesthetic mappings required depend entirely on the {@link LABKEY.vis.Geom}s being
 40  *      used in the plot. The only maps required are <strong><em>config.aes.x</em></strong> and
 41  *      <em><strong>config.aes.y</strong> (or alternatively yLeft or yRight)</em>. To find out the available aesthetic
 42  *      mappings for your plot, please see the documentation for each Geom you are using.
 43  * @param {Array} config.layers An array of {@link LABKEY.vis.Layer} objects.
 44  * @param {Object} [config.scales] (Optional) An object that describes the scales needed for each axis or dimension. If
 45  *      not defined by the user we do our best to create a default scale determined by the data given, however in some
 46  *      cases we will not be able to construct a scale, and we will display or throw an error. The possible scales are
 47  *      <strong>x</strong>, <strong>y (or yLeft)</strong>, <strong>yRight</strong>, <strong>color</strong>,
 48  *      <strong>shape</strong>, and <strong>size</strong>. Each scale object will have the following properties:
 49  *      <ul>
 50  *          <li><strong>scaleType:</strong> possible values "continuous" or "discrete".</li>
 51  *          <li><strong>trans:</strong> with values "linear" or "log". Controls the transformation of the data on
 52  *          the grid.</li>
 53  *          <li><strong>min:</strong> (<em>deprecated, use domain</em>) the minimum expected input value. Used to control what is visible on the grid.</li>
 54  *          <li><strong>max:</strong> (<em>deprecated, use domain</em>) the maximum expected input value. Used to control what is visible on the grid.</li>
 55  *          <li><strong>domain:</strong> For continuous scales it is an array of [min, max]. For discrete scales
 56  *              it is an an array of all possible input values to the scale.</li>
 57  *          <li><strong>range:</strong> An array of values that all input values (the domain) will be mapped to. Not
 58  *          used for any axis scales. For continuous color scales it is an array[min, max] hex values.</li>
 59  *          <li><strong>sortFn:</strong> If scaleType is "discrete", the sortFn can be used to order the values of the domain</li>
 60  *          <li><strong>tickFormat:</strong> Add axis label formatting.</li>
 61  *          <li><strong>tickValues:</strong> Define the axis tick values. Array of values.</li>
 62  *          <li><strong>tickDigits:</strong> Convert axis tick to exponential form if equal or greater than number of digits</li>
 63  *          <li><strong>tickLabelMax:</strong> Maximum number of tick labels to show for a categorical axis.</li>
 64  *          <li><strong>tickHoverText:</strong>: Adds hover text for axis labels.</li>
 65  *          <li><strong>tickCls:</strong> Add class to axis label.</li>
 66  *          <li><strong>tickRectCls:</strong> Add class to mouse area rectangle around axis label.</li>
 67  *          <li><strong>tickRectHeightOffset:</strong> Set axis mouse area rect width. Offset beyond label text width.</li>
 68  *          <li><strong>tickRectWidthOffset:</strong> Set axis mouse area rect height. Offset beyond label text height.</li>
 69  *          <li><strong>tickClick:</strong> Handler for axis label click. Binds to mouse area rect around label.</li>
 70  *          <li><strong>tickMouseOver:</strong> Handler for axis label mouse over. Binds to mouse area rect around label.</li>
 71  *          <li><strong>tickMouseOut:</strong> Handler for axis label mouse out. Binds to mouse area rect around label.</li>
 72  *      </ul>
 73  * @param {Object} [config.labels] (Optional) An object with the following properties: main, subtitle, x, y (or yLeft), yRight.
 74  *      Each property can have a {String} value, {Boolean} lookClickable, {Object} listeners, and other properties listed below.
 75  *      The value is the text that will appear on the label, lookClickable toggles if the label will appear clickable, and the
 76  *      listeners property allows the user to specify listeners on the labels such as click, hover, etc, as well as the functions to
 77  *      execute when the events occur. Each label will be an object that has the following properties:
 78  *      <ul>
 79  *          <li>
 80  *              <strong>value:</strong> The string value of the label (i.e. "Weight Over Time").
 81  *          </li>
 82  *          <li>
 83  *              <strong>fontSize:</strong> The font-size in pixels.
 84  *          </li>
 85  *          <li>
 86  *              <strong>position:</strong> The number of pixels from the edge to render the label.
 87  *          </li>
 88  *          <li>
 89  *              <strong>lookClickable:</strong> If true it styles the label so that it appears to be clickable. Defaults
 90  *              to false.
 91  *          </li>
 92  *          <li>
 93  *              <strong>visibility:</strong> The initial visibility state for the label. Defaults to normal.
 94  *          </li>
 95  *          <li>
 96  *              <strong>cls:</strong> Class added to label element.
 97  *          </li>
 98  *          <li>
 99  *              <strong>listeners:</strong> An object with properties for each listener the user wants attached
100  *              to the label. The value of each property is the function to be called when the event occurs. The
101  *              available listeners are: click, dblclick, hover, mousedown, mouseup, mousemove, mouseout, mouseover,
102  *              touchcancel, touchend, touchmove, and touchstart.
103  *          </li>
104  *      </ul>
105  * @param {Object} [config.margins] (Optional) Margin sizes in pixels. It can be useful to set the margins if the tick
106  *      marks on an axis are overlapping with your axis labels. Defaults to top: 75px, right: 75px, bottom: 50px, and
107  *      left: 75px. The right side may have a margin of 150px if a legend is needed. Custom define margin size for a
108  *      legend that exceeds 150px.
109  *      The object may contain any of the following properties:
110  *      <ul>
111  *          <li><strong>top:</strong> Size of top margin in pixels.</li>
112  *          <li><strong>bottom:</strong> Size of bottom margin in pixels.</li>
113  *          <li><strong>left:</strong> Size of left margin in pixels.</li>
114  *          <li><strong>right:</strong> Size of right margin in pixels.</li>
115  *      </ul>
116  * @param {String} [config.legendPos] (Optional) Used to specify where the legend will render. Currently only supports
117  *      "none" to disable the rendering of the legend. There are future plans to support "left" and "right" as well.
118  *      Defaults to "right".
119  * @param {Boolean} [config.legendNoWrap] (Optional) True to force legend text in a single line.
120  *      Defaults to false.
121  * @param {String} [config.bgColor] (Optional) The string representation of the background color. Defaults to white.
122  * @param {String} [config.gridColor] (Optional) The string representation of the grid color. Defaults to white.
123  * @param {String} [config.gridLineColor] (Optional) The string representation of the line colors used as part of the grid.
124  *      Defaults to grey (#dddddd).
125  * @param {Boolean} [config.clipRect] (Optional) Used to toggle the use of a clipRect, which prevents values that appear
126  *      outside of the specified grid area from being visible. Use of clipRect can negatively affect performance, do not
127  *      use if there is a large amount of elements on the grid. Defaults to false.
128  * @param {String} [config.fontFamily] (Optional) Font-family to use for plot text (labels, legend, etc.).
129  * @param {Boolean} [config.throwErrors] (Optional) Used to toggle between the plot throwing errors or displaying errors.
130  *      If true the plot will throw an error instead of displaying an error when necessary and possible. Defaults to
131  *      false.
132  *
133  * @param {Boolean} [config.requireYLogGutter] (Optional) Used to indicate that the plot has non-positive data on x dimension
134  *      that should be displayed in y log gutter in log scale.
135  * @param {Boolean} [config.requireXLogGutter] (Optional) Used to indicate that the plot has non-positive data on y dimension
136  *      that should be displayed in x log gutter in log scale.
137  * @param {Boolean} [config.isMainPlot] (Optional) Used in combination with requireYLogGutter and requireXLogGutter to
138  *      shift the main plot's axis position in order to show log gutters.
139  * @param {Boolean} [config.isShowYAxis] (Optional) Used to draw the Y axis to separate positive and negative values
140  *      for log scale plot in the undefined X gutter plot.
141  * @param {Boolean} [config.isShowXAxis] (Optional) Used to draw the X axis to separate positive and negative values
142  *      for log scale plot in the undefined Y gutter plot.
143  * @param {Float} [config.minXPositiveValue] (Optional) Used to adjust domains with non-positive lower bound and generate x axis
144  *      log scale wrapper for plots that contain <= 0 x value.
145  * @param {Float} [config.minYPositiveValue] (Optional) Used to adjust domains with non-positive lower bound and generate y axis
146  *      log scale wrapper for plots that contain <= 0 y value.
147  * @param {Object} [config.brushing] (Optional) Configuration for brushing events on the plot.
148  *      The object may contain any of the following properties:
149  *      <ul>
150  *          <li><strong>brush:</strong> Callback function during the brush event.</li>
151  *          <li><strong>brushclear:</strong> Callback function for when the brush event is cleared.</li>
152  *          <li><strong>brushend:</strong> Callback function for when the brush event ends.</li>
153  *          <li><strong>brushstart:</strong> Callback function for when the brush event starts.</li>
154  *      </ul>
155  * @param {Array} [config.legendData] (Optional) An array of objects to be used for the plot legend. Each object can
156  *      include the legend item text, color, and shape.
157  * @param {Object} [config.disableAxis] (Optional) Object indicating which plot axes to disable. Options include
158  *      xTop, xBottom, yLeft, and yRight.
159  * @param {String} [config.gridLinesVisible] (Optional) String indicating if only the 'x' or 'y' axis grid lines
160  *      should be shown for this plot. Default, without this property, is to show both x and y grid lines.
161  * @param {String} [config.tickColor] (Optional) The string representation of the color to use for the tick marks on
162  *      the x and y axes. Defaults to black.
163  * @param {String} [config.borderColor] (Optional) The string representation of the color to use for the border (axes).
164  *      Defaults to black.
165  * @param {String} [config.tickTextColor] (Optional) The string representation of the color to use for the x and y
166  *      axis tick labels. Defaults to black.
167  * @param {Integer} [config.tickLength] (Optional) The length, in pixels for the x and y axis tick marks. Defaults to 8.
168  * @param {Integer} [config.tickWidth] (Optional) The x and y axis tick line width. Defaults to 1.
169  * @param {Integer} [config.tickOverlapRotation] (Optional) The degree of rotation for overlapping x axis tick labels.
170  *      Defaults to 15 degrees.
171  * @param {Integer} [config.gridLineWidth] (Optional) The line width for the grid lines of the plot. Defaults to 1.
172  * @param {Integer} [config.borderWidth] (Optional) The border line width with the x and y axis. Defaults to 1.
173  *
174   @example
175  In this example we will create a simple scatter plot.
176  
177  <div id='plot'>
178  </div id='plot'>
179  <script type="text/javascript">
180 var scatterData = [];
181 
182 // Here we're creating some fake data to create a plot with.
183 for(var i = 0; i < 1000; i++){
184     var point = {
185         x: {value: parseInt((Math.random()*(150)))},
186         y: Math.random() * 1500
187     };
188     scatterData.push(point);
189 }
190 
191 // Create a new layer object.
192 var pointLayer = new LABKEY.vis.Layer({
193 	geom: new LABKEY.vis.Geom.Point()
194 });
195 
196 
197 // Create a new plot object.
198 var scatterPlot = new LABKEY.vis.Plot({
199 	renderTo: 'plot',
200 	width: 900,
201 	height: 700,
202 	data: scatterData,
203 	layers: [pointLayer],
204 	aes: {
205 		// Aesthetic mappings can be functions or strings.
206 		x: function(row){return row.x.value},
207 		y: 'y'
208 	}
209 });
210 
211 scatterPlot.render();
212  </script>
213 
214  @example
215  In this example we create a simple box plot.
216 
217  <div id='plot'>
218  </div id='plot'>
219  <script type="text/javascript">
220     // First let's create some data.
221 
222 var boxPlotData = [];
223 
224 for(var i = 0; i < 6; i++){
225     var group = "Group "+(i+1);
226     for(var j = 0; j < 25; j++){
227         boxPlotData.push({
228             group: group,
229             //Compute a random age between 25 and 55
230             age: parseInt(25+(Math.random()*(55-25))),
231             gender: parseInt((Math.random()*2)) === 0 ? 'male' : 'female'
232         });
233     }
234     for(j = 0; j < 3; j++){
235         boxPlotData.push({
236             group: group,
237             //Compute a random age between 75 and 95
238             age: parseInt(75+(Math.random()*(95-75))),
239             gender: parseInt((Math.random()*2)) === 0 ? 'male' : 'female'
240         });
241     }
242     for(j = 0; j < 3; j++){
243         boxPlotData.push({
244             group: group,
245             //Compute a random age between 1 and 16
246             age: parseInt(1+(Math.random()*(16-1))),
247             gender: parseInt((Math.random()*2)) === 0 ? 'male' : 'female'
248         });
249     }
250 }
251 
252 
253 // Now we create the Layer.
254 var boxLayer = new LABKEY.vis.Layer({
255     geom: new LABKEY.vis.Geom.Boxplot({
256     	// Customize the Boxplot Geom to fit our needs.
257 		position: 'jitter',
258 		outlierOpacity: '1',
259 		outlierFill: 'red',
260 		showOutliers: true,
261 		opacity: '.5',
262 		outlierColor: 'red'
263     }),
264     aes: {
265         hoverText: function(x, stats){
266             return x + ':\nMin: ' + stats.min + '\nMax: ' + stats.max + '\nQ1: ' +
267                 stats.Q1 + '\nQ2: ' + stats.Q2 + '\nQ3: ' + stats.Q3;
268         },
269         outlierHoverText: function(row){
270             return "Group: " + row.group + ", Age: " + row.age;
271         },
272         outlierShape: function(row){return row.gender;}
273     }
274 });
275 
276 
277 // Create a new Plot object.
278 var boxPlot = new LABKEY.vis.Plot({
279     renderTo: 'plot',
280     width: 900,
281     height: 300,
282     labels: {
283         main: {value: 'Example Box Plot'},
284         yLeft: {value: 'Age'},
285         x: {value: 'Groups of People'}
286     },
287     data: boxPlotData,
288     layers: [boxLayer],
289     aes: {
290         yLeft: 'age',
291         x: 'group'
292     },
293     scales: {
294         x: {
295             scaleType: 'discrete'
296         },
297         yLeft: {
298             scaleType: 'continuous',
299             trans: 'linear'
300         }
301     },
302     margins: {
303         bottom: 75
304     }
305 });
306 
307 boxPlot.render();
308  </script>
309 
310  */
311 (function(){
312     var initMargins = function(userMargins, legendPos, allAes, scales, labels){
313         var margins = {}, top = 75, right = 75, bottom = 50, left = 75; // Defaults.
314         var foundLegendScale = false, foundYRight = false;
315 
316         for(var i = 0; i < allAes.length; i++){
317             var aes = allAes[i];
318             if(!foundLegendScale && (aes.shape || (aes.color && (!scales.color || (scales.color && scales.color.scaleType == 'discrete'))) || aes.outlierColor || aes.outlierShape || aes.pathColor) && legendPos != 'none'){
319                 foundLegendScale = true;
320                 right = right + 150;
321             }
322 
323             if(!foundYRight && aes.yRight){
324                 foundYRight = true;
325                 right = right + 25;
326             }
327         }
328 
329         if(!userMargins){
330             userMargins = {};
331         }
332 
333         if(typeof userMargins.top === 'undefined'){
334             margins.top = top + (labels && labels.subtitle ? 20 : 0);
335         } else {
336             margins.top = userMargins.top;
337         }
338         if(typeof userMargins.right === 'undefined'){
339             margins.right = right;
340         } else {
341             margins.right = userMargins.right;
342         }
343         if(typeof userMargins.bottom === 'undefined'){
344             margins.bottom = bottom;
345         } else {
346             margins.bottom = userMargins.bottom;
347         }
348         if(typeof userMargins.left === 'undefined'){
349             margins.left = left;
350         } else {
351             margins.left = userMargins.left;
352         }
353 
354         return margins;
355     };
356 
357     var initGridDimensions = function(grid, margins) {
358         grid.leftEdge = margins.left;
359         grid.rightEdge = grid.width - margins.right + 10;
360         grid.topEdge = margins.top;
361         grid.bottomEdge = grid.height - margins.bottom;
362         return grid;
363     };
364 
365     var copyUserScales = function(origScales) {
366         // This copies the user's scales, but not the max/min because we don't want to over-write that, so we store the original
367         // scales separately (this.originalScales).
368         var scales = {}, newScaleName, origScale, newScale;
369         for (var scaleName in origScales) {
370             if (origScales.hasOwnProperty(scaleName)) {
371                 if(scaleName == 'y'){
372                     origScales.yLeft = origScales.y;
373                     newScaleName = (scaleName == 'y') ? 'yLeft' : scaleName;
374                 } else {
375                     newScaleName = scaleName;
376                 }
377                 newScale = {};
378                 origScale = origScales[scaleName];
379 
380                 newScale.scaleType = origScale.scaleType ? origScale.scaleType : 'continuous';
381                 newScale.sortFn = origScale.sortFn ? origScale.sortFn : null;
382                 newScale.trans = origScale.trans ? origScale.trans : 'linear';
383                 newScale.tickValues = origScale.tickValues ? origScale.tickValues : null;
384                 newScale.tickFormat = origScale.tickFormat ? origScale.tickFormat : null;
385                 newScale.tickDigits = origScale.tickDigits ? origScale.tickDigits : null;
386                 newScale.tickLabelMax = origScale.tickLabelMax ? origScale.tickLabelMax : null;
387                 newScale.tickHoverText = origScale.tickHoverText ? origScale.tickHoverText : null;
388                 newScale.tickCls = origScale.tickCls ? origScale.tickCls : null;
389                 newScale.tickRectCls = origScale.tickRectCls ? origScale.tickRectCls : null;
390                 newScale.tickRectHeightOffset = origScale.tickRectHeightOffset ? origScale.tickRectHeightOffset : null;
391                 newScale.tickRectWidthOffset = origScale.tickRectWidthOffset ? origScale.tickRectWidthOffset : null;
392                 newScale.domain = origScale.domain ? origScale.domain : null;
393                 newScale.range = origScale.range ? origScale.range : null;
394                 newScale.fontSize = origScale.fontSize ? origScale.fontSize : null;
395                 newScale.colorType = origScale.colorType ? origScale.colorType : null;
396 
397                 newScale.tickClick = origScale.tickClick ? origScale.tickClick : null;
398                 newScale.tickMouseOver = origScale.tickMouseOver ? origScale.tickMouseOver : null;
399                 newScale.tickMouseOut = origScale.tickMouseOut ? origScale.tickMouseOut : null;
400 
401                 if (!origScale.domain &&((origScale.hasOwnProperty('min') && LABKEY.vis.isValid(origScale.min)) ||
402                         (origScale.hasOwnProperty('max') && LABKEY.vis.isValid(origScale.max)))) {
403                     console.log('scale.min and scale.max are deprecated. Please use scale.domain.');
404                     newScale.domain = [origScale.min, origScale.max];
405                     origScale.domain = [origScale.min, origScale.max];
406                 }
407 
408                 if (newScale.scaleType == 'ordinal' || newScale.scaleType == 'categorical') {
409                     newScale.scaleType = 'discrete';
410                 }
411 
412                 scales[newScaleName] = newScale;
413             }
414         }
415         return scales;
416     };
417 
418     var setupDefaultScales = function(scales, aes) {
419         for (var aesthetic in aes) {
420             if (aes.hasOwnProperty(aesthetic)) {
421                 if(!scales[aesthetic]){
422                     // Not all aesthetics get a scale (like hoverText), so we have to be pretty specific.
423                     if(aesthetic === 'x' || aesthetic === 'xTop' || aesthetic === 'xSub' || aesthetic === 'yLeft'
424                             || aesthetic === 'yRight' || aesthetic === 'size'){
425                         scales[aesthetic] = {scaleType: 'continuous', trans: 'linear'};
426                     } else if (aesthetic == 'color' || aesthetic == 'outlierColor' || aesthetic == 'pathColor') {
427                         scales['color'] = {scaleType: 'discrete'};
428                     } else if(aesthetic == 'shape' || aesthetic == 'outlierShape'){
429                         scales['shape'] = {scaleType: 'discrete'};
430                     }
431                 }
432             }
433         }
434     };
435 
436     var getDiscreteAxisDomain = function(data, acc) {
437         // If any axis is discrete we need to know the domain before rendering so we render the grid correctly.
438         var domain = [], uniques = {}, i, value;
439 
440         for (i = 0; i < data.length; i++) {
441             value = acc(data[i]);
442             if (value != undefined)
443                 uniques[value] = true;
444         }
445 
446         for (value in uniques) {
447             if(uniques.hasOwnProperty(value)) {
448                 domain.push(value);
449             }
450         }
451 
452         return domain;
453     };
454 
455     var getContinuousDomain = function(aesName, userScale, data, acc, errorAes) {
456         var userMin, userMax, min, max, minAcc, maxAcc;
457 
458         if (userScale && userScale.domain) {
459             userMin = userScale.domain[0];
460             userMax = userScale.domain[1];
461         }
462 
463         if (LABKEY.vis.isValid(userMin)) {
464             min = userMin;
465         } else {
466             if ((aesName == 'yLeft' || aesName == 'yRight') && errorAes) {
467                 minAcc = function(d) {
468                     if (LABKEY.vis.isValid(acc(d))) {
469                         return acc(d) - errorAes.getValue(d);
470                     }
471                     else {
472                         return null;
473                     }
474                 };
475             } else {
476                 minAcc = acc;
477             }
478 
479             min = d3.min(data, minAcc);
480         }
481 
482         if (LABKEY.vis.isValid(userMax)) {
483             max = userMax;
484         } else {
485             if ((aesName == 'yLeft' || aesName == 'yRight') && errorAes) {
486                 maxAcc = function(d) {
487                     return acc(d) + errorAes.getValue(d);
488                 }
489             } else {
490                 maxAcc = acc;
491             }
492             max = d3.max(data, maxAcc);
493         }
494 
495         if (min == max ) {
496             // use *2 and /2 so that we won't end up with <= 0 value for log scale
497             if (userScale && userScale.trans && userScale.trans === 'log') {
498                 max = max * 2;
499                 min = min / 2;
500             }
501             else {
502                 max = max + 1;
503                 min = min - 1;
504             }
505         }
506 
507         // Keep time charts from getting in a bad state.
508         // They currently rely on us rendering an empty grid when there is an invalid x-axis.
509         if (isNaN(min) && isNaN(max)) {
510             return [0,0];
511         }
512 
513         return [min, max];
514     };
515 
516     var getDomain = function(aesName, userScale, scale, data, acc, errorAes) {
517         var tempDomain, curDomain = scale.domain, domain = [];
518 
519         if (scale.scaleType == 'discrete') {
520             tempDomain = getDiscreteAxisDomain(data, acc);
521 
522             if (scale.sortFn) {
523                 tempDomain.sort(scale.sortFn);
524             }
525 
526             if (!curDomain) {
527                 return tempDomain;
528             }
529 
530             for (var i = 0; i < tempDomain.length; i++) {
531                 if (curDomain.indexOf(tempDomain[i]) == -1) {
532                     curDomain.push(tempDomain[i]);
533                 }
534             }
535 
536             if (scale.sortFn) {
537                 curDomain.sort(scale.sortFn);
538             }
539 
540             return curDomain;
541         } else {
542             tempDomain = getContinuousDomain(aesName, userScale, data, acc, errorAes);
543             if (!curDomain) {
544                 return tempDomain;
545             }
546 
547             if (!LABKEY.vis.isValid(curDomain[0]) || tempDomain[0] < curDomain[0]) {
548                 domain[0] = tempDomain[0];
549             } else {
550                 domain[0] = curDomain[0];
551             }
552 
553             if (!LABKEY.vis.isValid(curDomain[1]) || tempDomain[1] > curDomain[1]) {
554                 domain[1] = tempDomain[1];
555             } else {
556                 domain[1] = curDomain[1];
557             }
558         }
559 
560         return domain;
561     };
562 
563     var requiresDomain = function(name, colorScale) {
564         if (name == 'yLeft' || name == 'yRight' || name == 'x' || name == 'xTop' || name == 'xSub' || name == 'size') {
565             return true;
566         }
567         // We only need the domain of the a color scale if it's a continuous one.
568         return (name == 'color' || name == 'outlierColor') && colorScale && colorScale.scaleType == 'continuous';
569     };
570 
571     var calculateDomains = function(userScales, scales, allAes, allData) {
572         var i, aesName, scale, userScale;
573         for (i = 0; i < allAes.length; i++) {
574             for (aesName in allAes[i]) {
575                 if (allAes[i].hasOwnProperty(aesName) && requiresDomain(aesName, scales.color)) {
576                     if (aesName == 'outlierColor') {
577                         scale = scales.color;
578                         userScale = userScales.color;
579                     } else {
580                         scale = scales[aesName];
581                         userScale = userScales[aesName];
582                     }
583 
584                     scale.domain = getDomain(aesName, userScale, scale, allData[i], allAes[i][aesName].getValue, allAes[i].error);
585                 }
586             }
587         }
588     };
589 
590     var getDefaultRange = function(scaleName, scale, userScale) {
591         if (scaleName == 'color' && scale.scaleType == 'continuous') {
592             return ['#222222', '#EEEEEE'];
593         }
594 
595         if (scaleName == 'color' && scale.scaleType == 'discrete') {
596             var colorType = (userScale ? userScale.colorType : null) || scale.colorType;
597             if (LABKEY.vis.Scale[colorType])
598                 return LABKEY.vis.Scale[colorType]();
599             else
600                 return LABKEY.vis.Scale.ColorDiscrete();
601         }
602 
603         if (scaleName == 'shape') {
604             return LABKEY.vis.Scale.Shape();
605         }
606 
607         if (scaleName == 'size') {
608             return [1, 5];
609         }
610 
611         return null;
612     };
613 
614     var calculateAxisScaleRanges = function(scales, grid, margins) {
615         var yRange = [grid.bottomEdge, grid.topEdge];
616 
617         if (scales.yRight) {
618             scales.yRight.range = yRange;
619         }
620 
621         if (scales.yLeft) {
622             scales.yLeft.range = yRange;
623         }
624 
625         var setXAxisRange = function(scale) {
626             if (scale.scaleType == 'continuous') {
627                 scale.range = [margins.left, grid.width - margins.right];
628             }
629             else {
630                 // We don't need extra padding in the discrete case because we use rangeBands which take care of that.
631                 scale.range = [grid.leftEdge, grid.rightEdge];
632             }
633         };
634 
635         if (scales.x) {
636             setXAxisRange(scales.x);
637         }
638 
639         if (scales.xTop) {
640             setXAxisRange(scales.xTop);
641         }
642 
643         if (scales.xSub) {
644             setXAxisRange(scales.xSub);
645         }
646     };
647 
648     var getLogScale = function (domain, range, minPositiveValue) {
649         var scale, scaleWrapper, increment = 0;
650 
651         // Issue 24727: adjust domain range to compensate for log scale fitting error margin
652         // With log scale, log transformation is applied before the mapping (fitting) to result range
653         // Javascript has binary floating points calculation issues. Use a small error constant to compensate.
654         var scaleRoundingEpsilon = 0.0001 * 0.5; // divide by half so that <= 0 value can be distinguashed from > 0 value
655 
656         if (minPositiveValue) {
657             scaleRoundingEpsilon = minPositiveValue * getLogDomainLowerBoundRatio(domain, range, minPositiveValue);
658         }
659 
660         // domain must not include or cross zero
661         if (domain[0] <= scaleRoundingEpsilon) {
662             // Issue 24967: incrementing domain is causing issue with brushing extent
663             // Ideally we'd increment as little as possible
664             increment = scaleRoundingEpsilon - domain[0];
665             domain[0] = domain[0] + increment;
666             domain[1] = domain[1] + increment;
667         }
668         else {
669             domain[0] = domain[0] - scaleRoundingEpsilon;
670             domain[1] = domain[1] + scaleRoundingEpsilon;
671         }
672 
673         scale = d3.scale.log().domain(domain).range(range);
674 
675         scaleWrapper = function(val) {
676             if(val != null) {
677                 if (increment > 0 && val <= scaleRoundingEpsilon) {
678                     // <= 0 points are now part of the main plot, it's illegal to pass negative value to a log scale with positive domain.
679                     // Since we don't care about the relative values of those gutter data, we can use domain's lower bound for all <=0 as a mock
680                     return scale(scaleRoundingEpsilon);
681                 }
682                 // use the original value to calculate the scaled value for all > 0 data
683                 return scale(val);
684             }
685 
686             return null;
687         };
688         scaleWrapper.domain = scale.domain;
689         scaleWrapper.range = scale.range;
690         scaleWrapper.invert = scale.invert;
691         scaleWrapper.base = scale.base;
692         scaleWrapper.clamp = scale.clamp;
693         scaleWrapper.ticks = function(){
694             var allTicks = scale.ticks();
695             var ticksToShow = [];
696 
697             if (allTicks.length < 2) {
698                 //make sure that at least 2 tick marks are shown for reference
699                 // skip rounding down if rounds down to 0, which is not allowed for log
700                 return [Math.ceil(scale.domain()[0]), Math.abs(scale.domain()[1]) < 1 ? scale.domain()[1] : Math.floor(scale.domain()[1])];
701             }
702             else if(allTicks.length < 10){
703                 return allTicks;
704             }
705             else {
706                 for(var i = 0; i < allTicks.length; i++){
707                     if(i % 9 == 0){
708                         ticksToShow.push(allTicks[i]);
709                     }
710                 }
711                 return ticksToShow;
712             }
713         };
714 
715         return scaleWrapper;
716     };
717 
718     // The lower domain bound need to adjusted to so that enough space is reserved for log gutter.
719     // The calculation takes into account the available plot size (range), max and min values (domain) in the plot.
720     var getLogDomainLowerBoundRatio = function(domain, range, minPositiveValue) {
721         // use 0.5 as a base ratio, so that plot distributions on edge grids are not skewed
722         var ratio = 0.5, logGutterSize = 30;
723         var gridNum = Math.ceil(Math.log(domain[1] / minPositiveValue)); // the number of axis ticks, equals order of magnitude diff of positive domain range
724         var rangeOrder = Math.floor(Math.abs(range[1] - range[0]) / logGutterSize) + 1; // calculate max allowed grid number, assuming each grid is at least log gutter size
725 
726         if (gridNum > rangeOrder) {
727             for (var i = 0; i < gridNum - rangeOrder; i++) {
728                 ratio *= 0.5;
729             }
730         }
731         else{
732             var gridSize = Math.abs(range[1] - range[0]) / gridNum; // the actual grid size of each grid
733 
734             // adjust ratio so that positive data won't fall into log gutter area
735             if (gridSize/2 < logGutterSize){
736                 ratio = 1 - logGutterSize/gridSize;
737             }
738         }
739         return ratio;
740     };
741 
742     var instantiateScales = function(plot, margins) {
743         var userScales = plot.originalScales, scales = plot.scales, grid = plot.grid, isMainPlot = plot.isMainPlot,
744             xLogGutter = plot.xLogGutter, yLogGutter = plot.yLogGutter, minXPositiveValue = plot.minXPositiveValue, minYPositiveValue = plot.minYPositiveValue;
745 
746         var scaleName, scale, userScale;
747 
748         calculateAxisScaleRanges(scales, grid, margins);
749 
750         if (isMainPlot) {
751             // adjust the plot range to reserve space for log gutter
752             var mainPlotRangeAdjustment = 30;
753             if (xLogGutter) {
754                 if (scales.yLeft && Ext.isArray(scales.yLeft.range)) {
755                     scales.yLeft.range = [scales.yLeft.range[0] + mainPlotRangeAdjustment, scales.yLeft.range[1]];
756                 }
757             }
758             if (yLogGutter) {
759                 if (scales.x && Ext.isArray(scales.x.range)) {
760                     scales.x.range = [scales.x.range[0] - mainPlotRangeAdjustment, scales.x.range[1]];
761                 }
762             }
763         }
764 
765         for (scaleName in scales) {
766             if (scales.hasOwnProperty(scaleName)) {
767                 scale = scales[scaleName];
768                 userScale = userScales[scaleName];
769 
770                 if (scale.scaleType == 'discrete') {
771                     if (scaleName == 'x' || scaleName == 'xTop' || scaleName == 'xSub' || scaleName == 'yLeft' || scaleName == 'yRight'){
772                         // Setup scale with domain (user provided or calculated) and compute range based off grid dimensions.
773                         scale.scale = d3.scale.ordinal().domain(scale.domain).rangeBands(scale.range, 1);
774                     } else {
775                         // Setup scales with user provided range or default range.
776                         if (userScale && userScale.scale) {
777                             scale.scale = userScale.scale;
778                         }
779                         else {
780                             scale.scale = d3.scale.ordinal();
781                         }
782 
783                         if (scale.domain) {
784                             scale.scale.domain(scale.domain);
785                         }
786 
787                         if (!scale.range) {
788                             scale.range = getDefaultRange(scaleName, scale, userScale);
789                         }
790 
791                         if (scale.scale.range) {
792                             scale.scale.range(scale.range);
793                         }
794                     }
795                 } else {
796                     if ((scaleName == 'color' || scaleName == 'size') && !scale.range) {
797                         scale.range = getDefaultRange(scaleName, scale, userScale);
798                     }
799 
800                     if (scale.range && scale.domain && LABKEY.vis.isValid(scale.domain[0]) && LABKEY.vis.isValid(scale.domain[1])) {
801                         if (scale.trans == 'linear') {
802                             scale.scale = d3.scale.linear().domain(scale.domain).range(scale.range);
803                         } else {
804                             if (scaleName == 'x' || scaleName == 'xTop') {
805                                 scale.scale = getLogScale(scale.domain, scale.range, minXPositiveValue);
806                             }
807                             else {
808                                 scale.scale = getLogScale(scale.domain, scale.range, minYPositiveValue);
809                             }
810                         }
811                     }
812                 }
813             }
814         }
815     };
816 
817     var initializeScales = function(plot, allAes, allData, margins, errorFn) {
818         var userScales = plot.originalScales, scales = plot.scales;
819 
820         for (var i = 0; i < allAes.length; i++) {
821             setupDefaultScales(scales, allAes[i]);
822         }
823 
824         for (var scaleName in scales) {
825             if(scales.hasOwnProperty(scaleName)) {
826                 if (scales[scaleName].scale) {
827                     delete scales[scaleName].scale;
828                 }
829 
830                 if (scales[scaleName].domain && (userScales[scaleName] && !userScales[scaleName].domain)) {
831                     delete scales[scaleName].domain;
832                 }
833 
834                 if (scales[scaleName].range && (userScales[scaleName] && !userScales[scaleName].range)) {
835                     delete scales[scaleName].range;
836                 }
837             }
838         }
839 
840         calculateDomains(userScales, scales, allAes, allData);
841         instantiateScales(plot, margins);
842 
843         if ((scales.x && !scales.x.scale) || (scales.xTop && !scales.xTop.scale)) {
844             errorFn('Unable to create an x scale, rendering aborted.');
845             return false;
846         }
847 
848         if((!scales.yLeft || !scales.yLeft.scale) && (!scales.yRight ||!scales.yRight.scale)){
849             errorFn('Unable to create a y scale, rendering aborted.');
850             return false;
851         }
852 
853         return true;
854     };
855 
856     var compareDomains  = function(domain1, domain2){
857         if(domain1.length != domain2.length){
858             return false;
859         }
860 
861         domain1.sort();
862         domain2.sort();
863 
864         for(var i = 0; i < domain1.length; i++){
865             if(domain1[i] != domain2[i]) {
866                 return false;
867             }
868         }
869 
870         return true;
871     };
872 
873     var generateLegendData = function(legendData, domain, colorFn, shapeFn){
874         for(var i = 0; i < domain.length; i++) {
875             legendData.push({
876                 text: domain[i],
877                 color: colorFn != null ? colorFn(domain[i]) : null,
878                 shape: shapeFn != null ? shapeFn(domain[i]) : null
879             });
880         }
881     };
882 
883     LABKEY.vis.Plot = function(config){
884         if(config.hasOwnProperty('rendererType') && config.rendererType == 'd3') {
885             this.yLogGutter = config.requireYLogGutter ? true : false;
886             this.xLogGutter = config.requireXLogGutter ? true : false;
887             this.isMainPlot = config.isMainPlot ? true : false;
888             this.isShowYAxisGutter = config.isShowYAxis ? true : false;
889             this.isShowXAxisGutter = config.isShowXAxis ? true : false;
890             this.minXPositiveValue = config.minXPositiveValue;
891             this.minYPositiveValue = config.minYPositiveValue;
892 
893             this.renderer = new LABKEY.vis.internal.D3Renderer(this);
894         } else {
895             this.renderer = new LABKEY.vis.internal.RaphaelRenderer(this);
896         }
897 
898         var error = function(msg){
899             if (this.throwErrors){
900                 throw new Error(msg);
901             } else {
902                 console.error(msg);
903                 if(console.trace){
904                     console.trace();
905                 }
906 
907                 this.renderer.renderError(msg);
908             }
909         };
910 
911         this.renderTo = config.renderTo ? config.renderTo : null; // The id of the DOM element to render the plot to, required.
912         this.grid = {
913             width: config.hasOwnProperty('width') ? config.width : null, // height of the grid where shapes/lines/etc gets plotted.
914             height: config.hasOwnProperty('height') ? config.height: null // widht of the grid.
915         };
916         this.originalScales = config.scales ? config.scales : {}; // The scales specified by the user.
917         this.scales = copyUserScales(this.originalScales); // The scales used internally.
918         this.originalAes = config.aes ? config.aes : null; // The original aesthetic specified by the user.
919         this.aes = LABKEY.vis.convertAes(this.originalAes); // The aesthetic object used internally.
920         this.labels = config.labels ? config.labels : {};
921         this.data = config.data ? config.data : null; // An array of rows, required. Each row could have several pieces of data. (e.g. {subjectId: '249534596', hemoglobin: '350', CD4:'1400', day:'120'})
922         this.layers = config.layers ? config.layers : []; // An array of layers, required. (e.g. a layer for a CD4 line chart over time, and a layer for a Hemoglobin line chart over time).
923         this.clipRect = config.clipRect ? config.clipRect : false;
924         this.legendPos = config.legendPos;
925         this.legendNoWrap = config.legendNoWrap;
926         this.throwErrors = config.throwErrors || false; // Allows the configuration to specify whether chart errors should be thrown or logged (default).
927         this.brushing = ('brushing' in config && config.brushing != null && config.brushing != undefined) ? config.brushing : null;
928         this.legendData = config.legendData ? config.legendData : null; // An array of rows for the legend text/color/etc. Optional.
929         this.disableAxis = config.disableAxis ? config.disableAxis : {xTop: false, xBottom: false, yLeft: false, yRight: false};
930         this.bgColor = config.bgColor ? config.bgColor : null;
931         this.gridColor = config.gridColor ? config.gridColor : null;
932         this.gridLineColor = config.gridLineColor ? config.gridLineColor : null;
933         this.gridLinesVisible = config.gridLinesVisible ? config.gridLinesVisible : null;
934         this.fontFamily = config.fontFamily ? config.fontFamily : null;
935         this.tickColor = config.tickColor ? config.tickColor : null;
936         this.borderColor = config.borderColor ? config.borderColor : null;
937         this.tickTextColor = config.tickTextColor ? config.tickTextColor : null;
938         this.tickLength = config.hasOwnProperty('tickLength') ? config.tickLength : null;
939         this.tickWidth = config.hasOwnProperty('tickWidth') ? config.tickWidth : null;
940         this.tickOverlapRotation = config.hasOwnProperty('tickOverlapRotation') ? config.tickOverlapRotation : null;
941         this.gridLineWidth = config.hasOwnProperty('gridLineWidth') ? config.gridLineWidth : null;
942         this.borderWidth = config.hasOwnProperty('borderWidth') ? config.borderWidth : null;
943 
944         // Stash the user's margins so when we re-configure margins during re-renders or setAes we don't forget the user's settings.
945         var allAes = [], margins = {}, userMargins = config.margins ? config.margins : {};
946 
947         allAes.push(this.aes);
948 
949         for(var i = 0; i < this.layers.length; i++){
950             if(this.layers[i].aes){
951                 allAes.push(this.layers[i].aes);
952             }
953         }
954 
955         if(this.labels.y){
956             this.labels.yLeft = this.labels.y;
957             this.labels.y = null;
958         }
959 
960         if(this.grid.width == null){
961             error.call(this, "Unable to create plot, width not specified");
962             return;
963         }
964 
965         if(this.grid.height == null){
966             error.call(this, "Unable to create plot, height not specified");
967             return;
968         }
969 
970         if(this.renderTo == null){
971             error.call(this, "Unable to create plot, renderTo not specified");
972             return;
973         }
974 
975         for(var aesthetic in this.aes){
976             if (this.aes.hasOwnProperty(aesthetic)) {
977                 LABKEY.vis.createGetter(this.aes[aesthetic]);
978             }
979         }
980 
981         this.getLegendData = function(){
982             var legendData = [];
983 
984             if ((this.scales.color && this.scales.color.scaleType === 'discrete') && this.scales.shape) {
985                 if(compareDomains(this.scales.color.scale.domain(), this.scales.shape.scale.domain())){
986                     // The color and shape domains are the same. Merge them in the legend.
987                     generateLegendData(legendData, this.scales.color.scale.domain(), this.scales.color.scale, this.scales.shape.scale);
988                 } else {
989                     // The color and shape domains are different.
990                     generateLegendData(legendData, this.scales.color.scale.domain(), this.scales.color.scale, null);
991                     generateLegendData(legendData, this.scales.shape.scale.domain(), null, this.scales.shape.scale);
992                 }
993             } else if(this.scales.color && this.scales.color.scaleType === 'discrete') {
994                 generateLegendData(legendData, this.scales.color.scale.domain(), this.scales.color.scale, null);
995             } else if(this.scales.shape) {
996                 generateLegendData(legendData, this.scales.shape.scale.domain(), null, this.scales.shape.scale);
997             }
998 
999             return legendData;
1000         };
1001 
1002         /**
1003          * Renders the plot.
1004          */
1005         this.render = function(){
1006             margins = initMargins(userMargins, this.legendPos, allAes, this.scales, this.labels);
1007             this.grid = initGridDimensions(this.grid, margins);
1008             this.renderer.initCanvas(); // Get the canvas prepped for render time.
1009             var allData = [this.data];
1010             var plot = this;
1011             var errorFn = function(msg) {
1012                 error.call(plot, msg);
1013             };
1014             allAes = [this.aes];
1015             for (var i = 0; i < this.layers.length; i++) {
1016                 // If the layer doesn't have data or aes, then it doesn't need to be considered for any scale calculations.
1017                 if (!this.layers[i].data && !this.layers[i].aes) {continue;}
1018                 allData.push(this.layers[i].data ? this.layers[i].data : this.data);
1019                 allAes.push(this.layers[i].aes ? this.layers[i].aes : this.aes);
1020             }
1021 
1022             if(!initializeScales(this, allAes, allData, margins, errorFn)){  // Sets up the scales.
1023                 return false; // if we have a critical error when trying to initialize the scales we don't continue with rendering.
1024             }
1025 
1026             if(!this.layers || this.layers.length < 1){
1027                 error.call(this,'No layers added to the plot, nothing to render.');
1028                 return false;
1029             }
1030 
1031             this.renderer.renderGrid(); // renders the grid (axes, grid lines).
1032             this.renderer.renderLabels();
1033 
1034             for(var i = 0; i < this.layers.length; i++){
1035                 this.layers[i].plot = this; // Add reference to the layer so it can trigger a re-render during setAes.
1036                 this.layers[i].render(this.renderer, this.grid, this.scales, this.data, this.aes, i);
1037             }
1038 
1039             if(!this.legendPos || (this.legendPos && !(this.legendPos == "none"))){
1040                 this.renderer.renderLegend();
1041             }
1042 
1043             return true;
1044         };
1045 
1046         var setLabel = function(name, value, lookClickable){
1047             if(!this.labels[name]){
1048                 this.labels[name] = {};
1049             }
1050 
1051             this.labels[name].value = value;
1052             this.labels[name].lookClickable = lookClickable;
1053             this.renderer.renderLabel(name);
1054         };
1055 
1056         /**
1057          * Sets the value of the main label and optionally makes it look clickable.
1058          * @param {String} value The string value to set the label to.
1059          * @param {Boolean} lookClickable If true it styles the label to look clickable.
1060          */
1061         this.setMainLabel = function(value, lookClickable){
1062             setLabel.call(this, 'main', value, lookClickable);
1063         };
1064 
1065         /**
1066          * Sets the value of the x-axis label and optionally makes it look clickable.
1067          * @param {String} value The string value to set the label to.
1068          * @param {Boolean} lookClickable If true it styles the label to look clickable.
1069          */
1070         this.setXLabel = function(value, lookClickable){
1071             setLabel.call(this, 'x', value, lookClickable);
1072         };
1073 
1074         /**
1075          * Sets the value of the right y-axis label and optionally makes it look clickable.
1076          * @param {String} value The string value to set the label to.
1077          * @param {Boolean} lookClickable If true it styles the label to look clickable.
1078          */
1079         this.setYRightLabel = function(value, lookClickable){
1080             setLabel.call(this, 'yRight', value, lookClickable);
1081         };
1082 
1083         /**
1084          * Sets the value of the left y-axis label and optionally makes it look clickable.
1085          * @param {String} value The string value to set the label to.
1086          * @param {Boolean} lookClickable If true it styles the label to look clickable.
1087          */
1088         this.setYLeftLabel = this.setYLabel = function(value, lookClickable){
1089             setLabel.call(this, 'yLeft', value, lookClickable);
1090         };
1091 
1092         /**
1093          * Adds a listener to a label.
1094          * @param {String} label string value of label to add a listener to. Valid values are y, yLeft, yRight, x, and main.
1095          * @param {String} listener the name of the listener to listen on.
1096          * @param {Function} fn The callback to b called when the event is fired.
1097          */
1098         this.addLabelListener = function(label, listener, fn){
1099             if(label == 'y') {
1100                 label = 'yLeft';
1101             }
1102             return this.renderer.addLabelListener(label, listener, fn);
1103         };
1104 
1105         /**
1106          * Sets the height of the plot and re-renders if requested.
1107          * @param {Number} h The height in pixels.
1108          * @param {Boolean} render Toggles if plot will be re-rendered or not.
1109          */
1110         this.setHeight = function(h, render){
1111             if(render == null || render == undefined){
1112                 render = true;
1113             }
1114 
1115             this.grid.height = h;
1116 
1117             if(render === true){
1118                 this.render();
1119             }
1120         };
1121 
1122         this.getHeight = function(){
1123             return this.grid.height;
1124         };
1125 
1126         /**
1127          * Sets the width of the plot and re-renders if requested.
1128          * @param {Number} w The width in pixels.
1129          * @param {Boolean} render Toggles if plot will be re-rendered or not.
1130          */
1131         this.setWidth = function(w, render){
1132             if(render == null || render == undefined){
1133                 render = true;
1134             }
1135 
1136             this.grid.width = w;
1137 
1138             if(render === true){
1139                 this.render();
1140             }
1141         };
1142 
1143         this.getWidth = function(){
1144             return this.grid.width;
1145         };
1146 
1147         /**
1148          * Changes the size of the plot and renders if requested.
1149          * @param {Number} w width in pixels.
1150          * @param {Number} h height in pixels.
1151          * @param {Boolean} render Toggles if the chart will be re-rendered or not. Defaults to false.
1152          */
1153         this.setSize = function(w, h, render){
1154             this.setWidth(w, false);
1155             this.setHeight(h, render);
1156         };
1157 
1158         /**
1159          * Adds a new layer to the plot.
1160          * @param {@link LABKEY.vis.Layer} layer
1161          */
1162         this.addLayer = function(layer){
1163             layer.parent = this; // Set the parent of each layer to the plot so we can grab things like data from it later.
1164             this.layers.push(layer);
1165         };
1166 
1167         /**
1168          * Replaces an existing layer with a new layer. Does not render the plot. Returns the new layer.
1169          * @param {@link LABKEY.vis.Layer} oldLayer
1170          * @param {@link LABKEY.vis.Layer} newLayer
1171          */
1172         this.replaceLayer = function(oldLayer, newLayer){
1173             var index = this.layers.indexOf(oldLayer);
1174             if (index == -1) {
1175                 this.layers.push(newLayer);
1176             }
1177             else {
1178                 this.layers.splice(index, 1, newLayer);
1179             }
1180             return this.layers;
1181         };
1182 
1183         /**
1184          * Clears the grid.
1185          */
1186         this.clearGrid = function(){
1187             this.renderer.clearGrid();
1188         };
1189 
1190         /**
1191          * Sets new margins for the plot and re-renders with the margins.
1192          * @param {Object} newMargins An object with the following properties:
1193          *      <ul>
1194          *          <li><strong>top:</strong> Size of top margin in pixels.</li>
1195          *          <li><strong>bottom:</strong> Size of bottom margin in pixels.</li>
1196          *          <li><strong>left:</strong> Size of left margin in pixels.</li>
1197          *          <li><strong>right:</strong> Size of right margin in pixels.</li>
1198          *      </ul>
1199          */
1200         this.setMargins = function(newMargins, render){
1201             userMargins = newMargins;
1202             margins = initMargins(userMargins, this.legendPos, allAes, this.scales, this.labels);
1203 
1204             if(render !== undefined && render !== null && render === true) {
1205                 this.render();
1206             }
1207         };
1208 
1209         this.setAes = function(newAes){
1210             // Note: this is only valid for plots using the D3Renderer.
1211             // Used to add or remove aesthetics to a plot. Also availalbe on LABKEY.vis.Layer objects to set aesthetics on
1212             // specific layers only.
1213             // To delete an aesthetic set it to null i.e. plot.setAes({color: null});
1214             LABKEY.vis.mergeAes(this.aes, newAes);
1215             this.render();
1216         };
1217 
1218         /**
1219          * Sets and updates the legend with new legend data.
1220          * @param (Array) Legend data
1221          */
1222         this.setLegend = function(newLegend){
1223             this.renderer.setLegendData(newLegend);
1224             this.renderer.renderLegend();
1225         };
1226 
1227         this.setBrushing = function(configBrushing) {
1228             this.renderer.setBrushing(configBrushing);
1229         };
1230 
1231         this.clearBrush = function() {
1232             if(this.renderer.clearBrush) {
1233                 this.renderer.clearBrush();
1234             }
1235         };
1236 
1237         this.getBrushExtent = function() {
1238             // Returns an array of arrays. First array is xMin, yMin, second array is xMax, yMax
1239             // If the seleciton is 1D, then the min/max of the non-selected dimension will be null/null.
1240             return this.renderer.getBrushExtent();
1241         };
1242 
1243         this.setBrushExtent = function(extent) {
1244             // Takes a 2D array. First array is xMin, yMin, second array is xMax, yMax. If the seleciton is 1D, then the
1245             // min/max of the non-selected dimension will be null/null.
1246             this.renderer.setBrushExtent(extent);
1247         };
1248 
1249         this.bindBrushing = function(otherPlots) {
1250             this.renderer.bindBrushing(otherPlots);
1251         };
1252 
1253         return this;
1254     };
1255 })();
1256 
1257 /**
1258  * @name LABKEY.vis.BarPlot
1259  * @class BarPlot wrapper to allow a user to easily create a simple bar plot without having to preprocess the data.
1260  * @param {Object} config An object that contains the following properties (in addition to those properties defined
1261  *      in {@link LABKEY.vis.Plot}).
1262  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
1263  * @param {Array} [config.data] The array of individual data points to be grouped for the bar plot. The LABKEY.vis.BarPlot
1264  *      wrapper will aggregate the data in this array based on the xAes function provided to get the individual totals
1265  *      for each bar in the plot.
1266  * @param {Function} [config.xAes] The function to determine which groups will be created for the x-axis of the plot.
1267  * @param {Object} [config.options] (Optional) Display options as defined in {@link LABKEY.vis.Geom.BarPlot}.
1268  *
1269  @example
1270  <div id='bar'></div>
1271  <script type="text/javascript">
1272  // Fake data which will be aggregated by the LABKEY.vis.BarPlot wrapper.
1273  var barPlotData = [
1274     {gender: 'Male', age: '21'}, {gender: 'Male', age: '43'},
1275     {gender: 'Male', age: '24'}, {gender: 'Male', age: '54'},
1276     {gender: 'Female', age: '24'}, {gender: 'Female', age: '33'},
1277     {gender: 'Female', age: '43'}, {gender: 'Female', age: '43'},
1278  ];
1279 
1280  // Create a new bar plot object.
1281  var barChart = new LABKEY.vis.BarPlot({
1282     renderTo: 'bar',
1283     rendererType: 'd3',
1284     width: 900,
1285     height: 300,
1286     labels: {
1287         main: {value: 'Example Bar Plot With Cumulative Totals'},
1288         yLeft: {value: 'Count'},
1289         x: {value: 'Value'}
1290     },
1291     options: {
1292         color: 'black',
1293         fill: '#c0c0c0',
1294         lineWidth: 1.5,
1295         colorTotal: 'black',
1296         fillTotal: 'steelblue',
1297         opacityTotal: .8,
1298         showCumulativeTotals: true,
1299         showValues: true
1300     },
1301     xAes: function(row){return row['age']},
1302     data: barPlotData
1303  });
1304 
1305  barChart.render();
1306  </script>
1307  */
1308 (function(){
1309 
1310     LABKEY.vis.BarPlot = function(config){
1311 
1312         if(config.renderTo == null){
1313             throw new Error("Unable to create bar plot, renderTo not specified");
1314         }
1315         if(config.data == null){
1316             throw new Error("Unable to create bar plot, data array not specified");
1317         }
1318         if (!config.aes) {
1319             config.aes = {};
1320         }
1321         if (config.xAes) { //backwards compatibility
1322             config.aes.x = config.xAes;
1323         }
1324         if (!config.aes.x) {
1325             throw new Error("Unable to create bar plot, x Aesthetic not specified");
1326         }
1327         if (config.xSubAes) {
1328             config.aes.xSub = config.xSubAes;
1329         }
1330         if (config.yAes) {
1331             config.aes.y = config.yAes;
1332         }
1333         if (!config.options) {
1334             config.options = {};
1335         }
1336         if (!config.aggregateType) {
1337             config.options.aggregateType = config.aes.y ? 'SUM' : 'COUNT'; //aggregate defaults
1338         }
1339 
1340         var showCumulativeTotals = config.options && config.options.showCumulativeTotals;
1341         if (showCumulativeTotals && config.aes.xSub) {
1342             throw new Error("Unable to render grouped bar chart with cumulative totals shown");
1343         }
1344 
1345         var aggregateData,
1346                 dimName = config.aes.x,
1347                 subDimName = config.aes.xSub,
1348                 aggType = config.options.aggregateType,
1349                 measureName = config.aes.y,
1350                 includeTotal = config.options.showCumulativeTotals;
1351 
1352         aggregateData = LABKEY.vis.getAggregateData(config.data, dimName, subDimName, measureName, aggType, '[blank]', includeTotal);
1353         config.aes.y = 'value';
1354         config.aes.x = 'label';
1355         if (subDimName) {
1356             config.aes.xSub = 'subLabel';
1357             config.aes.color = 'label';
1358         }
1359 
1360         config.layers = [new LABKEY.vis.Layer({
1361             geom: new LABKEY.vis.Geom.BarPlot(config.options),
1362             data: aggregateData,
1363             aes: config.aes
1364         })];
1365 
1366         if (!config.scales) {
1367             config.scales = {};
1368         }
1369         if (!config.scales.x) {
1370             config.scales.x = { scaleType: 'discrete' };
1371         }
1372         if (subDimName && !config.scales.xSub) {
1373             config.scales.xSub = { scaleType: 'discrete' };
1374         }
1375         if (!config.scales.y) {
1376             var domainMax = aggregateData.length == 0 ? 1 : null;
1377             if (showCumulativeTotals)
1378                 domainMax = aggregateData[aggregateData.length-1].total;
1379 
1380             config.scales.y = {
1381                 scaleType: 'continuous',
1382                 domain: [0, domainMax]
1383             };
1384         }
1385 
1386         if (showCumulativeTotals && !config.margins)
1387         {
1388             config.margins = {right: 125};
1389         }
1390 
1391         return new LABKEY.vis.Plot(config);
1392     };
1393 })();
1394 
1395 /**
1396  * @name LABKEY.vis.PieChart
1397  * @class PieChart which allows a user to programmatically create an interactive pie chart visualization (note: D3 rendering only).
1398  * @description The pie chart visualization is built off of the <a href="http://d3pie.org">d3pie JS library</a>. The config
1399  *      properties listed below are the only required properties to create a base pie chart. For additional display options
1400  *      and interaction options, you can provide any of the properties defined in the <a href="http://d3pie.org/#docs">d3pie docs</a>
1401  *      to the config object.
1402  * @param {Object} config An object that contains the following properties
1403  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
1404  * @param {Array} [config.data] The array of chart segment data. Each object is of the form: { label: "label", value: 123 }.
1405  * @param {Number} [config.width] The chart canvas width in pixels.
1406  * @param {Number} [config.height] The chart canvas height in pixels.
1407  *
1408  @example
1409  Example of a simple pie chart (only required config properties).
1410 
1411  <div id='pie'></div>
1412  <script type="text/javascript">
1413 
1414  </script>
1415  var pieChartData = [
1416      {label: "test1", value: 1},
1417      {label: "test2", value: 2},
1418      {label: "test3", value: 3},
1419      {label: "test4", value: 4}
1420  ];
1421 
1422  var pieChart = new LABKEY.vis.PieChart({
1423     renderTo: "pie",
1424     data: pieChartData,
1425     width: 300,
1426     height: 250
1427  });
1428 
1429  Example of a customized pie chart using some d3pie lib properties.
1430 
1431  <div id='pie2'></div>
1432  <script type="text/javascript">
1433  var pieChartData = [
1434      {label: "test1", value: 1},
1435      {label: "test2", value: 2},
1436      {label: "test3", value: 3},
1437      {label: "test4", value: 4}
1438  ];
1439 
1440  var pieChart2 = new LABKEY.vis.PieChart({
1441     renderTo: "pie2",
1442     data: pieChartData,
1443     width: 300,
1444     height: 250,
1445     // d3pie lib config properties
1446     header: {
1447         title: {
1448             text: 'Pie Chart Example'
1449         }
1450     },
1451     labels: {
1452         outer: {
1453             format: 'label-value2',
1454             pieDistance: 15
1455         },
1456         inner: {
1457             hideWhenLessThanPercentage: 10
1458         },
1459         lines: {
1460             style: 'straight',
1461             color: 'black'
1462         }
1463     },
1464     effects: {
1465         load: {
1466             speed: 2000
1467         },
1468         pullOutSegmentOnClick: {
1469             effect: 'linear',
1470             speed: '1000'
1471         },
1472         highlightLuminosity: -0.75
1473     },
1474     misc: {
1475         colors: {
1476             segments: LABKEY.vis.Scale.DarkColorDiscrete(),
1477             segmentStroke: '#a1a1a1'
1478         },
1479         gradient: {
1480             enabled: true,
1481             percentage: 60
1482         }
1483     },
1484     callbacks: {
1485         onload: function() {
1486             pieChart2.openSegment(3);
1487         }
1488     }
1489  });
1490  </script>
1491  */
1492 (function(){
1493 
1494     LABKEY.vis.PieChart = function(config){
1495 
1496         if(config.renderTo == null){
1497             throw new Error("Unable to create pie chart, renderTo not specified");
1498         }
1499 
1500         if(config.data == null){
1501             throw new Error("Unable to create pie chart, data not specified");
1502         }
1503         else if (Array.isArray(config.data)) {
1504             config.data = {content : config.data, sortOrder: 'value-desc'};
1505         }
1506 
1507         if(config.width == null && (config.size == null || config.size.canvasWidth == null)){
1508             throw new Error("Unable to create pie chart, width not specified");
1509         }
1510         else if(config.height == null && (config.size == null || config.size.canvasHeight == null)){
1511             throw new Error("Unable to create pie chart, height not specified");
1512         }
1513 
1514         if (config.size == null) {
1515             config.size = {}
1516         }
1517         config.size.canvasWidth = config.width || config.size.canvasWidth;
1518         config.size.canvasHeight = config.height || config.size.canvasHeight;
1519 
1520         // apply default font/colors/etc., it not explicitly set
1521         if (!config.header) config.header = {};
1522         if (!config.header.title) config.header.title = {};
1523         if (!config.header.title.font) config.header.title.font = 'Roboto, arial';
1524         if (!config.header.title.hasOwnProperty('fontSize')) config.header.title.fontSize = 18;
1525         if (!config.header.title.color) config.header.title.color = '#000000';
1526         if (!config.header.subtitle) config.header.subtitle = {};
1527         if (!config.header.subtitle.font) config.header.subtitle.font = 'Roboto, arial';
1528         if (!config.header.subtitle.hasOwnProperty('fontSize')) config.header.subtitle.fontSize = 16;
1529         if (!config.header.subtitle.color) config.header.subtitle.color = '#555555';
1530         if (!config.footer) config.footer = {};
1531         if (!config.footer.font) config.footer.font = 'Roboto, arial';
1532         if (!config.labels) config.labels = {};
1533         if (!config.labels.mainLabel) config.labels.mainLabel = {};
1534         if (!config.labels.mainLabel.font) config.labels.mainLabel.font = 'Roboto, arial';
1535         if (!config.labels.percentage) config.labels.percentage = {};
1536         if (!config.labels.percentage.font) config.labels.percentage.font = 'Roboto, arial';
1537         if (!config.labels.percentage.color) config.labels.percentage.color = '#DDDDDD';
1538         if (!config.labels.outer) config.labels.outer = {};
1539         if (!config.labels.outer.hasOwnProperty('pieDistance')) config.labels.outer.pieDistance = 10;
1540         if (!config.labels.inner) config.labels.inner = {};
1541         if (!config.labels.inner.format) config.labels.inner.format = 'percentage';
1542         if (!config.labels.inner.hasOwnProperty('hideWhenLessThanPercentage')) config.labels.inner.hideWhenLessThanPercentage = 10;
1543         if (!config.labels.lines) config.labels.lines = {};
1544         if (!config.labels.lines.style) config.labels.lines.style = 'straight';
1545         if (!config.labels.lines.color) config.labels.lines.color = '#222222';
1546         if (!config.misc) config.misc = {};
1547         if (!config.misc.colors) config.misc.colors = {};
1548         if (!config.misc.colors.segments) config.misc.colors.segments = LABKEY.vis.Scale.ColorDiscrete();
1549         if (!config.misc.colors.segmentStroke) config.misc.colors.segmentStroke = '#222222';
1550         if (!config.misc.gradient) config.misc.gradient = {};
1551         if (!config.misc.gradient.enabled) config.misc.gradient.enabled = false;
1552         if (!config.misc.gradient.hasOwnProperty('percentage')) config.misc.gradient.percentage = 95;
1553         if (!config.misc.gradient.color) config.misc.gradient.color = "#000000";
1554         if (!config.effects) config.effects = {};
1555         if (!config.effects.pullOutSegmentOnClick) config.effects.pullOutSegmentOnClick = {};
1556         if (!config.effects.pullOutSegmentOnClick.effect) config.effects.pullOutSegmentOnClick.effect = 'none';
1557         if (!config.tooltips) config.tooltips = {};
1558         if (!config.tooltips.type) config.tooltips.type = 'placeholder';
1559         if (!config.tooltips.string) config.tooltips.string = '{label}: {percentage}%';
1560         if (!config.tooltips.styles) config.tooltips.styles = {backgroundOpacity: 1};
1561 
1562         return new d3pie(config.renderTo, config);
1563     };
1564 })();
1565 
1566 /**
1567  * @name LABKEY.vis.TrendingLinePlot
1568  * @class TrendingLinePlot Wrapper to create a plot which shows data points compared to expected ranges
1569  *                          For LeveyJennings, the range is +/- 3 standard deviations from a mean.
1570  *                          For MovingRange, the range is [0, 3.268*mean(mR)].
1571  *                          For CUSUM, the range is [0, +5].
1572  * @description This helper will take the input data and generate a sequencial x-axis so that all data points are the same distance apart.
1573  * @param {Object} config An object that contains the following properties
1574  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
1575  * @param {String} [config.qcPlotType] Specifies the plot type to be one of "LeveyJennings", "CUSUM", "MovingRange". Defaults to "LeveyJennings".
1576  * @param {Number} [config.width] The chart canvas width in pixels.
1577  * @param {Number} [config.height] The chart canvas height in pixels.
1578  * @param {Array} [config.data] The array of chart segment data.
1579  *                          For LeveyJennings and MovingRange, each object is of the form: { label: "label", value: 123 }.
1580  *                          For CUSUM, each object is of the form: { label: "label", value: 123, negative: true}.
1581  * @param {Number} [config.data.value]
1582  *                          For LeveyJennings, it's the raw value.
1583  *                          For MovingRange, the calculated rM value, not the raw value.
1584  *                          For CUSUM, the calculated CUSUM value, not the raw value.
1585  * @param {String} [config.data.negative] CUSUM plot only. True for CUSUM-, false for CUSUM+. Default false;
1586  * @param {Object} [config.properties] An object that contains the properties specific to the Levey-Jennings plot
1587  * @param {String} [config.properties.value] The data property name for the value to be plotted on the left y-axis.
1588  *                          Used by LeveyJennings.
1589  * @param {String} [config.properties.valueRight] The data property name for the value to be plotted on the right y-axis.
1590  *                          Used by LeveyJennings.
1591  * @param {String} [config.properties.mean] The data property name for the mean of the expected range.
1592  *                          Used by LeveyJennings.
1593  * @param {String} [config.properties.stdDev] The data property name for the standard deviation of the expected range.
1594  *                          Used by LeveyJennings only.
1595  * @param {String} [config.properties.valueConversion] The data property name for the conversion of the plot to either percent
1596  *                          of the mean ('percentDeviation') or standard deviations ('standardDeviation').
1597  *                          Used by LeveyJennings and Moving Range only.
1598  * @param {String} [config.properties.defaultGuideSets] The data property name for default std dev and mean needed for percentDeviation
1599  *                          or standardDeviation conversion.
1600  *                          Used by LeveyJennings and Moving Range only.
1601  * @param {String} [config.properties.valueMR] The data property name for the moving range value to be plotted on the left y-axis.
1602  *                          Used by MovingRange.
1603  * @param {String} [config.properties.valueRightMR] The data property name for the moving range to be plotted on the right y-axis.
1604  *                          Used by MovingRange.
1605  * @param {String} [config.properties.meanMR] The data property name for the mean of the moving range.
1606  *                          Used MovingRange.
1607  * @param {String} [config.properties.positiveValue] The data property name for the value to be plotted on the left y-axis for CUSUM+.
1608  *                          Used by CUSUM only.
1609  * @param {String} [config.properties.positiveValueRight] The data property name for the value to be plotted on the right y-axis for CUSUM+.
1610  *                          Used by CUSUM only.
1611  * @param {String} [config.properties.negativeValue] The data property name for the value to be plotted on the left y-axis for CUSUM-.
1612  *                          Used by CUSUM only.
1613  * @param {String} [config.properties.negativeValueRight] The data property name for the value to be plotted on the right y-axis for CUSUM-.
1614  *                          Used by CUSUM only.
1615  * @param {String} [config.properties.xTickLabel] The data property name for the x-axis tick label.
1616  * @param {Number} [config.properties.xTickTagIndex] (Optional) The index/value of the x-axis label to be tagged (i.e. class="xticktag").
1617  * @param {Boolean} [config.properties.showTrendLine] (Optional) Whether or not to show a line connecting the data points. Default false.
1618  * @param {Boolean} [config.properties.showDataPoints] (Optional) Whether or not to show the individual data points. Default true.
1619  * @param {Boolean} [config.properties.disableRangeDisplay] (Optional) Whether or not to show the control ranges in the plot. Defaults to false.
1620  *                          For LeveyJennings, the range is +/- 3 standard deviations from a mean.
1621  *                          For MovingRange, the range is [0, 3.268*mean(mR)].
1622  *                          For CUSUM, the range is [0, +5].
1623  * @param {String} [config.properties.xTick] (Optional) The data property to use for unique x-axis tick marks. Defaults to sequence from 1:data length.
1624  * @param {String} [config.properties.yAxisScale] (Optional) Whether the y-axis should be plotted with linear or log scale. Default linear.
1625  * @param {Array} [config.properties.yAxisDomain] (Optional) Y-axis min/max values. Example: [0,20].
1626  * @param {String} [config.properties.color] (Optional) The data property name for the color to be used for the data point.
1627  * @param {Array} [config.properties.colorRange] (Optional) The array of color values to use for the data points.
1628  * @param {Function} [config.properties.pointOpacityFn] (Optional) A function to be called with the point data to
1629  *                  return an opacity value for that point.
1630  * @param {String} [config.groupBy] (optional) The data property name used to group plot lines and points.
1631  * @param {Function} [config.properties.hoverTextFn] (Optional) The hover text to display for each data point. The parameter
1632  *                  to that function will be a row of data with access to all values for that row.
1633  * @param {Function} [config.properties.mouseOverFn] (Optional) The function to call on data point mouse over. The parameters to
1634  *                  that function will be the click event, the point data, the selection layer, and the DOM element for the point itself.
1635  * @param {Object} [config.properties.mouseOverFnScope] (Optional) The scope to use for the call to mouseOverFn.
1636  * @param {Function} [config.properties.pointClickFn] (Optional) The function to call on data point click. The parameters to
1637  *                  that function will be the click event and the row of data for the selected point.
1638  */
1639 (function(){
1640     LABKEY.vis.TrendingLinePlotType = {
1641         LeveyJennings : 'Levey-Jennings',
1642         CUSUM : 'CUSUM',
1643         MovingRange: 'MovingRange'
1644     };
1645 
1646     LABKEY.vis.TrendingLinePlot = function(config){
1647         if (!config.qcPlotType)
1648             config.qcPlotType = LABKEY.vis.TrendingLinePlotType.LeveyJennings;
1649         var plotTypeLabel = LABKEY.vis.TrendingLinePlotType[config.qcPlotType];
1650 
1651         if(config.renderTo == null) {
1652             throw new Error("Unable to create " + plotTypeLabel + " plot, renderTo not specified");
1653         }
1654 
1655         if(config.data == null) {
1656             throw new Error("Unable to create " + plotTypeLabel + " plot, data array not specified");
1657         }
1658 
1659         if (config.properties == null || config.properties.xTickLabel == null) {
1660             throw new Error("Unable to create " + plotTypeLabel + " plot, properties object not specified. ");
1661         }
1662 
1663         if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.LeveyJennings) {
1664             if (config.properties.value == null) {
1665                 throw new Error("Unable to create " + plotTypeLabel + " plot, value object not specified. "
1666                         + "Required: value, xTickLabel. Optional: mean, stdDev, color, colorRange, hoverTextFn, mouseOverFn, "
1667                         + "pointClickFn, showTrendLine, showDataPoints, disableRangeDisplay, xTick, yAxisScale, yAxisDomain, xTickTagIndex.");
1668             }
1669         }
1670         else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM) {
1671             if (config.properties.positiveValue == null || config.properties.negativeValue == null) {
1672                 throw new Error("Unable to create " + plotTypeLabel + " plot."
1673                         + "Required: positiveValue, negativeValue, xTickLabel. Optional: positiveValueRight, negativeValueRight, "
1674                         + "xTickTagIndex, showTrendLine, showDataPoints, disableRangeDisplay, xTick, yAxisScale, color, colorRange.");
1675             }
1676         }
1677         else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.MovingRange) {
1678             if (config.properties.valueMR == null) {
1679                 throw new Error("Unable to create " + plotTypeLabel + " plot, value object not specified. "
1680                         + "Required: value, xTickLabel. Optional: meanMR, color, colorRange, hoverTextFn, mouseOverFn, "
1681                         + "pointClickFn, showTrendLine, showDataPoints, disableRangeDisplay, xTick, yAxisScale, yAxisDomain, xTickTagIndex.");
1682             }
1683         }
1684         else {
1685             throw new Error(plotTypeLabel + " plot type is not supported!");
1686         }
1687 
1688         // get a sorted array of the unique x-axis labels
1689         var uniqueXAxisKeys = {}, uniqueXAxisLabels = [];
1690         for (var i = 0; i < config.data.length; i++) {
1691             if (!uniqueXAxisKeys[config.data[i][config.properties.xTick]]) {
1692                 uniqueXAxisKeys[config.data[i][config.properties.xTick]] = true;
1693             }
1694         }
1695         uniqueXAxisLabels =  Object.keys(uniqueXAxisKeys).sort();
1696 
1697         // create a sequencial index to use for the x-axis value and keep a map from that index to the tick label
1698         // also, pull out the meanStdDev data for the unique x-axis values and calculate average values for the (LJ) trend line data
1699         var tickLabelMap = {}, index = -1, distinctColorValues = [], meanStdDevData = [],
1700             groupedTrendlineData = [], groupedTrendlineSeriesData = {},
1701             hasYRightMetric = config.properties.valueRight || config.properties.positiveValueRight || config.properties.valueRightMR;
1702 
1703         var convertToPercentDeviation = function(value, mean) {
1704             var calc = Math.round(((value / mean) * 100) * 100) / 100;
1705             if (isNaN(calc))
1706                 return 100;
1707 
1708             return calc;
1709         };
1710 
1711         var convertToStandardDeviation = function(value, mean, stddev) {
1712             var calc = Math.round(((value - mean) / stddev) * 100) / 100;
1713             if (isNaN(calc))
1714                 return 0;
1715 
1716             return calc;
1717         };
1718 
1719         var convertValues = function(conversion) {
1720             if (!conversion)
1721                 return;
1722 
1723             // Needed for point hover
1724             row.conversion = conversion;
1725 
1726             // Convert values
1727             if (row[valProp] !== undefined) {
1728                 row.rawValue = row[valProp];
1729                 if (conversion === "percentDeviation") {
1730                     row[valProp] = convertToPercentDeviation(row[valProp], row[meanProp]);
1731                 }
1732                 else {
1733                     row[valProp] = convertToStandardDeviation(row[valProp], row[meanProp], row[sdProp]);
1734                 }
1735             }
1736             else if (row[valRightProp] !== undefined) {
1737                 row.rawValue = row[valRightProp];
1738                 if (conversion === "percentDeviation") {
1739                     row[valRightProp] = convertToPercentDeviation(row[valRightProp], row[meanProp]);
1740                 }
1741                 else {
1742                     row[valRightProp] = convertToStandardDeviation(row[valRightProp], row[meanProp], row[sdProp]);
1743                 }
1744             }
1745         };
1746 
1747         // Handles Y Axis domain when performing percent or standard deviation conversions
1748         var convertYAxisDomain = function (value, stddev, mean) {
1749             var maxValue, minValue;
1750             if (config.qcPlotType === LABKEY.vis.TrendingLinePlotType.MovingRange
1751                     && config.properties.valueConversion === 'percentDeviation') {
1752                 maxValue = mean * LABKEY.vis.Stat.MOVING_RANGE_UPPER_LIMIT_WEIGHT;
1753                 minValue = mean;
1754             }
1755 
1756             if (maxValue !== undefined && minValue !== undefined) {
1757 
1758                 if (minValue > maxValue) {
1759                     var tmp = minValue;
1760                     minValue = maxValue;
1761                     maxValue = tmp;
1762                 }
1763 
1764                 if (value > maxValue)
1765                     maxValue = value;
1766 
1767                 if (value < minValue)
1768                     minValue = value;
1769 
1770                 if (config.properties.yAxisDomain[0] > maxValue) {
1771                     config.properties.yAxisDomain[0] = maxValue;
1772                 }
1773 
1774                 if (config.properties.yAxisDomain[1] < maxValue) {
1775                     config.properties.yAxisDomain[1] = maxValue;
1776                 }
1777 
1778                 if (config.properties.yAxisDomain[0] > minValue) {
1779                     config.properties.yAxisDomain[0] = minValue;
1780                 }
1781 
1782                 if (config.properties.yAxisDomain[1] < minValue) {
1783                     config.properties.yAxisDomain[1] = minValue;
1784                 }
1785             }
1786             else {
1787                 if (config.properties.yAxisDomain[0] > value) {
1788                     config.properties.yAxisDomain[0] = value;
1789                 }
1790 
1791                 if (config.properties.yAxisDomain[1] < value) {
1792                     config.properties.yAxisDomain[1] = value;
1793                 }
1794             }
1795         };
1796 
1797         var rangeConverted = false;
1798 
1799         var meanProp, sdProp, valProp, valRightProp;
1800         if (config.qcPlotType === LABKEY.vis.TrendingLinePlotType.MovingRange) {
1801             meanProp = config.properties["meanMR"] || "meanMR";
1802             sdProp = "stddevMR";
1803             valProp = config.properties["valueMR"];
1804             valRightProp = config.properties["valueRightMR"]
1805         }
1806         else {
1807             meanProp = config.properties["mean"] || "mean";
1808             sdProp = config.properties["stdDev"] || "stdDev";
1809             valProp = config.properties["value"];
1810             valRightProp = config.properties["valueRight"];
1811         }
1812 
1813         for (var j = 0; j < config.data.length; j++) {
1814             var row = config.data[j];
1815             var seriesType = row["SeriesType"];
1816 
1817             // Set default mean and std dev
1818             if (row.type === "data" && config.qcPlotType !== LABKEY.vis.TrendingLinePlotType.CUSUM) {
1819                 if (((valProp && row[valProp] !== undefined) || (valRightProp && row[valRightProp] !== undefined))) {
1820 
1821                     // If mean or std dev not in row, use default values
1822                     if (config.properties.defaultGuideSets && config.properties.defaultGuideSetLabel) {
1823                         var defaultGuideSet = config.properties.defaultGuideSets[row[config.properties.defaultGuideSetLabel]];
1824 
1825                         if (defaultGuideSet && defaultGuideSet[seriesType]) {
1826                             if ((row[meanProp] === undefined || row[meanProp] === null)) {
1827                                 if (config.qcPlotType === LABKEY.vis.TrendingLinePlotType.MovingRange && defaultGuideSet[seriesType].MR) {
1828                                     row[meanProp] = defaultGuideSet[seriesType].MR.Mean;
1829                                 }
1830                                 else if (config.qcPlotType === LABKEY.vis.TrendingLinePlotType.LeveyJennings && defaultGuideSet[seriesType].LJ) {
1831                                     row[meanProp] = defaultGuideSet[seriesType].LJ.Mean;
1832                                 }
1833                             }
1834 
1835                             if (row[sdProp] === undefined || row[sdProp] === null) {
1836                                 row[sdProp] = config.properties.defaultStdDev;
1837                                 if (config.qcPlotType === LABKEY.vis.TrendingLinePlotType.MovingRange && defaultGuideSet[seriesType].MR) {
1838                                     row[sdProp] = defaultGuideSet[seriesType].MR.StdDev;
1839                                 }
1840                                 else if (config.qcPlotType === LABKEY.vis.TrendingLinePlotType.LeveyJennings && defaultGuideSet[seriesType].LJ) {
1841                                     row[sdProp] = defaultGuideSet[seriesType].LJ.StdDev;
1842                                 }
1843                             }
1844                         }
1845                     }
1846                 }
1847 
1848                 // Handle value conversions
1849                 convertValues(config.properties.valueConversion);
1850                 if (config.properties.valueConversion === 'percentDeviation') {
1851                     row[sdProp] = convertToPercentDeviation(row[sdProp], row[meanProp]);
1852                     row[meanProp] = 100;
1853                 }
1854                 else if (config.properties.valueConversion === 'standardDeviation') {
1855                     row[sdProp] = 1;
1856                     row[meanProp] = 0;
1857                 }
1858 
1859                 if (!config.properties.valueRight && !config.properties.valueRightMR) {
1860 
1861                     if (!config.properties.yAxisDomain) {
1862                         config.properties.yAxisDomain = [0, 0];
1863                     }
1864 
1865                     if (!rangeConverted) {
1866                         config.properties.yAxisDomain[0] = row[meanProp];
1867                         config.properties.yAxisDomain[1] = row[meanProp];
1868                         rangeConverted = true;
1869                     }
1870 
1871                     if (row[valProp] !== undefined) {
1872                         convertYAxisDomain(row[valProp], row[sdProp], row[meanProp]);
1873                     }
1874                     else if (row[valRightProp] !== undefined) {
1875                         convertYAxisDomain(row[valRightProp], row[sdProp], row[meanProp]);
1876                     }
1877                 }
1878             }
1879 
1880             // track the distinct values in the color variable so that we know if we need the legend or not
1881             if (config.properties.color && distinctColorValues.indexOf(row[config.properties.color]) === -1) {
1882                 distinctColorValues.push(row[config.properties.color]);
1883             }
1884 
1885             // if we are grouping x values based on the xTick property, only increment index if we have a new xTick value
1886             if (config.properties.xTick)
1887             {
1888                 var addValueToTrendLineData = function(dataArr, seqValue, arrKey, fieldName, rowValue, sumField, countField)
1889                 {
1890                     if (dataArr[arrKey] == undefined)
1891                     {
1892                         dataArr[arrKey] = {
1893                             seqValue: seqValue
1894                         };
1895                     }
1896 
1897                     if (dataArr[arrKey][sumField] == undefined)
1898                     {
1899                         dataArr[arrKey][sumField] = 0;
1900                     }
1901                     if (dataArr[arrKey][countField] == undefined)
1902                     {
1903                         dataArr[arrKey][countField] = 0;
1904                     }
1905 
1906                     if (rowValue != undefined)
1907                     {
1908                         dataArr[arrKey][sumField] += rowValue;
1909                         dataArr[arrKey][countField]++;
1910                         dataArr[arrKey][fieldName] = dataArr[arrKey][sumField] / dataArr[arrKey][countField];
1911                     }
1912                 };
1913 
1914                 var addAllValuesToTrendLineData = function(dataArr, seqValue, arrKey, row, hasYRightMetric)
1915                 {
1916                     var plotValueName = config.properties.value, plotValueNameRight = config.properties.valueRight;
1917                     var plotValueNamePositive = config.properties.positiveValue, plotValueNameRightPositive = config.properties.positiveValueRight;
1918                     if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.MovingRange)
1919                     {
1920                         plotValueName = config.properties.valueMR;
1921                         plotValueNameRight = config.properties.valueRightMR;
1922                     }
1923                     else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM)
1924                     {
1925                         plotValueName = config.properties.negativeValue;
1926                         plotValueNameRight = config.properties.negativeValueRight;
1927                     }
1928 
1929                     addValueToTrendLineData(dataArr, seqValue, arrKey, plotValueName, row[plotValueName], 'sum1', 'count1');
1930                     if (hasYRightMetric)
1931                     {
1932                         addValueToTrendLineData(dataArr, seqValue, arrKey, plotValueNameRight, row[plotValueNameRight], 'sum2', 'count2');
1933                     }
1934 
1935                     if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM)
1936                     {
1937                         addValueToTrendLineData(dataArr, seqValue, arrKey, plotValueNamePositive, row[plotValueNamePositive], 'sum3', 'count3');
1938                         if (hasYRightMetric)
1939                         {
1940                             addValueToTrendLineData(dataArr, seqValue, arrKey, plotValueNameRightPositive, row[plotValueNameRightPositive], 'sum4', 'count4');
1941                         }
1942                     }
1943                 };
1944 
1945                 index = uniqueXAxisLabels.indexOf(row[config.properties.xTick]);
1946 
1947                 // calculate average values for the trend line data (used when grouping x by unique value)
1948                 addAllValuesToTrendLineData(groupedTrendlineData, index, index, row, hasYRightMetric);
1949 
1950                 // calculate average values for trend line data for each series (used when grouping x by unique value with a groupBy series property)
1951                 if (config.properties.groupBy && row[config.properties.groupBy]) {
1952                     var series = row[config.properties.groupBy];
1953                     var key = series + '|' + index;
1954 
1955                     addAllValuesToTrendLineData(groupedTrendlineSeriesData, index, key, row, hasYRightMetric);
1956 
1957                     groupedTrendlineSeriesData[key][config.properties.groupBy] = series;
1958                 }
1959             }
1960             else {
1961                 index++;
1962             }
1963 
1964             tickLabelMap[index] = row[config.properties.xTickLabel];
1965             row.seqValue = index;
1966 
1967             if (config.qcPlotType === LABKEY.vis.TrendingLinePlotType.LeveyJennings)
1968             {
1969                 if (config.properties.mean && config.properties.stdDev && !meanStdDevData[index])
1970                 {
1971                     meanStdDevData[index] = row;
1972                 }
1973             }
1974         }
1975 
1976         // min x-axis tick length is 10 by default
1977         var maxSeqValue = config.data.length > 0 ? config.data[config.data.length - 1].seqValue + 1 : 0;
1978         for (var i = maxSeqValue; i < 10; i++)
1979         {
1980             var temp = {type: 'empty', seqValue: i};
1981             temp[config.properties.xTickLabel] = "";
1982             if (config.properties.color && config.data[0]) {
1983                 temp[config.properties.color] = config.data[0][config.properties.color];
1984             }
1985             config.data.push(temp);
1986         }
1987 
1988         // we only need the color aes if there is > 1 distinct value in the color variable
1989         if (distinctColorValues.length < 2 && config.properties.groupBy == undefined) {
1990             config.properties.color = undefined;
1991         }
1992 
1993         config.tickOverlapRotation = 35;
1994 
1995         // CUSUM plots can only be linear scale
1996         var yAxisScaleOverride;
1997         if (config.qcPlotType === LABKEY.vis.TrendingLinePlotType.CUSUM) {
1998             yAxisScaleOverride = 'linear';
1999         }
2000 
2001         config.scales = {
2002             color: {
2003                 scaleType: 'discrete',
2004                 range: config.properties.colorRange
2005             },
2006             x: {
2007                 scaleType: 'discrete',
2008                 tickFormat: function(index) {
2009                     // only show a max of 35 labels on the x-axis to avoid overlap
2010                     if (index % Math.ceil(config.data[config.data.length-1].seqValue / 35) == 0) {
2011                         return tickLabelMap[index];
2012                     }
2013                     else {
2014                         return "";
2015                     }
2016                 },
2017                 tickCls: function(index) {
2018                     var baseTag = 'ticklabel';
2019                     var tagIndex = config.properties.xTickTagIndex;
2020                     if (tagIndex != undefined && tagIndex == index) {
2021                         return baseTag+' xticktag';
2022                     }
2023                     return baseTag;
2024                 }
2025             },
2026             yLeft: {
2027                 scaleType: 'continuous',
2028                 domain: config.properties.yAxisDomain,
2029                 trans: yAxisScaleOverride || config.properties.yAxisScale || 'linear',
2030                 tickFormat: function(val) {
2031                     return LABKEY.vis.isValid(val) && (val > 100000 || val < -100000) ? val.toExponential() : val;
2032                 }
2033             }
2034         };
2035 
2036         if (hasYRightMetric)
2037         {
2038             config.scales.yRight = {
2039                 scaleType: 'continuous',
2040                 domain: config.properties.yAxisDomain,
2041                 trans: yAxisScaleOverride || config.properties.yAxisScale || 'linear',
2042                 tickFormat: function(val) {
2043                     return LABKEY.vis.isValid(val) && (val > 100000 || val < -100000) ? val.toExponential() : val;
2044                 }
2045             };
2046         }
2047 
2048         // Issue 23626: map line/point color based on legend data
2049         if (config.legendData && config.properties.color && !config.properties.colorRange)
2050         {
2051             var legendColorMap = {};
2052             for (var i = 0; i < config.legendData.length; i++)
2053             {
2054                 if (config.legendData[i].name)
2055                 {
2056                     legendColorMap[config.legendData[i].name] = config.legendData[i].color;
2057                 }
2058             }
2059 
2060             if (config.qcPlotType === LABKEY.vis.TrendingLinePlotType.CUSUM)
2061             {
2062                 config.scales.color = {
2063                     scale: function(group) {
2064                         var normalizedGroup = group.replace('CUSUMmN', 'CUSUMm').replace('CUSUMmP', 'CUSUMm');
2065                         normalizedGroup = normalizedGroup.replace('CUSUMvN', 'CUSUMv').replace('CUSUMvP', 'CUSUMv');
2066                         return legendColorMap[normalizedGroup];
2067                     }
2068                 };
2069             }
2070             else
2071             {
2072                 config.scales.color = {
2073                     scale: function(group) {
2074                         return legendColorMap[group];
2075                     }
2076                 };
2077             }
2078         }
2079 
2080         if(!config.margins) {
2081             config.margins = {};
2082         }
2083 
2084         if(!config.margins.top) {
2085             config.margins.top = config.labels && config.labels.main ? 30 : 10;
2086         }
2087 
2088         if(!config.margins.right) {
2089             config.margins.right = (config.properties.color || (config.legendData && config.legendData.length > 0) ? 190 : 40)
2090                                     + (hasYRightMetric ? 45 : 0);
2091         }
2092 
2093         if(!config.margins.bottom) {
2094             config.margins.bottom = config.labels && config.labels.x ? 75 : 55;
2095         }
2096 
2097         if(!config.margins.left) {
2098             config.margins.left = config.labels && config.labels.y ? 75 : 55;
2099         }
2100 
2101         config.aes = {
2102             x: 'seqValue'
2103         };
2104 
2105         // determine the width the error bars
2106         if (config.properties.disableRangeDisplay) {
2107             config.layers = [];
2108         }
2109         else {
2110             var barWidth = Math.max(config.width / config.data[config.data.length-1].seqValue / 5, 3);
2111 
2112             if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.LeveyJennings) {
2113 
2114                 // +/- 3 standard deviation displayed using the ErrorBar geom with different colors
2115                 var stdDev3Layer = new LABKEY.vis.Layer({
2116                     geom: new LABKEY.vis.Geom.ErrorBar({size: 1, color: 'red', dashed: true, altColor: 'darkgrey', width: barWidth}),
2117                     data: meanStdDevData,
2118                     aes: {
2119                         error: function(row){return row[config.properties.stdDev] * 3;},
2120                         yLeft: config.properties.mean
2121                     }
2122                 });
2123                 var stdDev2Layer = new LABKEY.vis.Layer({
2124                     geom: new LABKEY.vis.Geom.ErrorBar({size: 1, color: 'blue', dashed: true, altColor: 'darkgrey', width: barWidth}),
2125                     data: meanStdDevData,
2126                     aes: {
2127                         error: function(row){return row[config.properties.stdDev] * 2;},
2128                         yLeft: config.properties.mean
2129                     }
2130                 });
2131                 var stdDev1Layer = new LABKEY.vis.Layer({
2132                     geom: new LABKEY.vis.Geom.ErrorBar({size: 1, color: 'green', dashed: true, altColor: 'darkgrey', width: barWidth}),
2133                     data: meanStdDevData,
2134                     aes: {
2135                         error: function(row){return row[config.properties.stdDev];},
2136                         yLeft: config.properties.mean
2137                     }
2138                 });
2139                 var meanLayer = new LABKEY.vis.Layer({
2140                     geom: new LABKEY.vis.Geom.ErrorBar({size: 1, color: 'darkgrey', width: barWidth}),
2141                     data: meanStdDevData,
2142                     aes: {
2143                         error: function(row){return 0;},
2144                         yLeft: config.properties.mean
2145                     }
2146                 });
2147                 config.layers = [stdDev3Layer, stdDev2Layer, stdDev1Layer, meanLayer];
2148             }
2149             else if (config.qcPlotType === LABKEY.vis.TrendingLinePlotType.CUSUM) {
2150                 var range = new LABKEY.vis.Layer({
2151                     geom: new LABKEY.vis.Geom.ControlRange({size: 1, color: 'red', dashed: true, altColor: 'darkgrey', width: barWidth}),
2152                     data: config.data,
2153                     aes: {
2154                         upper: function(){return LABKEY.vis.Stat.CUSUM_CONTROL_LIMIT;},
2155                         lower: function(){return LABKEY.vis.Stat.CUSUM_CONTROL_LIMIT_LOWER;},
2156                         yLeft: config.properties.mean
2157                     }
2158                 });
2159                 config.layers = [range];
2160             }
2161             else if (config.qcPlotType === LABKEY.vis.TrendingLinePlotType.MovingRange) {
2162                 if (config.properties.valueConversion === "standardDeviation") {
2163                     config.layers = [];
2164                 }
2165                 else {
2166                     var range = new LABKEY.vis.Layer({
2167                         geom: new LABKEY.vis.Geom.ControlRange({size: 1, color: 'red', dashed: true, altColor: 'darkgrey', width: barWidth}),
2168                         data: config.data,
2169                         aes: {
2170                             upper: function(row){return row[config.properties.meanMR] * LABKEY.vis.Stat.MOVING_RANGE_UPPER_LIMIT_WEIGHT;},
2171                             lower: function(){return LABKEY.vis.Stat.MOVING_RANGE_LOWER_LIMIT;},
2172                             yLeft: config.properties.mean
2173                         }
2174                     });
2175                     config.layers = [range];
2176                 }
2177             }
2178         }
2179 
2180         if (config.properties.showTrendLine)
2181         {
2182             var getPathLayerConfig = function(ySide, valueName, colorValue, negativeCusum)
2183             {
2184                 var pathLayerConfig = {
2185                     geom: new LABKEY.vis.Geom.Path({
2186                         opacity: .6,
2187                         size: 2,
2188                         dashed: config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM && !negativeCusum
2189                     }),
2190                     aes: {}
2191                 };
2192 
2193                 pathLayerConfig.aes[ySide] = valueName;
2194 
2195                 // if we aren't showing multiple series data via the group by, use the groupedTrendlineData for the path
2196                 if (config.properties.groupBy)
2197                 {
2198                     // convert the groupedTrendlineSeriesData object into an array of the object values
2199                     var seriesDataArr = [];
2200                     for(var i in groupedTrendlineSeriesData) {
2201                         if (groupedTrendlineSeriesData.hasOwnProperty(i)) {
2202                             var d = { seqValue: groupedTrendlineSeriesData[i].seqValue };
2203                             d[config.properties.groupBy] = groupedTrendlineSeriesData[i][config.properties.groupBy] + (hasYRightMetric ? '|' + valueName : '');
2204                             d[valueName] = groupedTrendlineSeriesData[i][valueName];
2205                             seriesDataArr.push(d);
2206                         }
2207                     }
2208                     pathLayerConfig.data = seriesDataArr;
2209 
2210                     pathLayerConfig.aes.pathColor = config.properties.groupBy;
2211                     pathLayerConfig.aes.group = config.properties.groupBy;
2212                 }
2213                 else
2214                 {
2215                     pathLayerConfig.data = groupedTrendlineData;
2216 
2217                     if (colorValue != undefined)
2218                     {
2219                         pathLayerConfig.aes.pathColor = function(data) {
2220                             return colorValue;
2221                         }
2222                     }
2223                 }
2224 
2225                 return pathLayerConfig;
2226             };
2227 
2228             if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM)
2229             {
2230                 if (hasYRightMetric)
2231                 {
2232                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.negativeValue, 1, true)));
2233                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yRight', config.properties.negativeValueRight, 0, true)));
2234                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.positiveValue, 1, false)));
2235                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yRight', config.properties.positiveValueRight, 0, false)));
2236                 }
2237                 else
2238                 {
2239                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.negativeValue, undefined, true)));
2240                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.positiveValue, undefined, false)));
2241                 }
2242             }
2243             else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.MovingRange)
2244             {
2245                 if (hasYRightMetric)
2246                 {
2247                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.valueMR, 0)));
2248                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yRight', config.properties.valueRightMR, 1)));
2249                 }
2250                 else
2251                 {
2252                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.valueMR)));
2253                 }
2254             }
2255             else
2256             {
2257                 if (hasYRightMetric)
2258                 {
2259                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.value, 0)));
2260                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yRight', config.properties.valueRight, 1)));
2261                 }
2262                 else
2263                 {
2264                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.value)));
2265                 }
2266             }
2267         }
2268 
2269         // points based on the data value, color and hover text can be added via params to config
2270         var getPointLayerConfig = function(ySide, valueName, colorValue, hasOutlierMap)
2271         {
2272             var pointLayerConfig = {
2273                 geom: new LABKEY.vis.Geom.Point({
2274                     position: config.properties.position,
2275                     opacity: config.properties.pointOpacityFn,
2276                     size: 3
2277                 }),
2278                 aes: {}
2279             };
2280 
2281             pointLayerConfig.aes[ySide] = valueName;
2282 
2283             if (config.properties.color) {
2284                 // Sometimes a row of data plots multiple points that need different colors.
2285                 // example color property:  { color: 'outliers' ... }
2286                 // example data row: { outliers: { 'CUSUMmP': false, 'CUSUMmN': true} ... }
2287                 if (hasOutlierMap && row[config.properties.color] === Object(row[config.properties.color])) {
2288                     pointLayerConfig.aes.color = function (row) {
2289                         return row[config.properties.color][valueName]
2290                     };
2291                 } else {
2292                     pointLayerConfig.aes.color = function(row) {
2293                         return row[config.properties.color] + (hasYRightMetric ? '|' + valueName : '');
2294                     };
2295                 }
2296             }
2297             else if (colorValue !== undefined) {
2298                 pointLayerConfig.aes.color = function(row){ return colorValue; };
2299             }
2300 
2301             if (config.properties.shape) {
2302                 pointLayerConfig.aes.shape = config.properties.shape;
2303             }
2304             if (config.properties.hoverTextFn) {
2305                 pointLayerConfig.aes.hoverText = function(row) {
2306                     return config.properties.hoverTextFn.call(this, row, valueName);
2307                 };
2308             }
2309             if (config.properties.pointClickFn) {
2310                 pointLayerConfig.aes.pointClickFn = config.properties.pointClickFn;
2311             }
2312 
2313             // add some mouse over effects to highlight selected point
2314             pointLayerConfig.aes.mouseOverFn = function(event, pointData, layerSel, point) {
2315                 d3.select(event.srcElement).transition().duration(800).attr("stroke-width", 5).ease("elastic");
2316 
2317                 if (config.properties.mouseOverFn) {
2318                     config.properties.mouseOverFn.call(config.properties.mouseOverFnScope || this, event, pointData, layerSel, point, valueName);
2319                 }
2320             };
2321             pointLayerConfig.aes.mouseOutFn = function(event, pointData, layerSel) {
2322                 d3.select(event.srcElement).transition().duration(800).attr("stroke-width", 1).ease("elastic");
2323             };
2324 
2325             if (config.properties.pointIdAttr) {
2326                 pointLayerConfig.aes.pointIdAttr = config.properties.pointIdAttr;
2327             }
2328 
2329             return pointLayerConfig;
2330         };
2331 
2332         if (config.properties.showDataPoints == undefined) {
2333             config.properties.showDataPoints = true;
2334         }
2335 
2336         if (config.properties.showDataPoints) {
2337             if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM) {
2338                 if (hasYRightMetric) {
2339                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.negativeValue, 1, true)));
2340                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yRight', config.properties.negativeValueRight, 0, true)));
2341                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.positiveValue, 1, true)));
2342                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yRight', config.properties.positiveValueRight, 0, true)));
2343 
2344                 }
2345                 else {
2346                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.negativeValue, undefined, true)));
2347                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.positiveValue, undefined, true)));
2348                 }
2349             }
2350             else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.MovingRange) {
2351                 if (hasYRightMetric) {
2352                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.valueMR, 0)));
2353                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yRight', config.properties.valueRightMR, 1)));
2354                 }
2355                 else {
2356                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.valueMR)));
2357                 }
2358             }
2359             else {
2360                 if (hasYRightMetric) {
2361                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.value, 0)));
2362                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yRight', config.properties.valueRight, 1)));
2363                 }
2364                 else {
2365                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.value)));
2366                 }
2367             }
2368         }
2369 
2370         return new LABKEY.vis.Plot(config);
2371     };
2372 
2373     LABKEY.vis.TrendingLineShape = {
2374         positiveCUSUM: function(){
2375             return "M3,-0.5L6,-0.5 6,0.5 3,0.5Z M-3,-0.5L0,-0.5 0,0.5 -3,0.5Z M-9,-0.5L-6,-0.5 -6,0.5 -9,0.5Z";
2376         },
2377         negativeCUSUM: function(){
2378             return "M-9,-0.5L6,-0.5 6,0.5 -9,0.5Z";
2379         },
2380         stdDevLJ: function(){
2381             return "M-9,-0.5L-7,-0.5 M-6,-0.5L-4,-0.5 M-3,-0.5L-1,-0.5 M0,-0.5L2,-0.5 M3,-0.5L5,-0.5";
2382         },
2383         meanLJ: function(){
2384             return "M-9,-0.5L5,-0.5";
2385         },
2386         limitMR: function(){
2387             return "M-9,-0.5L-7,-0.5 M-6,-0.5L-4,-0.5 M-3,-0.5L-1,-0.5 M0,-0.5L2,-0.5 M3,-0.5L5,-0.5";
2388         },
2389         limitCUSUM: function(){
2390             return "M-9,-0.5L-7,-0.5 M-6,-0.5L-4,-0.5 M-3,-0.5L-1,-0.5 M0,-0.5L2,-0.5 M3,-0.5L5,-0.5";
2391         }
2392     };
2393 
2394     /**
2395      * @ Deprecated
2396      */
2397     LABKEY.vis.LeveyJenningsPlot = LABKEY.vis.TrendingLinePlot;
2398 })();
2399 
2400 /**
2401  * @name LABKEY.vis.SurvivalCurvePlot
2402  * @class SurvivalCurvePlot Wrapper to create a plot which shows survival curve step lines and censor points (based on output from R survival package).
2403  * @description This helper will take the input data and generate stepwise data points for use with the Path geom.
2404  * @param {Object} config An object that contains the following properties
2405  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
2406  * @param {Number} [config.width] The chart canvas width in pixels.
2407  * @param {Number} [config.height] The chart canvas height in pixels.
2408  * @param {Array} [config.data] The array of step data for the survival curves.
2409  * @param {String} [config.groupBy] (optional) The data array object property used to group plot lines and points.
2410  * @param {Array} [config.censorData] The array of censor data to overlay on the survival step lines.
2411  * @param {Function} [config.censorHoverText] (optional) Function defining the hover text to display for the censor data points.
2412  */
2413 (function(){
2414 
2415     LABKEY.vis.SurvivalCurvePlot = function(config){
2416 
2417         if (config.renderTo == null){
2418             throw new Error("Unable to create survival curve plot, renderTo not specified.");
2419         }
2420 
2421         if (config.data == null || config.censorData == null){
2422             throw new Error("Unable to create survival curve plot, data and/or censorData array not specified.");
2423         }
2424 
2425         if (config.aes == null || config.aes.x == null || config.aes.yLeft == null) {
2426             throw new Error("Unable to create survival curve plot, aes (x and yLeft) not specified.")
2427         }
2428 
2429         // Convert data array for step-wise line plot
2430         var stepData = [];
2431         var groupBy = config.groupBy;
2432         var aesX = config.aes.x;
2433         var aesY = config.aes.yLeft;
2434 
2435         for (var i=0; i<config.data.length; i++)
2436         {
2437             stepData.push(config.data[i]);
2438 
2439             if ( (i<config.data.length-1) && (config.data[i][groupBy] == config.data[i+1][groupBy])
2440                     && (config.data[i][aesX] != config.data[i+1][aesX])
2441                     && (config.data[i][aesY] != config.data[i+1][aesY]))
2442             {
2443                 var point = {};
2444                 point[groupBy] = config.data[i][groupBy];
2445                 point[aesX] = config.data[i+1][aesX];
2446                 point[aesY] = config.data[i][aesY];
2447                 stepData.push(point);
2448             }
2449         }
2450         config.data = stepData;
2451 
2452         config.layers = [
2453             new LABKEY.vis.Layer({
2454                 geom: new LABKEY.vis.Geom.Path({size:2, opacity:1}),
2455                 aes: {
2456                     pathColor: config.groupBy,
2457                     group: config.groupBy
2458                 }
2459             }),
2460             new LABKEY.vis.Layer({
2461                 geom: new LABKEY.vis.Geom.Point({opacity:1}),
2462                 data: config.censorData,
2463                 aes: {
2464                     color: config.groupBy,
2465                     hoverText: config.censorHoverText,
2466                     shape: config.groupBy
2467                 }
2468 
2469             })
2470         ];
2471 
2472         if (!config.scales) config.scales = {};
2473         config.scales.x = { scaleType: 'continuous', trans: 'linear' };
2474         config.scales.yLeft = { scaleType: 'continuous', trans: 'linear', domain: [0, 1] };
2475 
2476         config.aes.mouseOverFn = function(event, pointData, layerSel) {
2477             mouseOverFn(event, pointData, layerSel, config.groupBy);
2478         };
2479 
2480         config.aes.mouseOutFn = mouseOutFn;
2481 
2482         return new LABKEY.vis.Plot(config);
2483     };
2484 
2485     var mouseOverFn = function(event, pointData, layerSel, subjectColumn) {
2486         var points = layerSel.selectAll('.point path');
2487         var lines = d3.selectAll('path.line');
2488 
2489         var opacityAcc = function(d) {
2490             if (d[subjectColumn] && d[subjectColumn] == pointData[subjectColumn])
2491             {
2492                 return 1;
2493             }
2494             return .3;
2495         };
2496 
2497         points.attr('fill-opacity', opacityAcc).attr('stroke-opacity', opacityAcc);
2498         lines.attr('fill-opacity', opacityAcc).attr('stroke-opacity', opacityAcc);
2499     };
2500 
2501     var mouseOutFn = function(event, pointData, layerSel) {
2502         layerSel.selectAll('.point path').attr('fill-opacity', 1).attr('stroke-opacity', 1);
2503         d3.selectAll('path.line').attr('fill-opacity', 1).attr('stroke-opacity', 1);
2504     };
2505 })();
2506 
2507 /**
2508  * @name LABKEY.vis.TimelinePlot
2509  * @class TimelinePlot Wrapper to create a plot which shows timeline events with event types on the y-axis and days/time on the x-axis.
2510  * @param {Object} config An object that contains the following properties
2511  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
2512  * @param {Number} [config.width] The chart canvas width in pixels.
2513  * @param {Number} [config.height] The chart canvas height in pixels.
2514  * @param {Array} [config.data] The array of event data including event types and subtypes for the plot.
2515  * @param {String} [config.gridLinesVisible] Possible options are 'y', 'x', and 'both' to determine which sets of
2516  *                  grid lines are rendered on the plot. Default is 'both'.
2517  * @param {Object} [config.disableAxis] Object specifying whether to disable rendering of any Axes on the plot. Default: {xTop: false, xBottom: false, yLeft: false, yRight: false}
2518  * @param {Date} [config.options.startDate] (Optional) The start date to use to calculate number of days until event date.
2519  * @param {Object} [config.options] (Optional) Display options as defined in {@link LABKEY.vis.Geom.TimelinePlot}.
2520  * @param {Boolean} [config.options.isCollapsed] (Optional) If true, the timeline collapses subtypes into their parent rows. Defaults to True.
2521  * @param {Number} [config.options.rowHeight] (Optional) The height of individual rows in pixels. For expanded timelines,
2522  *                  row height will resize to 75% of this value. Defaults to 1.
2523  * @param {Object} [config.options.highlight] (Optional) Special Data object containing information to highlight a specific
2524  *                  row in the timeline. Must have the same shape & properties as all other input data.
2525  * @param {String} [config.options.highlightRowColor] (Optional) Hex color to specify what color the highlighted row will
2526  *                  be if, found in the data. Defaults to #74B0C4.
2527  * @param {String} [config.options.activeEventKey] (Optional) Name of property that is paired with
2528  *                  @param config.options.activeEventIdentifier to identify a unique event in the data.
2529  * @param {String} [config.options.activeEventIdentifier] (Optional) Name of value that is paired with
2530  *                  @param config.options.activeEventKey to identify a unique event in the data.
2531  * @param {String} [config.options.activeEventStrokeColor] (Optional) Hex color to specify what color the active event
2532  *                  rect's stroke will be, if found in the data. Defaults to Red.
2533  * @param {Object} [config.options.emphasisEvents] (Optional) Object containing key:[value] pairs whose keys are property
2534  *                  names of a data object and whose value is an array of possible values that should have a highlight
2535 *                   line drawn on the chart when found. Example: {'type': ['death', 'Withdrawal']}
2536  * @param {String} [config.options.tickColor] (Optional) Hex color to specify the color of Axis ticks. Defaults to #DDDDDD.
2537  * @param {String} [config.options.emphasisTickColor] (Optional) Hex color to specify the color of emphasis event ticks,
2538  *                  if found in the data. Defaults to #1a969d.
2539  * @param {String} [config.options.timeUnit] (Optional) Unit of time to use when calculating how far an event's date
2540  *                  is from the start date. Default is years. Valid string values include minutes, hours, days, years, and decades.
2541  * @param {Number} [config.options.eventIconSize] (Optional) Size of event square width/height dimensions.
2542  * @param {String} [config.options.eventIconColor] (Optional) Hex color of event square stroke. Defaults to black (#0000000).
2543  * @param {String} [config.options.eventIconFill] (Optional) Hex color of event square inner fill. Defaults to black (#000000)..
2544  * @param {Float} [config.options.eventIconOpacity] (Optional) Float between 0 - 1 (inclusive) to specify how transparent the
2545  *                  fill of event icons will be. Defaults to 1.
2546  * @param {Array} [config.options.rowColorDomain] (Optional) Array of length 2 containing string Hex values for the two
2547  *                  alternating colors of timeline row rectangles. Defaults to ['#f2f2f2', '#ffffff'].
2548  */
2549 (function(){
2550 
2551     LABKEY.vis.TimelinePlot = function(config)
2552     {
2553         if (config.renderTo == undefined || config.renderTo == null) { throw new Error("Unable to create timeline plot, renderTo not specified."); }
2554 
2555         if (config.data == undefined || config.data == null) { throw new Error("Unable to create timeline plot, data array not specified."); }
2556 
2557         if (config.width == undefined || config.width == null) { throw new Error("Unable to create timeline plot, width not specified."); }
2558 
2559         if (!config.aes.y) {
2560             config.aes.y = 'key';
2561         }
2562 
2563         if (!config.options) {
2564             config.options = {};
2565         }
2566 
2567         //default x scale is in years
2568         if (!config.options.timeUnit) {
2569             config.options.timeUnit = 'years';
2570         }
2571 
2572         //set default left margin to make room for event label text
2573         if (!config.margins.left) {
2574             config.margins.left = 200
2575         }
2576 
2577         //default row height value
2578         if (!config.options.rowHeight) {
2579             config.options.rowHeight = 40;
2580         }
2581 
2582         //override default plot values if not set
2583         if (!config.margins.top) {
2584             config.margins.top = 40;
2585         }
2586         if (!config.margins.bottom) {
2587             config.margins.bottom = 50;
2588         }
2589         if (!config.gridLineWidth) {
2590             config.gridLineWidth = 1;
2591         }
2592         if (!config.gridColor) {
2593             config.gridColor = '#FFFFFF';
2594         }
2595         if (!config.borderColor) {
2596             config.borderColor = '#DDDDDD';
2597         }
2598         if (!config.tickColor) {
2599             config.tickColor = '#DDDDDD';
2600         }
2601 
2602         config.rendererType = 'd3';
2603         config.options.marginLeft = config.margins.left;
2604         config.options.parentName = config.aes.parentName;
2605         config.options.childName = config.aes.childName;
2606         config.options.dateKey = config.aes.x;
2607 
2608         config.scales = {
2609             x: {
2610                 scaleType: 'continuous'
2611             },
2612             yLeft: {
2613                 scaleType: 'discrete'
2614             }
2615         };
2616 
2617         var millis;
2618         switch(config.options.timeUnit.toLowerCase())
2619         {
2620             case 'minutes':
2621                 millis = 1000 * 60;
2622                 break;
2623             case 'hours':
2624                 millis = 1000 * 60 * 60;
2625                 break;
2626             case 'days':
2627                 millis = 1000 * 60 * 60 * 24;
2628                 break;
2629             case 'months':
2630                 millis = 1000 * 60 * 60 * 24 * 30.42;
2631                 break;
2632             case 'years':
2633                 millis = 1000 * 60 * 60 * 24 * 365;
2634                 break;
2635             case 'decades':
2636                 millis = 1000 * 60 * 60 * 24 * 365 * 10;
2637                 break;
2638             default:
2639                 millis = 1000;
2640         }
2641 
2642         //find the earliest occurring date in the data if startDate is not already specified
2643         var min = config.options.startDate ? config.options.startDate : null;
2644         if (min == null)
2645         {
2646             for (var i = 0; i < config.data.length; i++)
2647             {
2648                 config.data[i][config.aes.x] = new Date(config.data[i][config.aes.x]);
2649                 if (min == null)
2650                 {
2651                     min = config.data[i][config.aes.x];
2652                 }
2653                 min = config.data[i][config.aes.x] < min ? config.data[i][config.aes.x] : min;
2654             }
2655         }
2656 
2657         //Loop through the data and do calculations for each entry
2658         var max = 0;
2659         var parents = new Set();
2660         var children = new Set();
2661         var types = new Set();
2662         var domain = [];
2663         for (var j = 0; j < config.data.length; j++)
2664         {
2665             //calculate difference in time units
2666             var d = config.data[j];
2667             d[config.aes.x] = config.options.startDate ? new Date(d[config.aes.x]) : d[config.aes.x];
2668             var timeDifference = (d[config.aes.x] - min) / millis;
2669             d[config.options.timeUnit] = timeDifference;
2670 
2671             //update unique counts
2672             parents.add(d[config.aes.parentName]);
2673             children.add(d[config.aes.childName]);
2674 
2675             //update domain
2676             if (!config.options.isCollapsed) {
2677                 var str;
2678                 if (d[config.aes.parentName] != null && d[config.aes.parentName] != 'null' && d[config.aes.parentName] != undefined) {
2679                     str = d[config.aes.parentName];
2680                     if (!types.has(str) && str != undefined) {
2681                         domain.push(str);
2682                         types.add(str);
2683                     }
2684                     d.typeSubtype = str;
2685                 }
2686                 if (d[config.aes.childName] != null && d[config.aes.childName] != 'null' && d[config.aes.childName] != undefined) {
2687                     str += '-' + d[config.aes.childName];
2688                 }
2689                 if (!types.has(str) && str != undefined) {
2690                     domain.push(str);
2691                     types.add(str);
2692                 }
2693 
2694                 //typeSubtype will be a simple unique identifier for this type & subtype of event
2695                 d.typeSubtype = str;
2696             } else {
2697                 if (!types.has(d[config.aes.parentName])) {
2698                     domain.push(d[config.aes.parentName]);
2699                     types.add(d[config.aes.parentName]);
2700                 }
2701             }
2702 
2703             //update max value
2704             max = timeDifference > max ? timeDifference : max;
2705         }
2706         var numParentChildUniques = parents.size + children.size;
2707         if (children.has(null)) {
2708             numParentChildUniques--;
2709         }
2710         var numParentUniques = parents.size;
2711         domain.sort().reverse();
2712 
2713         //For a better looking title
2714         function capitalizeFirstLetter(string) {
2715             return string.charAt(0).toUpperCase() + string.slice(1);
2716         }
2717 
2718         //Update x label to include the start date for better context
2719         config.labels.x = {value: capitalizeFirstLetter(config.options.timeUnit) + " Since " + min.toDateString()};
2720 
2721         if (!config.options.isCollapsed) {
2722             config.aes.typeSubtype = "typeSubtype";
2723 
2724             config.scales.yLeft.domain = domain;
2725             var chartHeightMultiplier = numParentChildUniques !== numParentUniques ? Math.floor(config.options.rowHeight * .75) : config.options.rowHeight;
2726             config.height = (chartHeightMultiplier * numParentChildUniques) + config.margins.top + config.margins.bottom;
2727             config.options.rowHeight = (config.height - (config.margins.top + config.margins.bottom)) / numParentChildUniques;
2728             if (numParentChildUniques < 10) {
2729                 //small visual adjustment for short charts without many data points
2730                 config.options.rowHeight = config.options.rowHeight - (12 - numParentChildUniques);
2731             }
2732         } else {
2733             config.scales.yLeft.domain = domain;
2734             config.height = (config.options.rowHeight * numParentUniques) + config.margins.top + config.margins.bottom;
2735 
2736             config.options.rowHeight = (config.height - (config.margins.top + config.margins.bottom)) / numParentUniques;
2737             if (numParentUniques < 10) {
2738                 config.options.rowHeight = config.options.rowHeight - (12 - numParentUniques);
2739             }
2740         }
2741 
2742         config.scales.x.domain = [0, Math.ceil(max)];
2743         config.aes.x = config.options.timeUnit;
2744         config.layers = [
2745             new LABKEY.vis.Layer({
2746                 geom: new LABKEY.vis.Geom.TimelinePlot(config.options)
2747             })
2748         ];
2749 
2750         return new LABKEY.vis.Plot(config);
2751     };
2752 })();
2753 
2754