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-2017 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.valueMR] The data property name for the moving range value to be plotted on the left y-axis.
1571  *                          Used by MovingRange.
1572  * @param {String} [config.properties.valueRightMR] The data property name for the moving range to be plotted on the right y-axis.
1573  *                          Used by MovingRange.
1574  * @param {String} [config.properties.meanMR] The data property name for the mean of the moving range.
1575  *                          Used MovingRange.
1576  * @param {String} [config.properties.positiveValue] The data property name for the value to be plotted on the left y-axis for CUSUM+.
1577  *                          Used by CUSUM only.
1578  * @param {String} [config.properties.positiveValueRight] The data property name for the value to be plotted on the right y-axis for CUSUM+.
1579  *                          Used by CUSUM only.
1580  * @param {String} [config.properties.negativeValue] The data property name for the value to be plotted on the left y-axis for CUSUM-.
1581  *                          Used by CUSUM only.
1582  * @param {String} [config.properties.negativeValueRight] The data property name for the value to be plotted on the right y-axis for CUSUM-.
1583  *                          Used by CUSUM only.
1584  * @param {String} [config.properties.xTickLabel] The data property name for the x-axis tick label.
1585  * @param {Number} [config.properties.xTickTagIndex] (Optional) The index/value of the x-axis label to be tagged (i.e. class="xticktag").
1586  * @param {Boolean} [config.properties.showTrendLine] (Optional) Whether or not to show a line connecting the data points. Default false.
1587  * @param {Boolean} [config.properties.showDataPoints] (Optional) Whether or not to show the individual data points. Default true.
1588  * @param {Boolean} [config.properties.disableRangeDisplay] (Optional) Whether or not to show the control ranges in the plot. Defaults to false.
1589  *                          For LeveyJennings, the range is +/- 3 standard deviations from a mean.
1590  *                          For MovingRange, the range is [0, 3.268*mean(mR)].
1591  *                          For CUSUM, the range is [0, +5].
1592  * @param {String} [config.properties.xTick] (Optional) The data property to use for unique x-axis tick marks. Defaults to sequence from 1:data length.
1593  * @param {String} [config.properties.yAxisScale] (Optional) Whether the y-axis should be plotted with linear or log scale. Default linear.
1594  * @param {Array} [config.properties.yAxisDomain] (Optional) Y-axis min/max values. Example: [0,20].
1595  * @param {String} [config.properties.color] (Optional) The data property name for the color to be used for the data point.
1596  * @param {Array} [config.properties.colorRange] (Optional) The array of color values to use for the data points.
1597  * @param {Function} [config.properties.pointOpacityFn] (Optional) A function to be called with the point data to
1598  *                  return an opacity value for that point.
1599  * @param {String} [config.groupBy] (optional) The data property name used to group plot lines and points.
1600  * @param {Function} [config.properties.hoverTextFn] (Optional) The hover text to display for each data point. The parameter
1601  *                  to that function will be a row of data with access to all values for that row.
1602  * @param {Function} [config.properties.mouseOverFn] (Optional) The function to call on data point mouse over. The parameters to
1603  *                  that function will be the click event, the point data, the selection layer, and the DOM element for the point itself.
1604  * @param {Object} [config.properties.mouseOverFnScope] (Optional) The scope to use for the call to mouseOverFn.
1605  * @param {Function} [config.properties.pointClickFn] (Optional) The function to call on data point click. The parameters to
1606  *                  that function will be the click event and the row of data for the selected point.
1607  */
1608 (function(){
1609     LABKEY.vis.TrendingLinePlotType = {
1610         LeveyJennings : 'Levey-Jennings',
1611         CUSUM : 'CUSUM',
1612         MovingRange: 'MovingRange'
1613     };
1614 
1615     LABKEY.vis.TrendingLinePlot = function(config){
1616         if (!config.qcPlotType)
1617             config.qcPlotType = LABKEY.vis.TrendingLinePlotType.LeveyJennings;
1618         var plotTypeLabel = LABKEY.vis.TrendingLinePlotType[config.qcPlotType];
1619 
1620         if(config.renderTo == null) {
1621             throw new Error("Unable to create " + plotTypeLabel + " plot, renderTo not specified");
1622         }
1623 
1624         if(config.data == null) {
1625             throw new Error("Unable to create " + plotTypeLabel + " plot, data array not specified");
1626         }
1627 
1628         if (config.properties == null || config.properties.xTickLabel == null) {
1629             throw new Error("Unable to create " + plotTypeLabel + " plot, properties object not specified. ");
1630         }
1631 
1632         if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.LeveyJennings) {
1633             if (config.properties.value == null) {
1634                 throw new Error("Unable to create " + plotTypeLabel + " plot, value object not specified. "
1635                         + "Required: value, xTickLabel. Optional: mean, stdDev, color, colorRange, hoverTextFn, mouseOverFn, "
1636                         + "pointClickFn, showTrendLine, showDataPoints, disableRangeDisplay, xTick, yAxisScale, yAxisDomain, xTickTagIndex.");
1637             }
1638         }
1639         else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM) {
1640             if (config.properties.positiveValue == null || config.properties.negativeValue == null) {
1641                 throw new Error("Unable to create " + plotTypeLabel + " plot."
1642                         + "Required: positiveValue, negativeValue, xTickLabel. Optional: positiveValueRight, negativeValueRight, "
1643                         + "xTickTagIndex, showTrendLine, showDataPoints, disableRangeDisplay, xTick, yAxisScale, color, colorRange.");
1644             }
1645         }
1646         else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.MovingRange) {
1647             if (config.properties.valueMR == null) {
1648                 throw new Error("Unable to create " + plotTypeLabel + " plot, value object not specified. "
1649                         + "Required: value, xTickLabel. Optional: meanMR, color, colorRange, hoverTextFn, mouseOverFn, "
1650                         + "pointClickFn, showTrendLine, showDataPoints, disableRangeDisplay, xTick, yAxisScale, yAxisDomain, xTickTagIndex.");
1651             }
1652         }
1653         else {
1654             throw new Error(plotTypeLabel + " plot type is not supported!");
1655         }
1656 
1657         // get a sorted array of the unique x-axis labels
1658         var uniqueXAxisKeys = {}, uniqueXAxisLabels = [];
1659         for (var i = 0; i < config.data.length; i++) {
1660             if (!uniqueXAxisKeys[config.data[i][config.properties.xTick]]) {
1661                 uniqueXAxisKeys[config.data[i][config.properties.xTick]] = true;
1662             }
1663         }
1664         uniqueXAxisLabels =  Object.keys(uniqueXAxisKeys).sort();
1665 
1666         // create a sequencial index to use for the x-axis value and keep a map from that index to the tick label
1667         // also, pull out the meanStdDev data for the unique x-axis values and calculate average values for the (LJ) trend line data
1668         var tickLabelMap = {}, index = -1, distinctColorValues = [], meanStdDevData = [],
1669             groupedTrendlineData = [], groupedTrendlineSeriesData = {},
1670             hasYRightMetric = config.properties.valueRight || config.properties.positiveValueRight || config.properties.valueRightMR;
1671 
1672         for (var i = 0; i < config.data.length; i++)
1673         {
1674             var row = config.data[i];
1675 
1676             // track the distinct values in the color variable so that we know if we need the legend or not
1677             if (config.properties.color && distinctColorValues.indexOf(row[config.properties.color]) == -1) {
1678                 distinctColorValues.push(row[config.properties.color]);
1679             }
1680 
1681             // if we are grouping x values based on the xTick property, only increment index if we have a new xTick value
1682             if (config.properties.xTick)
1683             {
1684                 var addValueToTrendLineData = function(dataArr, seqValue, arrKey, fieldName, rowValue, sumField, countField)
1685                 {
1686                     if (dataArr[arrKey] == undefined)
1687                     {
1688                         dataArr[arrKey] = {
1689                             seqValue: seqValue
1690                         };
1691                     }
1692 
1693                     if (dataArr[arrKey][sumField] == undefined)
1694                     {
1695                         dataArr[arrKey][sumField] = 0;
1696                     }
1697                     if (dataArr[arrKey][countField] == undefined)
1698                     {
1699                         dataArr[arrKey][countField] = 0;
1700                     }
1701 
1702                     if (rowValue != undefined)
1703                     {
1704                         dataArr[arrKey][sumField] += rowValue;
1705                         dataArr[arrKey][countField]++;
1706                         dataArr[arrKey][fieldName] = dataArr[arrKey][sumField] / dataArr[arrKey][countField];
1707                     }
1708                 };
1709 
1710                 var addAllValuesToTrendLineData = function(dataArr, seqValue, arrKey, row, hasYRightMetric)
1711                 {
1712                     var plotValueName = config.properties.value, plotValueNameRight = config.properties.valueRight;
1713                     var plotValueNamePositive = config.properties.positiveValue, plotValueNameRightPositive = config.properties.positiveValueRight;
1714                     if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.MovingRange)
1715                     {
1716                         plotValueName = config.properties.valueMR;
1717                         plotValueNameRight = config.properties.valueRightMR;
1718                     }
1719                     else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM)
1720                     {
1721                         plotValueName = config.properties.negativeValue;
1722                         plotValueNameRight = config.properties.negativeValueRight;
1723                     }
1724 
1725                     addValueToTrendLineData(dataArr, seqValue, arrKey, plotValueName, row[plotValueName], 'sum1', 'count1');
1726                     if (hasYRightMetric)
1727                     {
1728                         addValueToTrendLineData(dataArr, seqValue, arrKey, plotValueNameRight, row[plotValueNameRight], 'sum2', 'count2');
1729                     }
1730 
1731                     if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM)
1732                     {
1733                         addValueToTrendLineData(dataArr, seqValue, arrKey, plotValueNamePositive, row[plotValueNamePositive], 'sum3', 'count3');
1734                         if (hasYRightMetric)
1735                         {
1736                             addValueToTrendLineData(dataArr, seqValue, arrKey, plotValueNameRightPositive, row[plotValueNameRightPositive], 'sum4', 'count4');
1737                         }
1738                     }
1739                 };
1740 
1741                 index = uniqueXAxisLabels.indexOf(row[config.properties.xTick]);
1742 
1743                 // calculate average values for the trend line data (used when grouping x by unique value)
1744                 addAllValuesToTrendLineData(groupedTrendlineData, index, index, row, hasYRightMetric);
1745 
1746                 // calculate average values for trend line data for each series (used when grouping x by unique value with a groupBy series property)
1747                 if (config.properties.groupBy && row[config.properties.groupBy]) {
1748                     var series = row[config.properties.groupBy];
1749                     var key = series + '|' + index;
1750 
1751                     addAllValuesToTrendLineData(groupedTrendlineSeriesData, index, key, row, hasYRightMetric);
1752 
1753                     groupedTrendlineSeriesData[key][config.properties.groupBy] = series;
1754                 }
1755             }
1756             else {
1757                 index++;
1758             }
1759 
1760             tickLabelMap[index] = row[config.properties.xTickLabel];
1761             row.seqValue = index;
1762 
1763             if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.LeveyJennings)
1764             {
1765                 if (config.properties.mean && config.properties.stdDev && !meanStdDevData[index])
1766                 {
1767                     meanStdDevData[index] = row;
1768                 }
1769             }
1770         }
1771 
1772         // min x-axis tick length is 10 by default
1773         var maxSeqValue = config.data.length > 0 ? config.data[config.data.length - 1].seqValue + 1 : 0;
1774         for (var i = maxSeqValue; i < 10; i++)
1775         {
1776             var temp = {type: 'empty', seqValue: i};
1777             temp[config.properties.xTickLabel] = "";
1778             if (config.properties.color && config.data[0]) {
1779                 temp[config.properties.color] = config.data[0][config.properties.color];
1780             }
1781             config.data.push(temp);
1782         }
1783 
1784         // we only need the color aes if there is > 1 distinct value in the color variable
1785         if (distinctColorValues.length < 2 && config.properties.groupBy == undefined) {
1786             config.properties.color = undefined;
1787         }
1788 
1789         config.tickOverlapRotation = 35;
1790 
1791         config.scales = {
1792             color: {
1793                 scaleType: 'discrete',
1794                 range: config.properties.colorRange
1795             },
1796             x: {
1797                 scaleType: 'discrete',
1798                 tickFormat: function(index) {
1799                     // only show a max of 35 labels on the x-axis to avoid overlap
1800                     if (index % Math.ceil(config.data[config.data.length-1].seqValue / 35) == 0) {
1801                         return tickLabelMap[index];
1802                     }
1803                     else {
1804                         return "";
1805                     }
1806                 },
1807                 tickCls: function(index) {
1808                     var baseTag = 'ticklabel';
1809                     var tagIndex = config.properties.xTickTagIndex;
1810                     if (tagIndex != undefined && tagIndex == index) {
1811                         return baseTag+' xticktag';
1812                     }
1813                     return baseTag;
1814                 }
1815             },
1816             yLeft: {
1817                 scaleType: 'continuous',
1818                 domain: config.properties.yAxisDomain,
1819                 trans: config.properties.yAxisScale || 'linear',
1820                 tickFormat: function(val) {
1821                     return LABKEY.vis.isValid(val) && (val > 100000 || val < -100000) ? val.toExponential() : val;
1822                 }
1823             }
1824         };
1825 
1826         if (hasYRightMetric)
1827         {
1828             config.scales.yRight = {
1829                 scaleType: 'continuous',
1830                 domain: config.properties.yAxisDomain,
1831                 trans: config.properties.yAxisScale || 'linear',
1832                 tickFormat: function(val) {
1833                     return LABKEY.vis.isValid(val) && (val > 100000 || val < -100000) ? val.toExponential() : val;
1834                 }
1835             };
1836         }
1837 
1838         // Issue 23626: map line/point color based on legend data
1839         if (config.legendData && config.properties.color && !config.properties.colorRange)
1840         {
1841             var legendColorMap = {};
1842             for (var i = 0; i < config.legendData.length; i++)
1843             {
1844                 if (config.legendData[i].name)
1845                 {
1846                     legendColorMap[config.legendData[i].name] = config.legendData[i].color;
1847                 }
1848             }
1849 
1850             if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM)
1851             {
1852                 config.scales.color = {
1853                     scale: function(group) {
1854                         var normalizedGroup = group.replace('CUSUMmN', 'CUSUMm').replace('CUSUMmP', 'CUSUMm');
1855                         normalizedGroup = normalizedGroup.replace('CUSUMvN', 'CUSUMv').replace('CUSUMvP', 'CUSUMv');
1856                         return legendColorMap[normalizedGroup];
1857                     }
1858                 };
1859             }
1860             else
1861             {
1862                 config.scales.color = {
1863                     scale: function(group) {
1864                         return legendColorMap[group];
1865                     }
1866                 };
1867             }
1868         }
1869 
1870         if(!config.margins) {
1871             config.margins = {};
1872         }
1873 
1874         if(!config.margins.top) {
1875             config.margins.top = config.labels && config.labels.main ? 30 : 10;
1876         }
1877 
1878         if(!config.margins.right) {
1879             config.margins.right = (config.properties.color || (config.legendData && config.legendData.length > 0) ? 190 : 40)
1880                                     + (hasYRightMetric ? 45 : 0);
1881         }
1882 
1883         if(!config.margins.bottom) {
1884             config.margins.bottom = config.labels && config.labels.x ? 75 : 55;
1885         }
1886 
1887         if(!config.margins.left) {
1888             config.margins.left = config.labels && config.labels.y ? 75 : 55;
1889         }
1890 
1891         config.aes = {
1892             x: 'seqValue'
1893         };
1894 
1895         // determine the width the error bars
1896         if (config.properties.disableRangeDisplay) {
1897             config.layers = [];
1898         }
1899         else {
1900             var barWidth = Math.max(config.width / config.data[config.data.length-1].seqValue / 5, 3);
1901 
1902             if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.LeveyJennings) {
1903 
1904                 // +/- 3 standard deviation displayed using the ErrorBar geom with different colors
1905                 var stdDev3Layer = new LABKEY.vis.Layer({
1906                     geom: new LABKEY.vis.Geom.ErrorBar({size: 1, color: 'red', dashed: true, altColor: 'darkgrey', width: barWidth}),
1907                     data: meanStdDevData,
1908                     aes: {
1909                         error: function(row){return row[config.properties.stdDev] * 3;},
1910                         yLeft: config.properties.mean
1911                     }
1912                 });
1913                 var stdDev2Layer = new LABKEY.vis.Layer({
1914                     geom: new LABKEY.vis.Geom.ErrorBar({size: 1, color: 'blue', dashed: true, altColor: 'darkgrey', width: barWidth}),
1915                     data: meanStdDevData,
1916                     aes: {
1917                         error: function(row){return row[config.properties.stdDev] * 2;},
1918                         yLeft: config.properties.mean
1919                     }
1920                 });
1921                 var stdDev1Layer = new LABKEY.vis.Layer({
1922                     geom: new LABKEY.vis.Geom.ErrorBar({size: 1, color: 'green', dashed: true, altColor: 'darkgrey', width: barWidth}),
1923                     data: meanStdDevData,
1924                     aes: {
1925                         error: function(row){return row[config.properties.stdDev];},
1926                         yLeft: config.properties.mean
1927                     }
1928                 });
1929                 var meanLayer = new LABKEY.vis.Layer({
1930                     geom: new LABKEY.vis.Geom.ErrorBar({size: 1, color: 'darkgrey', width: barWidth}),
1931                     data: meanStdDevData,
1932                     aes: {
1933                         error: function(row){return 0;},
1934                         yLeft: config.properties.mean
1935                     }
1936                 });
1937                 config.layers = [stdDev3Layer, stdDev2Layer, stdDev1Layer, meanLayer];
1938             }
1939             else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM) {
1940                 var range = new LABKEY.vis.Layer({
1941                     geom: new LABKEY.vis.Geom.ControlRange({size: 1, color: 'red', dashed: true, altColor: 'darkgrey', width: barWidth}),
1942                     data: config.data,
1943                     aes: {
1944                         upper: function(){return LABKEY.vis.Stat.CUSUM_CONTROL_LIMIT;},
1945                         lower: function(){return LABKEY.vis.Stat.CUSUM_CONTROL_LIMIT_LOWER;},
1946                         yLeft: config.properties.mean
1947                     }
1948                 });
1949                 config.layers = [range];
1950             }
1951             else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.MovingRange) {
1952                 var range = new LABKEY.vis.Layer({
1953                     geom: new LABKEY.vis.Geom.ControlRange({size: 1, color: 'red', dashed: true, altColor: 'darkgrey', width: barWidth}),
1954                     data: config.data,
1955                     aes: {
1956                         upper: function(row){return row[config.properties.meanMR] * LABKEY.vis.Stat.MOVING_RANGE_UPPER_LIMIT_WEIGHT;},
1957                         lower: function(){return LABKEY.vis.Stat.MOVING_RANGE_LOWER_LIMIT;},
1958                         yLeft: config.properties.mean
1959                     }
1960                 });
1961                 config.layers = [range];
1962             }
1963         }
1964 
1965         if (config.properties.showTrendLine)
1966         {
1967             var getPathLayerConfig = function(ySide, valueName, colorValue, negativeCusum)
1968             {
1969                 var pathLayerConfig = {
1970                     geom: new LABKEY.vis.Geom.Path({
1971                         opacity: .6,
1972                         size: 2,
1973                         dashed: config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM && !negativeCusum
1974                     }),
1975                     aes: {}
1976                 };
1977 
1978                 pathLayerConfig.aes[ySide] = valueName;
1979 
1980                 // if we aren't showing multiple series data via the group by, use the groupedTrendlineData for the path
1981                 if (config.properties.groupBy)
1982                 {
1983                     // convert the groupedTrendlineSeriesData object into an array of the object values
1984                     var seriesDataArr = [];
1985                     for(var i in groupedTrendlineSeriesData) {
1986                         if (groupedTrendlineSeriesData.hasOwnProperty(i)) {
1987                             var d = { seqValue: groupedTrendlineSeriesData[i].seqValue };
1988                             d[config.properties.groupBy] = groupedTrendlineSeriesData[i][config.properties.groupBy] + (hasYRightMetric ? '|' + valueName : '');
1989                             d[valueName] = groupedTrendlineSeriesData[i][valueName];
1990                             seriesDataArr.push(d);
1991                         }
1992                     }
1993                     pathLayerConfig.data = seriesDataArr;
1994 
1995                     pathLayerConfig.aes.pathColor = config.properties.groupBy;
1996                     pathLayerConfig.aes.group = config.properties.groupBy;
1997                 }
1998                 else
1999                 {
2000                     pathLayerConfig.data = groupedTrendlineData;
2001 
2002                     if (colorValue != undefined)
2003                     {
2004                         pathLayerConfig.aes.pathColor = function(data) {
2005                             return colorValue;
2006                         }
2007                     }
2008                 }
2009 
2010                 return pathLayerConfig;
2011             };
2012 
2013             if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM)
2014             {
2015                 if (hasYRightMetric)
2016                 {
2017                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.negativeValue, 1, true)));
2018                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yRight', config.properties.negativeValueRight, 0, true)));
2019                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.positiveValue, 1, false)));
2020                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yRight', config.properties.positiveValueRight, 0, false)));
2021                 }
2022                 else
2023                 {
2024                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.negativeValue, undefined, true)));
2025                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.positiveValue, undefined, false)));
2026                 }
2027             }
2028             else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.MovingRange)
2029             {
2030                 if (hasYRightMetric)
2031                 {
2032                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.valueMR, 0)));
2033                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yRight', config.properties.valueRightMR, 1)));
2034                 }
2035                 else
2036                 {
2037                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.valueMR)));
2038                 }
2039             }
2040             else
2041             {
2042                 if (hasYRightMetric)
2043                 {
2044                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.value, 0)));
2045                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yRight', config.properties.valueRight, 1)));
2046                 }
2047                 else
2048                 {
2049                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.value)));
2050                 }
2051             }
2052         }
2053 
2054         // points based on the data value, color and hover text can be added via params to config
2055         var getPointLayerConfig = function(ySide, valueName, colorValue)
2056         {
2057             var pointLayerConfig = {
2058                 geom: new LABKEY.vis.Geom.Point({
2059                     position: config.properties.position,
2060                     opacity: config.properties.pointOpacityFn,
2061                     size: 3
2062                 }),
2063                 aes: {}
2064             };
2065 
2066             pointLayerConfig.aes[ySide] = valueName;
2067 
2068             if (config.properties.color) {
2069                 pointLayerConfig.aes.color = function(row) {
2070                     return row[config.properties.color] + (hasYRightMetric ? '|' + valueName : '');
2071                 };
2072             }
2073             else if (colorValue != undefined) {
2074                 pointLayerConfig.aes.color = function(row){ return colorValue; };
2075             }
2076 
2077             if (config.properties.shape) {
2078                 pointLayerConfig.aes.shape = config.properties.shape;
2079             }
2080             if (config.properties.hoverTextFn) {
2081                 pointLayerConfig.aes.hoverText = function(row) {
2082                     return config.properties.hoverTextFn.call(this, row, valueName);
2083                 };
2084             }
2085             if (config.properties.pointClickFn) {
2086                 pointLayerConfig.aes.pointClickFn = config.properties.pointClickFn;
2087             }
2088 
2089             // add some mouse over effects to highlight selected point
2090             pointLayerConfig.aes.mouseOverFn = function(event, pointData, layerSel, point) {
2091                 d3.select(event.srcElement).transition().duration(800).attr("stroke-width", 5).ease("elastic");
2092 
2093                 if (config.properties.mouseOverFn) {
2094                     config.properties.mouseOverFn.call(config.properties.mouseOverFnScope || this, event, pointData, layerSel, point, valueName);
2095                 }
2096             };
2097             pointLayerConfig.aes.mouseOutFn = function(event, pointData, layerSel) {
2098                 d3.select(event.srcElement).transition().duration(800).attr("stroke-width", 1).ease("elastic");
2099             };
2100 
2101             if (config.properties.pointIdAttr) {
2102                 pointLayerConfig.aes.pointIdAttr = config.properties.pointIdAttr;
2103             }
2104 
2105             return pointLayerConfig;
2106         };
2107 
2108         if (config.properties.showDataPoints == undefined) {
2109             config.properties.showDataPoints = true;
2110         }
2111 
2112         if (config.properties.showDataPoints) {
2113             if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM) {
2114                 if (hasYRightMetric) {
2115                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.negativeValue, 1)));
2116                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yRight', config.properties.negativeValueRight, 0)));
2117                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.positiveValue, 1)));
2118                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yRight', config.properties.positiveValueRight, 0)));
2119 
2120                 }
2121                 else {
2122                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.negativeValue)));
2123                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.positiveValue)));
2124                 }
2125             }
2126             else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.MovingRange) {
2127                 if (hasYRightMetric) {
2128                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.valueMR, 0)));
2129                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yRight', config.properties.valueRightMR, 1)));
2130                 }
2131                 else {
2132                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.valueMR)));
2133                 }
2134             }
2135             else {
2136                 if (hasYRightMetric) {
2137                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.value, 0)));
2138                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yRight', config.properties.valueRight, 1)));
2139                 }
2140                 else {
2141                     config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.value)));
2142                 }
2143             }
2144         }
2145 
2146         return new LABKEY.vis.Plot(config);
2147     };
2148 
2149     LABKEY.vis.TrendingLineShape = {
2150         positiveCUSUM: function(){
2151             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";
2152         },
2153         negativeCUSUM: function(){
2154             return "M-9,-0.5L6,-0.5 6,0.5 -9,0.5Z";
2155         }
2156     };
2157 
2158     /**
2159      * @ Deprecated
2160      */
2161     LABKEY.vis.LeveyJenningsPlot = LABKEY.vis.TrendingLinePlot;
2162 })();
2163 
2164 /**
2165  * @name LABKEY.vis.SurvivalCurvePlot
2166  * @class SurvivalCurvePlot Wrapper to create a plot which shows survival curve step lines and censor points (based on output from R survival package).
2167  * @description This helper will take the input data and generate stepwise data points for use with the Path geom.
2168  * @param {Object} config An object that contains the following properties
2169  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
2170  * @param {Number} [config.width] The chart canvas width in pixels.
2171  * @param {Number} [config.height] The chart canvas height in pixels.
2172  * @param {Array} [config.data] The array of step data for the survival curves.
2173  * @param {String} [config.groupBy] (optional) The data array object property used to group plot lines and points.
2174  * @param {Array} [config.censorData] The array of censor data to overlay on the survival step lines.
2175  * @param {Function} [config.censorHoverText] (optional) Function defining the hover text to display for the censor data points.
2176  */
2177 (function(){
2178 
2179     LABKEY.vis.SurvivalCurvePlot = function(config){
2180 
2181         if (config.renderTo == null){
2182             throw new Error("Unable to create survival curve plot, renderTo not specified.");
2183         }
2184 
2185         if (config.data == null || config.censorData == null){
2186             throw new Error("Unable to create survival curve plot, data and/or censorData array not specified.");
2187         }
2188 
2189         if (config.aes == null || config.aes.x == null || config.aes.yLeft == null) {
2190             throw new Error("Unable to create survival curve plot, aes (x and yLeft) not specified.")
2191         }
2192 
2193         // Convert data array for step-wise line plot
2194         var stepData = [];
2195         var groupBy = config.groupBy;
2196         var aesX = config.aes.x;
2197         var aesY = config.aes.yLeft;
2198 
2199         for (var i=0; i<config.data.length; i++)
2200         {
2201             stepData.push(config.data[i]);
2202 
2203             if ( (i<config.data.length-1) && (config.data[i][groupBy] == config.data[i+1][groupBy])
2204                     && (config.data[i][aesX] != config.data[i+1][aesX])
2205                     && (config.data[i][aesY] != config.data[i+1][aesY]))
2206             {
2207                 var point = {};
2208                 point[groupBy] = config.data[i][groupBy];
2209                 point[aesX] = config.data[i+1][aesX];
2210                 point[aesY] = config.data[i][aesY];
2211                 stepData.push(point);
2212             }
2213         }
2214         config.data = stepData;
2215 
2216         config.layers = [
2217             new LABKEY.vis.Layer({
2218                 geom: new LABKEY.vis.Geom.Path({size:2, opacity:1}),
2219                 aes: {
2220                     pathColor: config.groupBy,
2221                     group: config.groupBy
2222                 }
2223             }),
2224             new LABKEY.vis.Layer({
2225                 geom: new LABKEY.vis.Geom.Point({opacity:1}),
2226                 data: config.censorData,
2227                 aes: {
2228                     color: config.groupBy,
2229                     hoverText: config.censorHoverText,
2230                     shape: config.groupBy
2231                 }
2232 
2233             })
2234         ];
2235 
2236         if (!config.scales) config.scales = {};
2237         config.scales.x = { scaleType: 'continuous', trans: 'linear' };
2238         config.scales.yLeft = { scaleType: 'continuous', trans: 'linear', domain: [0, 1] };
2239 
2240         config.aes.mouseOverFn = function(event, pointData, layerSel) {
2241             mouseOverFn(event, pointData, layerSel, config.groupBy);
2242         };
2243 
2244         config.aes.mouseOutFn = mouseOutFn;
2245 
2246         return new LABKEY.vis.Plot(config);
2247     };
2248 
2249     var mouseOverFn = function(event, pointData, layerSel, subjectColumn) {
2250         var points = layerSel.selectAll('.point path');
2251         var lines = d3.selectAll('path.line');
2252 
2253         var opacityAcc = function(d) {
2254             if (d[subjectColumn] && d[subjectColumn] == pointData[subjectColumn])
2255             {
2256                 return 1;
2257             }
2258             return .3;
2259         };
2260 
2261         points.attr('fill-opacity', opacityAcc).attr('stroke-opacity', opacityAcc);
2262         lines.attr('fill-opacity', opacityAcc).attr('stroke-opacity', opacityAcc);
2263     };
2264 
2265     var mouseOutFn = function(event, pointData, layerSel) {
2266         layerSel.selectAll('.point path').attr('fill-opacity', 1).attr('stroke-opacity', 1);
2267         d3.selectAll('path.line').attr('fill-opacity', 1).attr('stroke-opacity', 1);
2268     };
2269 })();
2270 
2271 /**
2272  * @name LABKEY.vis.TimelinePlot
2273  * @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.
2274  * @param {Object} config An object that contains the following properties
2275  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
2276  * @param {Number} [config.width] The chart canvas width in pixels.
2277  * @param {Number} [config.height] The chart canvas height in pixels.
2278  * @param {Array} [config.data] The array of event data including event types and subtypes for the plot.
2279  * @param {String} [config.gridLinesVisible] Possible options are 'y', 'x', and 'both' to determine which sets of
2280  *                  grid lines are rendered on the plot. Default is 'both'.
2281  * @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}
2282  * @param {Date} [config.options.startDate] (Optional) The start date to use to calculate number of days until event date.
2283  * @param {Object} [config.options] (Optional) Display options as defined in {@link LABKEY.vis.Geom.TimelinePlot}.
2284  * @param {Boolean} [config.options.isCollapsed] (Optional) If true, the timeline collapses subtypes into their parent rows. Defaults to True.
2285  * @param {Number} [config.options.rowHeight] (Optional) The height of individual rows in pixels. For expanded timelines,
2286  *                  row height will resize to 75% of this value. Defaults to 1.
2287  * @param {Object} [config.options.highlight] (Optional) Special Data object containing information to highlight a specific
2288  *                  row in the timeline. Must have the same shape & properties as all other input data.
2289  * @param {String} [config.options.highlightRowColor] (Optional) Hex color to specify what color the highlighted row will
2290  *                  be if, found in the data. Defaults to #74B0C4.
2291  * @param {String} [config.options.activeEventKey] (Optional) Name of property that is paired with
2292  *                  @param config.options.activeEventIdentifier to identify a unique event in the data.
2293  * @param {String} [config.options.activeEventIdentifier] (Optional) Name of value that is paired with
2294  *                  @param config.options.activeEventKey to identify a unique event in the data.
2295  * @param {String} [config.options.activeEventStrokeColor] (Optional) Hex color to specify what color the active event
2296  *                  rect's stroke will be, if found in the data. Defaults to Red.
2297  * @param {Object} [config.options.emphasisEvents] (Optional) Object containing key:[value] pairs whose keys are property
2298  *                  names of a data object and whose value is an array of possible values that should have a highlight
2299 *                   line drawn on the chart when found. Example: {'type': ['death', 'Withdrawal']}
2300  * @param {String} [config.options.tickColor] (Optional) Hex color to specify the color of Axis ticks. Defaults to #DDDDDD.
2301  * @param {String} [config.options.emphasisTickColor] (Optional) Hex color to specify the color of emphasis event ticks,
2302  *                  if found in the data. Defaults to #1a969d.
2303  * @param {String} [config.options.timeUnit] (Optional) Unit of time to use when calculating how far an event's date
2304  *                  is from the start date. Default is years. Valid string values include minutes, hours, days, years, and decades.
2305  * @param {Number} [config.options.eventIconSize] (Optional) Size of event square width/height dimensions.
2306  * @param {String} [config.options.eventIconColor] (Optional) Hex color of event square stroke. Defaults to black (#0000000).
2307  * @param {String} [config.options.eventIconFill] (Optional) Hex color of event square inner fill. Defaults to black (#000000)..
2308  * @param {Float} [config.options.eventIconOpacity] (Optional) Float between 0 - 1 (inclusive) to specify how transparent the
2309  *                  fill of event icons will be. Defaults to 1.
2310  * @param {Array} [config.options.rowColorDomain] (Optional) Array of length 2 containing string Hex values for the two
2311  *                  alternating colors of timeline row rectangles. Defaults to ['#f2f2f2', '#ffffff'].
2312  */
2313 (function(){
2314 
2315     LABKEY.vis.TimelinePlot = function(config)
2316     {
2317         if (config.renderTo == undefined || config.renderTo == null) { throw new Error("Unable to create timeline plot, renderTo not specified."); }
2318 
2319         if (config.data == undefined || config.data == null) { throw new Error("Unable to create timeline plot, data array not specified."); }
2320 
2321         if (config.width == undefined || config.width == null) { throw new Error("Unable to create timeline plot, width not specified."); }
2322 
2323         if (!config.aes.y) {
2324             config.aes.y = 'key';
2325         }
2326 
2327         if (!config.options) {
2328             config.options = {};
2329         }
2330 
2331         //default x scale is in years
2332         if (!config.options.timeUnit) {
2333             config.options.timeUnit = 'years';
2334         }
2335 
2336         //set default left margin to make room for event label text
2337         if (!config.margins.left) {
2338             config.margins.left = 200
2339         }
2340 
2341         //default row height value
2342         if (!config.options.rowHeight) {
2343             config.options.rowHeight = 40;
2344         }
2345 
2346         //override default plot values if not set
2347         if (!config.margins.top) {
2348             config.margins.top = 40;
2349         }
2350         if (!config.margins.bottom) {
2351             config.margins.bottom = 50;
2352         }
2353         if (!config.gridLineWidth) {
2354             config.gridLineWidth = 1;
2355         }
2356         if (!config.gridColor) {
2357             config.gridColor = '#FFFFFF';
2358         }
2359         if (!config.borderColor) {
2360             config.borderColor = '#DDDDDD';
2361         }
2362         if (!config.tickColor) {
2363             config.tickColor = '#DDDDDD';
2364         }
2365 
2366         config.rendererType = 'd3';
2367         config.options.marginLeft = config.margins.left;
2368         config.options.parentName = config.aes.parentName;
2369         config.options.childName = config.aes.childName;
2370         config.options.dateKey = config.aes.x;
2371 
2372         config.scales = {
2373             x: {
2374                 scaleType: 'continuous'
2375             },
2376             yLeft: {
2377                 scaleType: 'discrete'
2378             }
2379         };
2380 
2381         var millis;
2382         switch(config.options.timeUnit.toLowerCase())
2383         {
2384             case 'minutes':
2385                 millis = 1000 * 60;
2386                 break;
2387             case 'hours':
2388                 millis = 1000 * 60 * 60;
2389                 break;
2390             case 'days':
2391                 millis = 1000 * 60 * 60 * 24;
2392                 break;
2393             case 'months':
2394                 millis = 1000 * 60 * 60 * 24 * 30.42;
2395                 break;
2396             case 'years':
2397                 millis = 1000 * 60 * 60 * 24 * 365;
2398                 break;
2399             case 'decades':
2400                 millis = 1000 * 60 * 60 * 24 * 365 * 10;
2401                 break;
2402             default:
2403                 millis = 1000;
2404         }
2405 
2406         //find the earliest occurring date in the data if startDate is not already specified
2407         var min = config.options.startDate ? config.options.startDate : null;
2408         if (min == null)
2409         {
2410             for (var i = 0; i < config.data.length; i++)
2411             {
2412                 config.data[i][config.aes.x] = new Date(config.data[i][config.aes.x]);
2413                 if (min == null)
2414                 {
2415                     min = config.data[i][config.aes.x];
2416                 }
2417                 min = config.data[i][config.aes.x] < min ? config.data[i][config.aes.x] : min;
2418             }
2419         }
2420 
2421         //Loop through the data and do calculations for each entry
2422         var max = 0;
2423         var parents = new Set();
2424         var children = new Set();
2425         var types = new Set();
2426         var domain = [];
2427         for (var j = 0; j < config.data.length; j++)
2428         {
2429             //calculate difference in time units
2430             var d = config.data[j];
2431             d[config.aes.x] = config.options.startDate ? new Date(d[config.aes.x]) : d[config.aes.x];
2432             var timeDifference = (d[config.aes.x] - min) / millis;
2433             d[config.options.timeUnit] = timeDifference;
2434 
2435             //update unique counts
2436             parents.add(d[config.aes.parentName]);
2437             children.add(d[config.aes.childName]);
2438 
2439             //update domain
2440             if (!config.options.isCollapsed) {
2441                 var str;
2442                 if (d[config.aes.parentName] != null && d[config.aes.parentName] != 'null' && d[config.aes.parentName] != undefined) {
2443                     str = d[config.aes.parentName];
2444                     if (!types.has(str) && str != undefined) {
2445                         domain.push(str);
2446                         types.add(str);
2447                     }
2448                     d.typeSubtype = str;
2449                 }
2450                 if (d[config.aes.childName] != null && d[config.aes.childName] != 'null' && d[config.aes.childName] != undefined) {
2451                     str += '-' + d[config.aes.childName];
2452                 }
2453                 if (!types.has(str) && str != undefined) {
2454                     domain.push(str);
2455                     types.add(str);
2456                 }
2457 
2458                 //typeSubtype will be a simple unique identifier for this type & subtype of event
2459                 d.typeSubtype = str;
2460             } else {
2461                 if (!types.has(d[config.aes.parentName])) {
2462                     domain.push(d[config.aes.parentName]);
2463                     types.add(d[config.aes.parentName]);
2464                 }
2465             }
2466 
2467             //update max value
2468             max = timeDifference > max ? timeDifference : max;
2469         }
2470         var numParentChildUniques = parents.size + children.size;
2471         if (children.has(null)) {
2472             numParentChildUniques--;
2473         }
2474         var numParentUniques = parents.size;
2475         domain.sort().reverse();
2476 
2477         //For a better looking title
2478         function capitalizeFirstLetter(string) {
2479             return string.charAt(0).toUpperCase() + string.slice(1);
2480         }
2481 
2482         //Update x label to include the start date for better context
2483         config.labels.x = {value: capitalizeFirstLetter(config.options.timeUnit) + " Since " + min.toDateString()};
2484 
2485         if (!config.options.isCollapsed) {
2486             config.aes.typeSubtype = "typeSubtype";
2487 
2488             config.scales.yLeft.domain = domain;
2489             var chartHeightMultiplier = numParentChildUniques !== numParentUniques ? Math.floor(config.options.rowHeight * .75) : config.options.rowHeight;
2490             config.height = (chartHeightMultiplier * numParentChildUniques) + config.margins.top + config.margins.bottom;
2491             config.options.rowHeight = (config.height - (config.margins.top + config.margins.bottom)) / numParentChildUniques;
2492             if (numParentChildUniques < 10) {
2493                 //small visual adjustment for short charts without many data points
2494                 config.options.rowHeight = config.options.rowHeight - (12 - numParentChildUniques);
2495             }
2496         } else {
2497             config.scales.yLeft.domain = domain;
2498             config.height = (config.options.rowHeight * numParentUniques) + config.margins.top + config.margins.bottom;
2499 
2500             config.options.rowHeight = (config.height - (config.margins.top + config.margins.bottom)) / numParentUniques;
2501             if (numParentUniques < 10) {
2502                 config.options.rowHeight = config.options.rowHeight - (12 - numParentUniques);
2503             }
2504         }
2505 
2506         config.scales.x.domain = [0, Math.ceil(max)];
2507         config.aes.x = config.options.timeUnit;
2508         config.layers = [
2509             new LABKEY.vis.Layer({
2510                 geom: new LABKEY.vis.Geom.TimelinePlot(config.options)
2511             })
2512         ];
2513 
2514         return new LABKEY.vis.Plot(config);
2515     };
2516 })();
2517 
2518