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