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-2016 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          * Clears the grid.
1144          */
1145         this.clearGrid = function(){
1146             this.renderer.clearGrid();
1147         };
1148 
1149         /**
1150          * Sets new margins for the plot and re-renders with the margins.
1151          * @param {Object} newMargins An object with the following properties:
1152          *      <ul>
1153          *          <li><strong>top:</strong> Size of top margin in pixels.</li>
1154          *          <li><strong>bottom:</strong> Size of bottom margin in pixels.</li>
1155          *          <li><strong>left:</strong> Size of left margin in pixels.</li>
1156          *          <li><strong>right:</strong> Size of right margin in pixels.</li>
1157          *      </ul>
1158          */
1159         this.setMargins = function(newMargins, render){
1160             userMargins = newMargins;
1161             margins = initMargins(userMargins, this.legendPos, allAes, this.scales, this.labels);
1162 
1163             if(render !== undefined && render !== null && render === true) {
1164                 this.render();
1165             }
1166         };
1167 
1168         this.setAes = function(newAes){
1169             // Note: this is only valid for plots using the D3Renderer.
1170             // Used to add or remove aesthetics to a plot. Also availalbe on LABKEY.vis.Layer objects to set aesthetics on
1171             // specific layers only.
1172             // To delete an aesthetic set it to null i.e. plot.setAes({color: null});
1173             LABKEY.vis.mergeAes(this.aes, newAes);
1174             this.render();
1175         };
1176 
1177         this.setBrushing = function(configBrushing) {
1178             this.renderer.setBrushing(configBrushing);
1179         };
1180 
1181         this.clearBrush = function() {
1182             if(this.renderer.clearBrush) {
1183                 this.renderer.clearBrush();
1184             }
1185         };
1186 
1187         this.getBrushExtent = function() {
1188             // Returns an array of arrays. First array is xMin, yMin, second array is xMax, yMax
1189             // If the seleciton is 1D, then the min/max of the non-selected dimension will be null/null.
1190             return this.renderer.getBrushExtent();
1191         };
1192 
1193         this.setBrushExtent = function(extent) {
1194             // Takes a 2D array. First array is xMin, yMin, second array is xMax, yMax. If the seleciton is 1D, then the
1195             // min/max of the non-selected dimension will be null/null.
1196             this.renderer.setBrushExtent(extent);
1197         };
1198 
1199         this.bindBrushing = function(otherPlots) {
1200             this.renderer.bindBrushing(otherPlots);
1201         };
1202 
1203         return this;
1204     };
1205 })();
1206 
1207 /**
1208  * @name LABKEY.vis.BarPlot
1209  * @class BarPlot wrapper to allow a user to easily create a simple bar plot without having to preprocess the data.
1210  * @param {Object} config An object that contains the following properties (in addition to those properties defined
1211  *      in {@link LABKEY.vis.Plot}).
1212  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
1213  * @param {Array} [config.data] The array of individual data points to be grouped for the bar plot. The LABKEY.vis.BarPlot
1214  *      wrapper will aggregate the data in this array based on the xAes function provided to get the individual totals
1215  *      for each bar in the plot.
1216  * @param {Function} [config.xAes] The function to determine which groups will be created for the x-axis of the plot.
1217  * @param {Object} [config.options] (Optional) Display options as defined in {@link LABKEY.vis.Geom.BarPlot}.
1218  *
1219  @example
1220  <div id='bar'></div>
1221  <script type="text/javascript">
1222  // Fake data which will be aggregated by the LABKEY.vis.BarPlot wrapper.
1223  var barPlotData = [
1224     {gender: 'Male', age: '21'}, {gender: 'Male', age: '43'},
1225     {gender: 'Male', age: '24'}, {gender: 'Male', age: '54'},
1226     {gender: 'Female', age: '24'}, {gender: 'Female', age: '33'},
1227     {gender: 'Female', age: '43'}, {gender: 'Female', age: '43'},
1228  ];
1229 
1230  // Create a new bar plot object.
1231  var barChart = new LABKEY.vis.BarPlot({
1232     renderTo: 'bar',
1233     rendererType: 'd3',
1234     width: 900,
1235     height: 300,
1236     labels: {
1237         main: {value: 'Example Bar Plot With Cumulative Totals'},
1238         yLeft: {value: 'Count'},
1239         x: {value: 'Value'}
1240     },
1241     options: {
1242         color: 'black',
1243         fill: '#c0c0c0',
1244         lineWidth: 1.5,
1245         colorTotal: 'black',
1246         fillTotal: 'steelblue',
1247         opacityTotal: .8,
1248         showCumulativeTotals: true,
1249         showValues: true
1250     },
1251     xAes: function(row){return row['age']},
1252     data: barPlotData
1253  });
1254 
1255  barChart.render();
1256  </script>
1257  */
1258 (function(){
1259 
1260     LABKEY.vis.BarPlot = function(config){
1261 
1262         if(config.renderTo == null){
1263             throw new Error("Unable to create bar plot, renderTo not specified");
1264         }
1265         if(config.data == null){
1266             throw new Error("Unable to create bar plot, data array not specified");
1267         }
1268         if (!config.aes) {
1269             config.aes = {};
1270         }
1271         if (config.xAes) { //backwards compatibility
1272             config.aes.x = config.xAes;
1273         }
1274         if (!config.aes.x) {
1275             throw new Error("Unable to create bar plot, x Aesthetic not specified");
1276         }
1277         if (config.xSubAes) {
1278             config.aes.xSub = config.xSubAes;
1279         }
1280         if (config.yAes) {
1281             config.aes.y = config.yAes;
1282         }
1283         if (!config.options) {
1284             config.options = {};
1285         }
1286         if (!config.aggregateType) {
1287             config.options.aggregateType = config.aes.y ? 'SUM' : 'COUNT'; //aggregate defaults
1288         }
1289 
1290         var showCumulativeTotals = config.options && config.options.showCumulativeTotals;
1291         if (showCumulativeTotals && config.aes.xSub) {
1292             throw new Error("Unable to render grouped bar chart with cumulative totals shown");
1293         }
1294 
1295         var aggregateData,
1296                 dimName = config.aes.x,
1297                 subDimName = config.aes.xSub,
1298                 aggType = config.options.aggregateType,
1299                 measureName = config.aes.y,
1300                 includeTotal = config.options.showCumulativeTotals;
1301 
1302         aggregateData = LABKEY.vis.getAggregateData(config.data, dimName, subDimName, measureName, aggType, '[blank]', includeTotal);
1303         config.aes.y = 'value';
1304         config.aes.x = 'label';
1305         if (subDimName) {
1306             config.aes.xSub = 'subLabel';
1307             config.aes.color = 'label';
1308         }
1309 
1310         config.layers = [new LABKEY.vis.Layer({
1311             geom: new LABKEY.vis.Geom.BarPlot(config.options),
1312             data: aggregateData,
1313             aes: config.aes
1314         })];
1315 
1316         if (!config.scales) {
1317             config.scales = {};
1318         }
1319         if (!config.scales.x) {
1320             config.scales.x = { scaleType: 'discrete' };
1321         }
1322         if (subDimName && !config.scales.xSub) {
1323             config.scales.xSub = { scaleType: 'discrete' };
1324         }
1325         if (!config.scales.y) {
1326             var domainMax = aggregateData.length == 0 ? 1 : null;
1327             if (showCumulativeTotals)
1328                 domainMax = aggregateData[aggregateData.length-1].total;
1329 
1330             config.scales.y = {
1331                 scaleType: 'continuous',
1332                 domain: [0, domainMax]
1333             };
1334         }
1335 
1336         if (showCumulativeTotals && !config.margins)
1337         {
1338             config.margins = {right: 125};
1339         }
1340 
1341         return new LABKEY.vis.Plot(config);
1342     };
1343 })();
1344 
1345 /**
1346  * @name LABKEY.vis.PieChart
1347  * @class PieChart which allows a user to programmatically create an interactive pie chart visualization (note: D3 rendering only).
1348  * @description The pie chart visualization is built off of the <a href="http://d3pie.org">d3pie JS library</a>. The config
1349  *      properties listed below are the only required properties to create a base pie chart. For additional display options
1350  *      and interaction options, you can provide any of the properties defined in the <a href="http://d3pie.org/#docs">d3pie docs</a>
1351  *      to the config object.
1352  * @param {Object} config An object that contains the following properties
1353  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
1354  * @param {Array} [config.data] The array of chart segment data. Each object is of the form: { label: "label", value: 123 }.
1355  * @param {Number} [config.width] The chart canvas width in pixels.
1356  * @param {Number} [config.height] The chart canvas height in pixels.
1357  *
1358  @example
1359  Example of a simple pie chart (only required config properties).
1360 
1361  <div id='pie'></div>
1362  <script type="text/javascript">
1363 
1364  </script>
1365  var pieChartData = [
1366      {label: "test1", value: 1},
1367      {label: "test2", value: 2},
1368      {label: "test3", value: 3},
1369      {label: "test4", value: 4}
1370  ];
1371 
1372  var pieChart = new LABKEY.vis.PieChart({
1373     renderTo: "pie",
1374     data: pieChartData,
1375     width: 300,
1376     height: 250
1377  });
1378 
1379  Example of a customized pie chart using some d3pie lib properties.
1380 
1381  <div id='pie2'></div>
1382  <script type="text/javascript">
1383  var pieChartData = [
1384      {label: "test1", value: 1},
1385      {label: "test2", value: 2},
1386      {label: "test3", value: 3},
1387      {label: "test4", value: 4}
1388  ];
1389 
1390  var pieChart2 = new LABKEY.vis.PieChart({
1391     renderTo: "pie2",
1392     data: pieChartData,
1393     width: 300,
1394     height: 250,
1395     // d3pie lib config properties
1396     header: {
1397         title: {
1398             text: 'Pie Chart Example'
1399         }
1400     },
1401     labels: {
1402         outer: {
1403             format: 'label-value2',
1404             pieDistance: 15
1405         },
1406         inner: {
1407             hideWhenLessThanPercentage: 10
1408         },
1409         lines: {
1410             style: 'straight',
1411             color: 'black'
1412         }
1413     },
1414     effects: {
1415         load: {
1416             speed: 2000
1417         },
1418         pullOutSegmentOnClick: {
1419             effect: 'linear',
1420             speed: '1000'
1421         },
1422         highlightLuminosity: -0.75
1423     },
1424     misc: {
1425         colors: {
1426             segments: LABKEY.vis.Scale.DarkColorDiscrete(),
1427             segmentStroke: '#a1a1a1'
1428         },
1429         gradient: {
1430             enabled: true,
1431             percentage: 60
1432         }
1433     },
1434     callbacks: {
1435         onload: function() {
1436             pieChart2.openSegment(3);
1437         }
1438     }
1439  });
1440  </script>
1441  */
1442 (function(){
1443 
1444     LABKEY.vis.PieChart = function(config){
1445 
1446         if(config.renderTo == null){
1447             throw new Error("Unable to create pie chart, renderTo not specified");
1448         }
1449 
1450         if(config.data == null){
1451             throw new Error("Unable to create pie chart, data not specified");
1452         }
1453         else if (Array.isArray(config.data)) {
1454             config.data = {content : config.data, sortOrder: 'value-desc'};
1455         }
1456 
1457         if(config.width == null && (config.size == null || config.size.canvasWidth == null)){
1458             throw new Error("Unable to create pie chart, width not specified");
1459         }
1460         else if(config.height == null && (config.size == null || config.size.canvasHeight == null)){
1461             throw new Error("Unable to create pie chart, height not specified");
1462         }
1463 
1464         if (config.size == null) {
1465             config.size = {}
1466         }
1467         config.size.canvasWidth = config.width || config.size.canvasWidth;
1468         config.size.canvasHeight = config.height || config.size.canvasHeight;
1469 
1470         // apply default font/colors/etc., it not explicitly set
1471         if (!config.header) config.header = {};
1472         if (!config.header.title) config.header.title = {};
1473         if (!config.header.title.font) config.header.title.font = 'Roboto, arial';
1474         if (!config.header.title.hasOwnProperty('fontSize')) config.header.title.fontSize = 18;
1475         if (!config.header.title.color) config.header.title.color = '#000000';
1476         if (!config.header.subtitle) config.header.subtitle = {};
1477         if (!config.header.subtitle.font) config.header.subtitle.font = 'Roboto, arial';
1478         if (!config.header.subtitle.hasOwnProperty('fontSize')) config.header.subtitle.fontSize = 16;
1479         if (!config.header.subtitle.color) config.header.subtitle.color = '#555555';
1480         if (!config.footer) config.footer = {};
1481         if (!config.footer.font) config.footer.font = 'Roboto, arial';
1482         if (!config.labels) config.labels = {};
1483         if (!config.labels.mainLabel) config.labels.mainLabel = {};
1484         if (!config.labels.mainLabel.font) config.labels.mainLabel.font = 'Roboto, arial';
1485         if (!config.labels.percentage) config.labels.percentage = {};
1486         if (!config.labels.percentage.font) config.labels.percentage.font = 'Roboto, arial';
1487         if (!config.labels.percentage.color) config.labels.percentage.color = '#DDDDDD';
1488         if (!config.labels.outer) config.labels.outer = {};
1489         if (!config.labels.outer.hasOwnProperty('pieDistance')) config.labels.outer.pieDistance = 10;
1490         if (!config.labels.inner) config.labels.inner = {};
1491         if (!config.labels.inner.format) config.labels.inner.format = 'percentage';
1492         if (!config.labels.inner.hasOwnProperty('hideWhenLessThanPercentage')) config.labels.inner.hideWhenLessThanPercentage = 10;
1493         if (!config.labels.lines) config.labels.lines = {};
1494         if (!config.labels.lines.style) config.labels.lines.style = 'straight';
1495         if (!config.labels.lines.color) config.labels.lines.color = '#222222';
1496         if (!config.misc) config.misc = {};
1497         if (!config.misc.colors) config.misc.colors = {};
1498         if (!config.misc.colors.segments) config.misc.colors.segments = LABKEY.vis.Scale.ColorDiscrete();
1499         if (!config.misc.colors.segmentStroke) config.misc.colors.segmentStroke = '#222222';
1500         if (!config.misc.gradient) config.misc.gradient = {};
1501         if (!config.misc.gradient.enabled) config.misc.gradient.enabled = false;
1502         if (!config.misc.gradient.hasOwnProperty('percentage')) config.misc.gradient.percentage = 95;
1503         if (!config.misc.gradient.color) config.misc.gradient.color = "#000000";
1504         if (!config.effects) config.effects = {};
1505         if (!config.effects.pullOutSegmentOnClick) config.effects.pullOutSegmentOnClick = {};
1506         if (!config.effects.pullOutSegmentOnClick.effect) config.effects.pullOutSegmentOnClick.effect = 'none';
1507         if (!config.tooltips) config.tooltips = {};
1508         if (!config.tooltips.type) config.tooltips.type = 'placeholder';
1509         if (!config.tooltips.string) config.tooltips.string = '{label}: {percentage}%';
1510         if (!config.tooltips.styles) config.tooltips.styles = {backgroundOpacity: 1};
1511 
1512         return new d3pie(config.renderTo, config);
1513     };
1514 })();
1515 
1516 /**
1517  * @name LABKEY.vis.TrendingLinePlot
1518  * @class TrendingLinePlot Wrapper to create a plot which shows data points compared to expected ranges
1519  *                          For LeveyJennings, the range is +/- 3 standard deviations from a mean.
1520  *                          For MovingRange, the range is [0, 3.268*mean(mR)].
1521  *                          For CUSUM, the range is [0, +5].
1522  * @description This helper will take the input data and generate a sequencial x-axis so that all data points are the same distance apart.
1523  * @param {Object} config An object that contains the following properties
1524  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
1525  * @param {String} [config.qcPlotType] Specifies the plot type to be one of "LeveyJennings", "CUSUM", "MovingRange". Defaults to "LeveyJennings".
1526  * @param {Number} [config.width] The chart canvas width in pixels.
1527  * @param {Number} [config.height] The chart canvas height in pixels.
1528  * @param {Array} [config.data] The array of chart segment data.
1529  *                          For LeveyJennings and MovingRange, each object is of the form: { label: "label", value: 123 }.
1530  *                          For CUSUM, each object is of the form: { label: "label", value: 123, negative: true}.
1531  * @param {Number} [config.data.value]
1532  *                          For LeveyJennings, it's the raw value.
1533  *                          For MovingRange, the calculated rM value, not the raw value.
1534  *                          For CUSUM, the calculated CUSUM value, not the raw value.
1535  * @param {String} [config.data.negative] CUSUM plot only. True for CUSUM-, false for CUSUM+. Default false;
1536  * @param {Object} [config.properties] An object that contains the properties specific to the Levey-Jennings plot
1537  * @param {String} [config.properties.value] The data property name for the value to be plotted on the left y-axis.
1538  *                          Used by LeveyJennings.
1539  * @param {String} [config.properties.valueRight] The data property name for the value to be plotted on the right y-axis.
1540  *                          Used by LeveyJennings.
1541  * @param {String} [config.properties.mean] The data property name for the mean of the expected range.
1542  *                          Used by LeveyJennings.
1543  * @param {String} [config.properties.stdDev] The data property name for the standard deviation of the expected range.
1544  *                          Used by LeveyJennings only.
1545  * @param {String} [config.properties.valueMR] The data property name for the moving range value to be plotted on the left y-axis.
1546  *                          Used by MovingRange.
1547  * @param {String} [config.properties.valueRightMR] The data property name for the moving range to be plotted on the right y-axis.
1548  *                          Used by MovingRange.
1549  * @param {String} [config.properties.meanMR] The data property name for the mean of the moving range.
1550  *                          Used MovingRange.
1551  * @param {String} [config.properties.positiveValue] The data property name for the value to be plotted on the left y-axis for CUSUM+.
1552  *                          Used by CUSUM only.
1553  * @param {String} [config.properties.positiveValueRight] The data property name for the value to be plotted on the right y-axis for CUSUM+.
1554  *                          Used by CUSUM only.
1555  * @param {String} [config.properties.negativeValue] The data property name for the value to be plotted on the left y-axis for CUSUM-.
1556  *                          Used by CUSUM only.
1557  * @param {String} [config.properties.negativeValueRight] The data property name for the value to be plotted on the right y-axis for CUSUM-.
1558  *                          Used by CUSUM only.
1559  * @param {String} [config.properties.xTickLabel] The data property name for the x-axis tick label.
1560  * @param {Number} [config.properties.xTickTagIndex] (Optional) The index/value of the x-axis label to be tagged (i.e. class="xticktag").
1561  * @param {Boolean} [config.properties.showTrendLine] (Optional) Whether or not to show a line connecting the data points. Default false.
1562  * @param {Boolean} [config.properties.disableRangeDisplay] (Optional) Whether or not to show the control ranges in the plot. Defaults to false.
1563  *                          For LeveyJennings, the range is +/- 3 standard deviations from a mean.
1564  *                          For MovingRange, the range is [0, 3.268*mean(mR)].
1565  *                          For CUSUM, the range is [0, +5].
1566  * @param {String} [config.properties.xTick] (Optional) The data property to use for unique x-axis tick marks. Defaults to sequence from 1:data length.
1567  * @param {String} [config.properties.yAxisScale] (Optional) Whether the y-axis should be plotted with linear or log scale. Default linear.
1568  * @param {Array} [config.properties.yAxisDomain] (Optional) Y-axis min/max values. Example: [0,20].
1569  * @param {String} [config.properties.color] (Optional) The data property name for the color to be used for the data point.
1570  * @param {Array} [config.properties.colorRange] (Optional) The array of color values to use for the data points.
1571  * @param {String} [config.groupBy] (optional) The data property name used to group plot lines and points.
1572  * @param {Function} [config.properties.hoverTextFn] (Optional) The hover text to display for each data point. The parameter
1573  *                  to that function will be a row of data with access to all values for that row.
1574  * @param {Function} [config.properties.pointClickFn] (Optional) The function to call on data point click. The parameters to
1575  *                  that function will be the click event and the row of data for the selected point.
1576  */
1577 (function(){
1578     LABKEY.vis.TrendingLinePlotType = {
1579         LeveyJennings : 'Levey-Jennings',
1580         CUSUM : 'CUSUM',
1581         MovingRange: 'MovingRange'
1582     };
1583 
1584     LABKEY.vis.TrendingLinePlot = function(config){
1585         if (!config.qcPlotType)
1586             config.qcPlotType = LABKEY.vis.TrendingLinePlotType.LeveyJennings;
1587         var plotTypeLabel = LABKEY.vis.TrendingLinePlotType[config.qcPlotType];
1588 
1589         if(config.renderTo == null) {
1590             throw new Error("Unable to create " + plotTypeLabel + " plot, renderTo not specified");
1591         }
1592 
1593         if(config.data == null) {
1594             throw new Error("Unable to create " + plotTypeLabel + " plot, data array not specified");
1595         }
1596 
1597         if (config.properties == null || config.properties.xTickLabel == null) {
1598             throw new Error("Unable to create " + plotTypeLabel + " plot, properties object not specified. ");
1599         }
1600 
1601         if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.LeveyJennings) {
1602             if (config.properties.value == null) {
1603                 throw new Error("Unable to create " + plotTypeLabel + " plot, value object not specified. "
1604                         + "Required: value, xTickLabel. Optional: mean, stdDev, color, colorRange, hoverTextFn, "
1605                         + "pointClickFn, showTrendLine, disableRangeDisplay, xTick, yAxisScale, yAxisDomain, xTickTagIndex.");
1606             }
1607         }
1608         else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM) {
1609             if (config.properties.positiveValue == null || config.properties.negativeValue == null) {
1610                 throw new Error("Unable to create " + plotTypeLabel + " plot."
1611                         + "Required: positiveValue, negativeValue, xTickLabel. Optional: positiveValueRight, negativeValueRight, "
1612                         + "xTickTagIndex, showTrendLine, disableRangeDisplay, xTick, yAxisScale, color, colorRange.");
1613             }
1614         }
1615         else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.MovingRange) {
1616             if (config.properties.valueMR == null) {
1617                 throw new Error("Unable to create " + plotTypeLabel + " plot, value object not specified. "
1618                         + "Required: value, xTickLabel. Optional: meanMR, color, colorRange, hoverTextFn, "
1619                         + "pointClickFn, showTrendLine, disableRangeDisplay, xTick, yAxisScale, yAxisDomain, xTickTagIndex.");
1620             }
1621         }
1622         else {
1623             throw new Error(plotTypeLabel + " plot type is not supported!");
1624         }
1625 
1626 
1627         // get a sorted array of the unique x-axis labels
1628         var uniqueXAxisKeys = {}, uniqueXAxisLabels = [];
1629         for (var i = 0; i < config.data.length; i++) {
1630             if (!uniqueXAxisKeys[config.data[i][config.properties.xTick]]) {
1631                 uniqueXAxisKeys[config.data[i][config.properties.xTick]] = true;
1632             }
1633         }
1634         uniqueXAxisLabels =  Object.keys(uniqueXAxisKeys).sort();
1635 
1636         // create a sequencial index to use for the x-axis value and keep a map from that index to the tick label
1637         // also, pull out the meanStdDev data for the unique x-axis values and calculate average values for the (LJ) trend line data
1638         var tickLabelMap = {}, index = -1, distinctColorValues = [], meanStdDevData = [],
1639             groupedTrendlineData = [], groupedTrendlineSeriesData = {},
1640             hasYRightMetric = config.properties.valueRight || config.properties.positiveValueRight || config.properties.valueRightMR;
1641 
1642         for (var i = 0; i < config.data.length; i++)
1643         {
1644             var row = config.data[i];
1645 
1646             // track the distinct values in the color variable so that we know if we need the legend or not
1647             if (config.properties.color && distinctColorValues.indexOf(row[config.properties.color]) == -1) {
1648                 distinctColorValues.push(row[config.properties.color]);
1649             }
1650 
1651             // if we are grouping x values based on the xTick property, only increment index if we have a new xTick value
1652             if (config.properties.xTick)
1653             {
1654                 var addValueToTrendLineData = function(dataArr, seqValue, arrKey, fieldName, rowValue, sumField, countField)
1655                 {
1656                     if (dataArr[arrKey] == undefined)
1657                     {
1658                         dataArr[arrKey] = {
1659                             seqValue: seqValue
1660                         };
1661                     }
1662 
1663                     if (dataArr[arrKey][sumField] == undefined)
1664                     {
1665                         dataArr[arrKey][sumField] = 0;
1666                     }
1667                     if (dataArr[arrKey][countField] == undefined)
1668                     {
1669                         dataArr[arrKey][countField] = 0;
1670                     }
1671 
1672                     if (rowValue != undefined)
1673                     {
1674                         dataArr[arrKey][sumField] += rowValue;
1675                         dataArr[arrKey][countField]++;
1676                         dataArr[arrKey][fieldName] = dataArr[arrKey][sumField] / dataArr[arrKey][countField];
1677                     }
1678                 };
1679 
1680                 var addAllValuesToTrendLineData = function(dataArr, seqValue, arrKey, row, hasYRightMetric)
1681                 {
1682                     var plotValueName = config.properties.value, plotValueNameRight = config.properties.valueRight;
1683                     var plotValueNamePositive = config.properties.positiveValue, plotValueNameRightPositive = config.properties.positiveValueRight;
1684                     if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.MovingRange)
1685                     {
1686                         plotValueName = config.properties.valueMR;
1687                         plotValueNameRight = config.properties.valueRightMR;
1688                     }
1689                     else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM)
1690                     {
1691                         plotValueName = config.properties.negativeValue;
1692                         plotValueNameRight = config.properties.negativeValueRight;
1693                     }
1694 
1695                     addValueToTrendLineData(dataArr, seqValue, arrKey, plotValueName, row[plotValueName], 'sum1', 'count1');
1696                     if (hasYRightMetric)
1697                     {
1698                         addValueToTrendLineData(dataArr, seqValue, arrKey, plotValueNameRight, row[plotValueNameRight], 'sum2', 'count2');
1699                     }
1700 
1701                     if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM)
1702                     {
1703                         addValueToTrendLineData(dataArr, seqValue, arrKey, plotValueNamePositive, row[plotValueNamePositive], 'sum3', 'count3');
1704                         if (hasYRightMetric)
1705                         {
1706                             addValueToTrendLineData(dataArr, seqValue, arrKey, plotValueNameRightPositive, row[plotValueNameRightPositive], 'sum4', 'count4');
1707                         }
1708                     }
1709                 };
1710 
1711                 index = uniqueXAxisLabels.indexOf(row[config.properties.xTick]);
1712 
1713                 // calculate average values for the trend line data (used when grouping x by unique value)
1714                 addAllValuesToTrendLineData(groupedTrendlineData, index, index, row, hasYRightMetric);
1715 
1716                 // calculate average values for trend line data for each series (used when grouping x by unique value with a groupBy series property)
1717                 if (config.properties.groupBy && row[config.properties.groupBy]) {
1718                     var series = row[config.properties.groupBy];
1719                     var key = series + '|' + index;
1720 
1721                     addAllValuesToTrendLineData(groupedTrendlineSeriesData, index, key, row, hasYRightMetric);
1722 
1723                     groupedTrendlineSeriesData[key][config.properties.groupBy] = series;
1724                 }
1725             }
1726             else {
1727                 index++;
1728             }
1729 
1730             tickLabelMap[index] = row[config.properties.xTickLabel];
1731             row.seqValue = index;
1732 
1733             if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.LeveyJennings)
1734             {
1735                 if (config.properties.mean && config.properties.stdDev && !meanStdDevData[index])
1736                 {
1737                     meanStdDevData[index] = row;
1738                 }
1739             }
1740         }
1741 
1742         // min x-axis tick length is 10 by default
1743         var maxSeqValue = config.data.length > 0 ? config.data[config.data.length - 1].seqValue + 1 : 0;
1744         for (var i = maxSeqValue; i < 10; i++)
1745         {
1746             var temp = {type: 'empty', seqValue: i};
1747             temp[config.properties.xTickLabel] = "";
1748             if (config.properties.color && config.data[0]) {
1749                 temp[config.properties.color] = config.data[0][config.properties.color];
1750             }
1751             config.data.push(temp);
1752         }
1753 
1754         // we only need the color aes if there is > 1 distinct value in the color variable
1755         if (distinctColorValues.length < 2 && config.properties.groupBy == undefined) {
1756             config.properties.color = undefined;
1757         }
1758 
1759         config.tickOverlapRotation = 35;
1760 
1761         config.scales = {
1762             color: {
1763                 scaleType: 'discrete',
1764                 range: config.properties.colorRange
1765             },
1766             x: {
1767                 scaleType: 'discrete',
1768                 tickFormat: function(index) {
1769                     // only show a max of 35 labels on the x-axis to avoid overlap
1770                     if (index % Math.ceil(config.data[config.data.length-1].seqValue / 35) == 0) {
1771                         return tickLabelMap[index];
1772                     }
1773                     else {
1774                         return "";
1775                     }
1776                 },
1777                 tickCls: function(index) {
1778                     var baseTag = 'ticklabel';
1779                     var tagIndex = config.properties.xTickTagIndex;
1780                     if (tagIndex != undefined && tagIndex == index) {
1781                         return baseTag+' xticktag';
1782                     }
1783                     return baseTag;
1784                 }
1785             },
1786             yLeft: {
1787                 scaleType: 'continuous',
1788                 domain: config.properties.yAxisDomain,
1789                 trans: config.properties.yAxisScale || 'linear',
1790                 tickFormat: function(val) {
1791                     return LABKEY.vis.isValid(val) && (val > 100000 || val < -100000) ? val.toExponential() : val;
1792                 }
1793             }
1794         };
1795 
1796         if (hasYRightMetric)
1797         {
1798             config.scales.yRight = {
1799                 scaleType: 'continuous',
1800                 domain: config.properties.yAxisDomain,
1801                 trans: config.properties.yAxisScale || 'linear',
1802                 tickFormat: function(val) {
1803                     return LABKEY.vis.isValid(val) && (val > 100000 || val < -100000) ? val.toExponential() : val;
1804                 }
1805             };
1806         }
1807 
1808         // Issue 23626: map line/point color based on legend data
1809         if (config.legendData && config.properties.color && !config.properties.colorRange)
1810         {
1811             var legendColorMap = {};
1812             for (var i = 0; i < config.legendData.length; i++)
1813             {
1814                 if (config.legendData[i].name)
1815                 {
1816                     legendColorMap[config.legendData[i].name] = config.legendData[i].color;
1817                 }
1818             }
1819 
1820             if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM)
1821             {
1822                 config.scales.color = {
1823                     scale: function(group) {
1824                         var normalizedGroup = group.replace('CUSUMmN', 'CUSUMm').replace('CUSUMmP', 'CUSUMm');
1825                         normalizedGroup = normalizedGroup.replace('CUSUMvN', 'CUSUMv').replace('CUSUMvP', 'CUSUMv');
1826                         return legendColorMap[normalizedGroup];
1827                     }
1828                 };
1829             }
1830             else
1831             {
1832                 config.scales.color = {
1833                     scale: function(group) {
1834                         return legendColorMap[group];
1835                     }
1836                 };
1837             }
1838         }
1839 
1840         if(!config.margins) {
1841             config.margins = {};
1842         }
1843 
1844         if(!config.margins.top) {
1845             config.margins.top = config.labels && config.labels.main ? 30 : 10;
1846         }
1847 
1848         if(!config.margins.right) {
1849             config.margins.right = (config.properties.color || (config.legendData && config.legendData.length > 0) ? 190 : 40)
1850                                     + (hasYRightMetric ? 45 : 0);
1851         }
1852 
1853         if(!config.margins.bottom) {
1854             config.margins.bottom = config.labels && config.labels.x ? 75 : 55;
1855         }
1856 
1857         if(!config.margins.left) {
1858             config.margins.left = config.labels && config.labels.y ? 75 : 55;
1859         }
1860 
1861         config.aes = {
1862             x: 'seqValue'
1863         };
1864 
1865         // determine the width the error bars
1866         if (config.properties.disableRangeDisplay)
1867             config.layers = [];
1868         else {
1869             var barWidth = Math.max(config.width / config.data[config.data.length-1].seqValue / 5, 3);
1870 
1871             if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.LeveyJennings) {
1872 
1873                 // +/- 3 standard deviation displayed using the ErrorBar geom with different colors
1874                 var stdDev3Layer = new LABKEY.vis.Layer({
1875                     geom: new LABKEY.vis.Geom.ErrorBar({size: 1, color: 'red', dashed: true, altColor: 'darkgrey', width: barWidth}),
1876                     data: meanStdDevData,
1877                     aes: {
1878                         error: function(row){return row[config.properties.stdDev] * 3;},
1879                         yLeft: config.properties.mean
1880                     }
1881                 });
1882                 var stdDev2Layer = new LABKEY.vis.Layer({
1883                     geom: new LABKEY.vis.Geom.ErrorBar({size: 1, color: 'blue', dashed: true, altColor: 'darkgrey', width: barWidth}),
1884                     data: meanStdDevData,
1885                     aes: {
1886                         error: function(row){return row[config.properties.stdDev] * 2;},
1887                         yLeft: config.properties.mean
1888                     }
1889                 });
1890                 var stdDev1Layer = new LABKEY.vis.Layer({
1891                     geom: new LABKEY.vis.Geom.ErrorBar({size: 1, color: 'green', dashed: true, altColor: 'darkgrey', width: barWidth}),
1892                     data: meanStdDevData,
1893                     aes: {
1894                         error: function(row){return row[config.properties.stdDev];},
1895                         yLeft: config.properties.mean
1896                     }
1897                 });
1898                 var meanLayer = new LABKEY.vis.Layer({
1899                     geom: new LABKEY.vis.Geom.ErrorBar({size: 1, color: 'darkgrey', width: barWidth}),
1900                     data: meanStdDevData,
1901                     aes: {
1902                         error: function(row){return 0;},
1903                         yLeft: config.properties.mean
1904                     }
1905                 });
1906                 config.layers = [stdDev3Layer, stdDev2Layer, stdDev1Layer, meanLayer];
1907             }
1908             else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM) {
1909                 var range = new LABKEY.vis.Layer({
1910                     geom: new LABKEY.vis.Geom.ControlRange({size: 1, color: 'red', dashed: true, altColor: 'darkgrey', width: barWidth}),
1911                     data: config.data,
1912                     aes: {
1913                         upper: function(){return LABKEY.vis.Stat.CUSUM_CONTROL_LIMIT;},
1914                         lower: function(){return LABKEY.vis.Stat.CUSUM_CONTROL_LIMIT_LOWER;},
1915                         yLeft: config.properties.mean
1916                     }
1917                 });
1918                 config.layers = [range];
1919             }
1920             else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.MovingRange) {
1921                 var range = new LABKEY.vis.Layer({
1922                     geom: new LABKEY.vis.Geom.ControlRange({size: 1, color: 'red', dashed: true, altColor: 'darkgrey', width: barWidth}),
1923                     data: config.data,
1924                     aes: {
1925                         upper: function(row){return row[config.properties.meanMR] * LABKEY.vis.Stat.MOVING_RANGE_UPPER_LIMIT_WEIGHT;},
1926                         lower: function(){return LABKEY.vis.Stat.MOVING_RANGE_LOWER_LIMIT;},
1927                         yLeft: config.properties.mean
1928                     }
1929                 });
1930                 config.layers = [range];
1931             }
1932         }
1933 
1934         if (config.properties.showTrendLine)
1935         {
1936             var getPathLayerConfig = function(ySide, valueName, colorValue, negativeCusum)
1937             {
1938                 var pathLayerConfig = {
1939                     geom: new LABKEY.vis.Geom.Path({
1940                         opacity: .6,
1941                         size: 2,
1942                         dashed: config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM && !negativeCusum
1943                     }),
1944                     aes: {}
1945                 };
1946 
1947                 pathLayerConfig.aes[ySide] = valueName;
1948 
1949                 // if we aren't showing multiple series data via the group by, use the groupedTrendlineData for the path
1950                 if (config.properties.groupBy)
1951                 {
1952                     // convert the groupedTrendlineSeriesData object into an array of the object values
1953                     var seriesDataArr = [];
1954                     for(var i in groupedTrendlineSeriesData) {
1955                         if (groupedTrendlineSeriesData.hasOwnProperty(i)) {
1956                             var d = { seqValue: groupedTrendlineSeriesData[i].seqValue };
1957                             d[config.properties.groupBy] = groupedTrendlineSeriesData[i][config.properties.groupBy] + (hasYRightMetric ? '|' + valueName : '');
1958                             d[valueName] = groupedTrendlineSeriesData[i][valueName];
1959                             seriesDataArr.push(d);
1960                         }
1961                     }
1962                     pathLayerConfig.data = seriesDataArr;
1963 
1964                     pathLayerConfig.aes.pathColor = config.properties.groupBy;
1965                     pathLayerConfig.aes.group = config.properties.groupBy;
1966                 }
1967                 else
1968                 {
1969                     pathLayerConfig.data = groupedTrendlineData;
1970 
1971                     if (colorValue != undefined)
1972                     {
1973                         pathLayerConfig.aes.pathColor = function(data) {
1974                             return colorValue;
1975                         }
1976                     }
1977                 }
1978 
1979                 return pathLayerConfig;
1980             };
1981 
1982             if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM)
1983             {
1984                 if (hasYRightMetric)
1985                 {
1986                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.negativeValue, 1, true)));
1987                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yRight', config.properties.negativeValueRight, 0, true)));
1988                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.positiveValue, 1, false)));
1989                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yRight', config.properties.positiveValueRight, 0, false)));
1990                 }
1991                 else
1992                 {
1993                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.negativeValue, undefined, true)));
1994                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.positiveValue, undefined, false)));
1995                 }
1996             }
1997             else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.MovingRange)
1998             {
1999                 if (hasYRightMetric)
2000                 {
2001                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.valueMR, 0)));
2002                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yRight', config.properties.valueRightMR, 1)));
2003                 }
2004                 else
2005                 {
2006                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.valueMR)));
2007                 }
2008             }
2009             else
2010             {
2011                 if (hasYRightMetric)
2012                 {
2013                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.value, 0)));
2014                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yRight', config.properties.valueRight, 1)));
2015                 }
2016                 else
2017                 {
2018                     config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.value)));
2019                 }
2020             }
2021         }
2022 
2023         // points based on the data value, color and hover text can be added via params to config
2024         var getPointLayerConfig = function(ySide, valueName, colorValue)
2025         {
2026             var pointLayerConfig = {
2027                 geom: new LABKEY.vis.Geom.Point({
2028                     position: config.properties.position,
2029                     size: 3
2030                 }),
2031                 aes: {}
2032             };
2033 
2034             pointLayerConfig.aes[ySide] = valueName;
2035 
2036             if (config.properties.color) {
2037                 pointLayerConfig.aes.color = function(row) {
2038                     return row[config.properties.color] + (hasYRightMetric ? '|' + valueName : '');
2039                 };
2040             }
2041             else if (colorValue != undefined) {
2042                 pointLayerConfig.aes.color = function(row){ return colorValue; };
2043             }
2044 
2045             if (config.properties.shape) {
2046                 pointLayerConfig.aes.shape = config.properties.shape;
2047             }
2048             if (config.properties.hoverTextFn) {
2049                 pointLayerConfig.aes.hoverText = function(row) {
2050                     return config.properties.hoverTextFn.call(this, row, valueName);
2051                 };
2052             }
2053             if (config.properties.pointClickFn) {
2054                 pointLayerConfig.aes.pointClickFn = config.properties.pointClickFn;
2055             }
2056 
2057             // add some mouse over effects to highlight selected point
2058             pointLayerConfig.aes.mouseOverFn = function(event, pointData, layerSel) {
2059                 d3.select(event.srcElement).transition().duration(800).attr("stroke-width", 5).ease("elastic");
2060             };
2061             pointLayerConfig.aes.mouseOutFn = function(event, pointData, layerSel) {
2062                 d3.select(event.srcElement).transition().duration(800).attr("stroke-width", 1).ease("elastic");
2063             };
2064 
2065             return pointLayerConfig;
2066         };
2067 
2068         if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.CUSUM)
2069         {
2070             if (hasYRightMetric)
2071             {
2072                 config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.negativeValue, 1)));
2073                 config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yRight', config.properties.negativeValueRight, 0)));
2074                 config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.positiveValue, 1)));
2075                 config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yRight', config.properties.positiveValueRight, 0)));
2076 
2077             }
2078             else
2079             {
2080                 config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.negativeValue)));
2081                 config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.positiveValue)));
2082             }
2083         }
2084         else if (config.qcPlotType == LABKEY.vis.TrendingLinePlotType.MovingRange)
2085         {
2086             if (hasYRightMetric)
2087             {
2088                 config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.valueMR, 0)));
2089                 config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yRight', config.properties.valueRightMR, 1)));
2090             }
2091             else
2092             {
2093                 config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.valueMR)));
2094             }
2095         }
2096         else
2097         {
2098             if (hasYRightMetric)
2099             {
2100                 config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.value, 0)));
2101                 config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yRight', config.properties.valueRight, 1)));
2102             }
2103             else
2104             {
2105                 config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.value)));
2106             }
2107         }
2108 
2109         return new LABKEY.vis.Plot(config);
2110     };
2111 
2112 
2113     /**
2114      * @ Deprecated
2115      */
2116     LABKEY.vis.LeveyJenningsPlot = LABKEY.vis.TrendingLinePlot;
2117 })();
2118 
2119 /**
2120  * @name LABKEY.vis.SurvivalCurvePlot
2121  * @class SurvivalCurvePlot Wrapper to create a plot which shows survival curve step lines and censor points (based on output from R survival package).
2122  * @description This helper will take the input data and generate stepwise data points for use with the Path geom.
2123  * @param {Object} config An object that contains the following properties
2124  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
2125  * @param {Number} [config.width] The chart canvas width in pixels.
2126  * @param {Number} [config.height] The chart canvas height in pixels.
2127  * @param {Array} [config.data] The array of step data for the survival curves.
2128  * @param {String} [config.groupBy] (optional) The data array object property used to group plot lines and points.
2129  * @param {Array} [config.censorData] The array of censor data to overlay on the survival step lines.
2130  * @param {Function} [config.censorHoverText] (optional) Function defining the hover text to display for the censor data points.
2131  */
2132 (function(){
2133 
2134     LABKEY.vis.SurvivalCurvePlot = function(config){
2135 
2136         if (config.renderTo == null){
2137             throw new Error("Unable to create survival curve plot, renderTo not specified.");
2138         }
2139 
2140         if (config.data == null || config.censorData == null){
2141             throw new Error("Unable to create survival curve plot, data and/or censorData array not specified.");
2142         }
2143 
2144         if (config.aes == null || config.aes.x == null || config.aes.yLeft == null) {
2145             throw new Error("Unable to create survival curve plot, aes (x and yLeft) not specified.")
2146         }
2147 
2148         // Convert data array for step-wise line plot
2149         var stepData = [];
2150         var groupBy = config.groupBy;
2151         var aesX = config.aes.x;
2152         var aesY = config.aes.yLeft;
2153 
2154         for (var i=0; i<config.data.length; i++)
2155         {
2156             stepData.push(config.data[i]);
2157 
2158             if ( (i<config.data.length-1) && (config.data[i][groupBy] == config.data[i+1][groupBy])
2159                     && (config.data[i][aesX] != config.data[i+1][aesX])
2160                     && (config.data[i][aesY] != config.data[i+1][aesY]))
2161             {
2162                 var point = {};
2163                 point[groupBy] = config.data[i][groupBy];
2164                 point[aesX] = config.data[i+1][aesX];
2165                 point[aesY] = config.data[i][aesY];
2166                 stepData.push(point);
2167             }
2168         }
2169         config.data = stepData;
2170 
2171         config.layers = [
2172             new LABKEY.vis.Layer({
2173                 geom: new LABKEY.vis.Geom.Path({size:2, opacity:1}),
2174                 aes: {
2175                     pathColor: config.groupBy,
2176                     group: config.groupBy
2177                 }
2178             }),
2179             new LABKEY.vis.Layer({
2180                 geom: new LABKEY.vis.Geom.Point({opacity:1}),
2181                 data: config.censorData,
2182                 aes: {
2183                     color: config.groupBy,
2184                     hoverText: config.censorHoverText,
2185                     shape: config.groupBy
2186                 }
2187 
2188             })
2189         ];
2190 
2191         if (!config.scales) config.scales = {};
2192         config.scales.x = { scaleType: 'continuous', trans: 'linear' };
2193         config.scales.yLeft = { scaleType: 'continuous', trans: 'linear', domain: [0, 1] };
2194 
2195         config.aes.mouseOverFn = function(event, pointData, layerSel) {
2196             mouseOverFn(event, pointData, layerSel, config.groupBy);
2197         };
2198 
2199         config.aes.mouseOutFn = mouseOutFn;
2200 
2201         return new LABKEY.vis.Plot(config);
2202     };
2203 
2204     var mouseOverFn = function(event, pointData, layerSel, subjectColumn) {
2205         var points = layerSel.selectAll('.point path');
2206         var lines = d3.selectAll('path.line');
2207 
2208         var opacityAcc = function(d) {
2209             if (d[subjectColumn] && d[subjectColumn] == pointData[subjectColumn])
2210             {
2211                 return 1;
2212             }
2213             return .3;
2214         };
2215 
2216         points.attr('fill-opacity', opacityAcc).attr('stroke-opacity', opacityAcc);
2217         lines.attr('fill-opacity', opacityAcc).attr('stroke-opacity', opacityAcc);
2218     };
2219 
2220     var mouseOutFn = function(event, pointData, layerSel) {
2221         layerSel.selectAll('.point path').attr('fill-opacity', 1).attr('stroke-opacity', 1);
2222         d3.selectAll('path.line').attr('fill-opacity', 1).attr('stroke-opacity', 1);
2223     };
2224 })();
2225 
2226 /**
2227  * @name LABKEY.vis.TimelinePlot
2228  * @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.
2229  * @param {Object} config An object that contains the following properties
2230  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
2231  * @param {Number} [config.width] The chart canvas width in pixels.
2232  * @param {Number} [config.height] The chart canvas height in pixels.
2233  * @param {Array} [config.data] The array of event data including event types and subtypes for the plot.
2234  * @param {String} [config.gridLinesVisible] Possible options are 'y', 'x', and 'both' to determine which sets of
2235  *                  grid lines are rendered on the plot. Default is 'both'.
2236  * @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}
2237  * @param {Date} [config.options.startDate] (Optional) The start date to use to calculate number of days until event date.
2238  * @param {Object} [config.options] (Optional) Display options as defined in {@link LABKEY.vis.Geom.TimelinePlot}.
2239  * @param {Boolean} [config.options.isCollapsed] (Optional) If true, the timeline collapses subtypes into their parent rows. Defaults to True.
2240  * @param {Number} [config.options.rowHeight] (Optional) The height of individual rows in pixels. For expanded timelines,
2241  *                  row height will resize to 75% of this value. Defaults to 1.
2242  * @param {Object} [config.options.highlight] (Optional) Special Data object containing information to highlight a specific
2243  *                  row in the timeline. Must have the same shape & properties as all other input data.
2244  * @param {String} [config.options.highlightRowColor] (Optional) Hex color to specifiy what color the highlighted row will
2245  *                  be if, found in the data. Defaults to #74B0C4.
2246  * @param {String} [config.options.activeEventKey] (Optional) Name of property that is paired with
2247  *                  @param config.options.activeEventIdentifier to identify a unique event in the data.
2248  * @param {String} [config.options.activeEventIdentifier] (Optional) Name of value that is paired with
2249  *                  @param config.options.activeEventKey to identify a unique event in the data.
2250  * @param {String} [config.options.activeEventStrokeColor] (Optional) Hex color to specifiy what color the active event
2251  *                  rect's stroke will be, if found in the data. Defaults to Red.
2252  * @param {Object} [config.options.emphasisEvents] (Optional) Object containing key:[value] pairs whose keys are property
2253  *                  names of a data object and whose value is an array of possible values that should have a highlight
2254 *                   line drawn on the chart when found. Example: {'type': ['death', 'Withdrawal']}
2255  * @param {String} [config.options.tickColor] (Optional) Hex color to specifiy the color of Axis ticks.D efaults to #DDDDDD.
2256  * @param {String} [config.options.emphasisTickColor] (Optional) Hex color to specify the color of emphasis event ticks,
2257  *                  if found in the data. Defaults to #1a969d.
2258  * @param {String} [config.options.timeUnit] (Optional) Unit of time to use when calculating how far an event's date
2259  *                  is from the start date. Default is years. Valid string values include minutes, hours, days, years, and decades.
2260  * @param {Number} [config.options.eventIconSize] (Optional) Size of event square width/height dimensions.
2261  * @param {String} [config.options.eventIconColor] (Optional) Hex color of event square stroke. Defaults to black (#0000000).
2262  * @param {String} [config.options.eventIconFill] (Optional) Hex color of event square inner fill. Defaults to black (#000000)..
2263  * @param {Float} [config.options.eventIconOpacity] (Optional) Float between 0 - 1 (inclusive) to specify how transparent the
2264  *                  fill of event icons will be. Defaults to 1.
2265  * @param {Array} [config.options.rowColorDomain] (Optional) Array of length 2 containing string Hex values for the two
2266  *                  alternating colors of timeline row rectangles. Defaults to ['#f2f2f2', '#ffffff'].
2267  */
2268 (function(){
2269 
2270     LABKEY.vis.TimelinePlot = function(config)
2271     {
2272         if (config.renderTo == undefined || config.renderTo == null) { throw new Error("Unable to create timeline plot, renderTo not specified."); }
2273 
2274         if (config.data == undefined || config.data == null) { throw new Error("Unable to create timeline plot, data array not specified."); }
2275 
2276         if (config.width == undefined || config.width == null) { throw new Error("Unable to create timeline plot, width not specified."); }
2277 
2278         if (!config.aes.y) {
2279             config.aes.y = 'key';
2280         }
2281 
2282         if (!config.options) {
2283             config.options = {};
2284         }
2285 
2286         //default x scale is in years
2287         if (!config.options.timeUnit) {
2288             config.options.timeUnit = 'years';
2289         }
2290 
2291         //set default left margin to make room for event label text
2292         if (!config.margins.left) {
2293             config.margins.left = 200
2294         }
2295 
2296         //default row height value
2297         if (!config.options.rowHeight) {
2298             config.options.rowHeight = 40;
2299         }
2300 
2301         //override default plot values if not set
2302         if (!config.margins.top) {
2303             config.margins.top = 40;
2304         }
2305         if (!config.margins.bottom) {
2306             config.margins.bottom = 50;
2307         }
2308         if (!config.gridLineWidth) {
2309             config.gridLineWidth = 1;
2310         }
2311         if (!config.gridColor) {
2312             config.gridColor = '#FFFFFF';
2313         }
2314         if (!config.borderColor) {
2315             config.borderColor = '#DDDDDD';
2316         }
2317         if (!config.tickColor) {
2318             config.tickColor = '#DDDDDD';
2319         }
2320 
2321         config.rendererType = 'd3';
2322         config.options.marginLeft = config.margins.left;
2323         config.options.parentName = config.aes.parentName;
2324         config.options.childName = config.aes.childName;
2325         config.options.dateKey = config.aes.x;
2326 
2327         config.scales = {
2328             x: {
2329                 scaleType: 'continuous'
2330             },
2331             yLeft: {
2332                 scaleType: 'discrete'
2333             }
2334         };
2335 
2336         var millis;
2337         switch(config.options.timeUnit.toLowerCase())
2338         {
2339             case 'minutes':
2340                 millis = 1000 * 60;
2341                 break;
2342             case 'hours':
2343                 millis = 1000 * 60 * 60;
2344                 break;
2345             case 'days':
2346                 millis = 1000 * 60 * 60 * 24;
2347                 break;
2348             case 'months':
2349                 millis = 1000 * 60 * 60 * 24 * 30.42;
2350                 break;
2351             case 'years':
2352                 millis = 1000 * 60 * 60 * 24 * 365;
2353                 break;
2354             case 'decades':
2355                 millis = 1000 * 60 * 60 * 24 * 365 * 10;
2356                 break;
2357             default:
2358                 millis = 1000;
2359         }
2360 
2361         //find the earliest occurring date in the data if startDate is not already specified
2362         var min = config.options.startDate ? config.options.startDate : null;
2363         if (min == null)
2364         {
2365             for (var i = 0; i < config.data.length; i++)
2366             {
2367                 config.data[i][config.aes.x] = new Date(config.data[i][config.aes.x]);
2368                 if (min == null)
2369                 {
2370                     min = config.data[i][config.aes.x];
2371                 }
2372                 min = config.data[i][config.aes.x] < min ? config.data[i][config.aes.x] : min;
2373             }
2374         }
2375 
2376         //Loop through the data and do calculations for each entry
2377         var max = 0;
2378         var parents = new Set();
2379         var children = new Set();
2380         var types = new Set();
2381         var domain = [];
2382         for (var j = 0; j < config.data.length; j++)
2383         {
2384             //calculate difference in time units
2385             var d = config.data[j];
2386             d[config.aes.x] = config.options.startDate ? new Date(d[config.aes.x]) : d[config.aes.x];
2387             var timeDifference = (d[config.aes.x] - min) / millis;
2388             d[config.options.timeUnit] = timeDifference;
2389 
2390             //update unique counts
2391             parents.add(d[config.aes.parentName]);
2392             children.add(d[config.aes.childName]);
2393 
2394             //update domain
2395             if (!config.options.isCollapsed) {
2396                 var str;
2397                 if (d[config.aes.parentName] != null && d[config.aes.parentName] != 'null' && d[config.aes.parentName] != undefined) {
2398                     str = d[config.aes.parentName];
2399                     if (!types.has(str) && str != undefined) {
2400                         domain.push(str);
2401                         types.add(str);
2402                     }
2403                     d.typeSubtype = str;
2404                 }
2405                 if (d[config.aes.childName] != null && d[config.aes.childName] != 'null' && d[config.aes.childName] != undefined) {
2406                     str += '-' + d[config.aes.childName];
2407                 }
2408                 if (!types.has(str) && str != undefined) {
2409                     domain.push(str);
2410                     types.add(str);
2411                 }
2412 
2413                 //typeSubtype will be a simple unique identifier for this type & subtype of event
2414                 d.typeSubtype = str;
2415             } else {
2416                 if (!types.has(d[config.aes.parentName])) {
2417                     domain.push(d[config.aes.parentName]);
2418                     types.add(d[config.aes.parentName]);
2419                 }
2420             }
2421 
2422             //update max value
2423             max = timeDifference > max ? timeDifference : max;
2424         }
2425         var numParentChildUniques = parents.size + children.size;
2426         if (children.has(null)) {
2427             numParentChildUniques--;
2428         }
2429         var numParentUniques = parents.size;
2430         domain.sort().reverse();
2431 
2432         //For a better looking title
2433         function capitalizeFirstLetter(string) {
2434             return string.charAt(0).toUpperCase() + string.slice(1);
2435         }
2436 
2437         //Update x label to include the start date for better context
2438         config.labels.x = {value: capitalizeFirstLetter(config.options.timeUnit) + " Since " + min.toDateString()};
2439 
2440         if (!config.options.isCollapsed) {
2441             config.aes.typeSubtype = "typeSubtype";
2442 
2443             config.scales.yLeft.domain = domain;
2444             var chartHeightMultiplier = numParentChildUniques !== numParentUniques ? Math.floor(config.options.rowHeight * .75) : config.options.rowHeight;
2445             config.height = (chartHeightMultiplier * numParentChildUniques) + config.margins.top + config.margins.bottom;
2446             config.options.rowHeight = (config.height - (config.margins.top + config.margins.bottom)) / numParentChildUniques;
2447             if (numParentChildUniques < 10) {
2448                 //small visual adjustment for short charts without many data points
2449                 config.options.rowHeight = config.options.rowHeight - (12 - numParentChildUniques);
2450             }
2451         } else {
2452             config.scales.yLeft.domain = domain;
2453             config.height = (config.options.rowHeight * numParentUniques) + config.margins.top + config.margins.bottom;
2454 
2455             config.options.rowHeight = (config.height - (config.margins.top + config.margins.bottom)) / numParentUniques;
2456             if (numParentUniques < 10) {
2457                 config.options.rowHeight = config.options.rowHeight - (12 - numParentUniques);
2458             }
2459         }
2460 
2461         config.scales.x.domain = [0, Math.ceil(max)];
2462         config.aes.x = config.options.timeUnit;
2463         config.layers = [
2464             new LABKEY.vis.Layer({
2465                 geom: new LABKEY.vis.Geom.TimelinePlot(config.options)
2466             })
2467         ];
2468 
2469         return new LABKEY.vis.Plot(config);
2470     };
2471 })();
2472 
2473