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>tickDigits:</strong> Convert axis tick to exponential form if equal or greater than number of digits</li>
 62  *          <li><strong>tickLabelMax:</strong> Maximum number of tick labels to show for a categorical axis.</li>
 63  *          <li><strong>tickHoverText:</strong>: Adds hover text for axis labels.</li>
 64  *          <li><strong>tickCls:</strong> Add class to axis label.</li>
 65  *          <li><strong>tickRectCls:</strong> Add class to mouse area rectangle around axis label.</li>
 66  *          <li><strong>tickRectHeightOffset:</strong> Set axis mouse area rect width. Offset beyond label text width.</li>
 67  *          <li><strong>tickRectWidthOffset:</strong> Set axis mouse area rect height. Offset beyond label text height.</li>
 68  *          <li><strong>tickClick:</strong> Handler for axis label click. Binds to mouse area rect around label.</li>
 69  *          <li><strong>tickMouseOver:</strong> Handler for axis label mouse over. Binds to mouse area rect around label.</li>
 70  *          <li><strong>tickMouseOut:</strong> Handler for axis label mouse out. Binds to mouse area rect around label.</li>
 71  *      </ul>
 72  * @param {Object} [config.labels] (Optional) An object with the following properties: main, x, y (or yLeft), yRight.
 73  *      Each property can have a {String} value, {Boolean} lookClickable, {Object} listeners, and other properties listed below.
 74  *      The value is the text that will appear on the label, lookClickable toggles if the label will appear clickable, and the
 75  *      listeners property allows the user to specify listeners on the labels such as click, hover, etc, as well as the functions to
 76  *      execute when the events occur. Each label will be an object that has the following properties:
 77  *      <ul>
 78  *          <li>
 79  *              <strong>value:</strong> The string value of the label (i.e. "Weight Over Time").
 80  *          </li>
 81  *          <li>
 82  *              <strong>fontSize:</strong> The font-size in pixels.
 83  *          </li>
 84  *          <li>
 85  *              <strong>position:</strong> The number of pixels from the edge to render the label.
 86  *          </li>
 87  *          <li>
 88  *              <strong>lookClickable:</strong> If true it styles the label so that it appears to be clickable. Defaults
 89  *              to false.
 90  *          </li>
 91  *          <li>
 92  *              <strong>visibility:</strong> The initial visibility state for the label. Defaults to normal.
 93  *          </li>
 94  *          <li>
 95  *              <strong>cls:</strong> Class added to label element.
 96  *          </li>
 97  *          <li>
 98  *              <strong>listeners:</strong> An object with properties for each listener the user wants attached
 99  *              to the label. The value of each property is the function to be called when the event occurs. The
100  *              available listeners are: click, dblclick, hover, mousedown, mouseup, mousemove, mouseout, mouseover,
101  *              touchcancel, touchend, touchmove, and touchstart.
102  *          </li>
103  *      </ul>
104  * @param {Object} [config.margins] (Optional) Margin sizes in pixels. It can be useful to set the margins if the tick
105  *      marks on an axis are overlapping with your axis labels. Defaults to top: 75px, right: 75px, bottom: 50px, and
106  *      left: 75px. The right side may have a margin of 150px if a legend is needed. Custom define margin size for a
107  *      legend that exceeds 150px.
108  *      The object may contain any of the following properties:
109  *      <ul>
110  *          <li><strong>top:</strong> Size of top margin in pixels.</li>
111  *          <li><strong>bottom:</strong> Size of bottom margin in pixels.</li>
112  *          <li><strong>left:</strong> Size of left margin in pixels.</li>
113  *          <li><strong>right:</strong> Size of right margin in pixels.</li>
114  *      </ul>
115  * @param {String} [config.legendPos] (Optional) Used to specify where the legend will render. Currently only supports
116  *      "none" to disable the rendering of the legend. There are future plans to support "left" and "right" as well.
117  *      Defaults to "right".
118  * @param {String} [config.bgColor] (Optional) The string representation of the background color. Defaults to white.
119  * @param {String} [config.gridColor] (Optional) The string representation of the grid color. Defaults to white.
120  * @param {String} [config.gridLineColor] (Optional) The string representation of the line colors used as part of the grid.
121  *      Defaults to grey (#dddddd).
122  * @param {Boolean} [config.clipRect] (Optional) Used to toggle the use of a clipRect, which prevents values that appear
123  *      outside of the specified grid area from being visible. Use of clipRect can negatively affect performance, do not
124  *      use if there is a large amount of elements on the grid. Defaults to false.
125  * @param {String} [config.fontFamily] (Optional) Font-family to use for plot text (labels, legend, etc.).
126  * @param {Boolean} [config.throwErrors] (Optional) Used to toggle between the plot throwing errors or displaying errors.
127  *      If true the plot will throw an error instead of displaying an error when necessary and possible. Defaults to
128  *      false.
129  *
130  * @param {Boolean} [config.requireYLogGutter] (Optional) Used to indicate that the plot has non-positive data on x dimension
131  *      that should be displayed in y log gutter in log scale.
132  * @param {Boolean} [config.requireXLogGutter] (Optional) Used to indicate that the plot has non-positive data on y dimension
133  *      that should be displayed in x log gutter in log scale.
134  * @param {Boolean} [config.isMainPlot] (Optional) Used in combination with requireYLogGutter and requireXLogGutter to
135  *      shift the main plot's axis position in order to show log gutters.
136  * @param {Boolean} [config.isShowYAxis] (Optional) Used to draw the Y axis to separate positive and negative values
137  *      for log scale plot in the undefined X gutter plot.
138  * @param {Boolean} [config.isShowXAxis] (Optional) Used to draw the X axis to separate positive and negative values
139  *      for log scale plot in the undefined Y gutter plot.
140  * @param {Float} [config.minXPositiveValue] (Optional) Used to adjust domains with non-positive lower bound and generate x axis
141  *      log scale wrapper for plots that contain <= 0 x value.
142  * @param {Float} [config.minYPositiveValue] (Optional) Used to adjust domains with non-positive lower bound and generate y axis
143  *      log scale wrapper for plots that contain <= 0 y value.
144  *
145  *
146   @example
147  In this example we will create a simple scatter plot.
148  
149  <div id='plot'>
150  </div id='plot'>
151  <script type="text/javascript">
152 var scatterData = [];
153 
154 // Here we're creating some fake data to create a plot with.
155 for(var i = 0; i < 1000; i++){
156     var point = {
157         x: {value: parseInt((Math.random()*(150)))},
158         y: Math.random() * 1500
159     };
160     scatterData.push(point);
161 }
162 
163 // Create a new layer object.
164 var pointLayer = new LABKEY.vis.Layer({
165 	geom: new LABKEY.vis.Geom.Point()
166 });
167 
168 
169 // Create a new plot object.
170 var scatterPlot = new LABKEY.vis.Plot({
171 	renderTo: 'plot',
172 	width: 900,
173 	height: 700,
174 	data: scatterData,
175 	layers: [pointLayer],
176 	aes: {
177 		// Aesthetic mappings can be functions or strings.
178 		x: function(row){return row.x.value},
179 		y: 'y'
180 	}
181 });
182 
183 scatterPlot.render();
184  </script>
185 
186  @example
187  In this example we create a simple box plot.
188 
189  <div id='plot'>
190  </div id='plot'>
191  <script type="text/javascript">
192     // First let's create some data.
193 
194 var boxPlotData = [];
195 
196 for(var i = 0; i < 6; i++){
197     var group = "Group "+(i+1);
198     for(var j = 0; j < 25; j++){
199         boxPlotData.push({
200             group: group,
201             //Compute a random age between 25 and 55
202             age: parseInt(25+(Math.random()*(55-25))),
203             gender: parseInt((Math.random()*2)) === 0 ? 'male' : 'female'
204         });
205     }
206     for(j = 0; j < 3; j++){
207         boxPlotData.push({
208             group: group,
209             //Compute a random age between 75 and 95
210             age: parseInt(75+(Math.random()*(95-75))),
211             gender: parseInt((Math.random()*2)) === 0 ? 'male' : 'female'
212         });
213     }
214     for(j = 0; j < 3; j++){
215         boxPlotData.push({
216             group: group,
217             //Compute a random age between 1 and 16
218             age: parseInt(1+(Math.random()*(16-1))),
219             gender: parseInt((Math.random()*2)) === 0 ? 'male' : 'female'
220         });
221     }
222 }
223 
224 
225 // Now we create the Layer.
226 var boxLayer = new LABKEY.vis.Layer({
227     geom: new LABKEY.vis.Geom.Boxplot({
228     	// Customize the Boxplot Geom to fit our needs.
229 		position: 'jitter',
230 		outlierOpacity: '1',
231 		outlierFill: 'red',
232 		showOutliers: true,
233 		opacity: '.5',
234 		outlierColor: 'red'
235     }),
236     aes: {
237         hoverText: function(x, stats){
238             return x + ':\nMin: ' + stats.min + '\nMax: ' + stats.max + '\nQ1: ' +
239                 stats.Q1 + '\nQ2: ' + stats.Q2 + '\nQ3: ' + stats.Q3;
240         },
241         outlierHoverText: function(row){
242             return "Group: " + row.group + ", Age: " + row.age;
243         },
244         outlierShape: function(row){return row.gender;}
245     }
246 });
247 
248 
249 // Create a new Plot object.
250 var boxPlot = new LABKEY.vis.Plot({
251     renderTo: 'plot',
252     width: 900,
253     height: 300,
254     labels: {
255         main: {value: 'Example Box Plot'},
256         yLeft: {value: 'Age'},
257         x: {value: 'Groups of People'}
258     },
259     data: boxPlotData,
260     layers: [boxLayer],
261     aes: {
262         yLeft: 'age',
263         x: 'group'
264     },
265     scales: {
266         x: {
267             scaleType: 'discrete'
268         },
269         yLeft: {
270             scaleType: 'continuous',
271             trans: 'linear'
272         }
273     },
274     margins: {
275         bottom: 75
276     }
277 });
278 
279 boxPlot.render();
280  </script>
281 
282  */
283 (function(){
284     var initMargins = function(userMargins, legendPos, allAes, scales){
285         var margins = {}, top = 75, right = 75, bottom = 50, left = 75; // Defaults.
286         var foundLegendScale = false, foundYRight = false;
287 
288         for(var i = 0; i < allAes.length; i++){
289             var aes = allAes[i];
290             if(!foundLegendScale && (aes.shape || (aes.color && (!scales.color || (scales.color && scales.color.scaleType == 'discrete'))) || aes.outlierColor || aes.outlierShape || aes.pathColor) && legendPos != 'none'){
291                 foundLegendScale = true;
292                 right = right + 150;
293             }
294 
295             if(!foundYRight && aes.yRight){
296                 foundYRight = true;
297                 right = right + 25;
298             }
299         }
300 
301         if(!userMargins){
302             userMargins = {};
303         }
304 
305         if(typeof userMargins.top === 'undefined'){
306             margins.top = top;
307         } else {
308             margins.top = userMargins.top;
309         }
310         if(typeof userMargins.right === 'undefined'){
311             margins.right = right;
312         } else {
313             margins.right = userMargins.right;
314         }
315         if(typeof userMargins.bottom === 'undefined'){
316             margins.bottom = bottom;
317         } else {
318             margins.bottom = userMargins.bottom;
319         }
320         if(typeof userMargins.left === 'undefined'){
321             margins.left = left;
322         } else {
323             margins.left = userMargins.left;
324         }
325 
326         return margins;
327     };
328 
329     var initGridDimensions = function(grid, margins) {
330         grid.leftEdge = margins.left;
331         grid.rightEdge = grid.width - margins.right + 10;
332         grid.topEdge = margins.top;
333         grid.bottomEdge = grid.height - margins.bottom;
334         return grid;
335     };
336 
337     var copyUserScales = function(origScales) {
338         // 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
339         // scales separately (this.originalScales).
340         var scales = {}, newScaleName, origScale, newScale;
341         for (var scaleName in origScales) {
342             if (origScales.hasOwnProperty(scaleName)) {
343                 if(scaleName == 'y'){
344                     origScales.yLeft = origScales.y;
345                     newScaleName = (scaleName == 'y') ? 'yLeft' : scaleName;
346                 } else {
347                     newScaleName = scaleName;
348                 }
349                 newScale = {};
350                 origScale = origScales[scaleName];
351 
352                 newScale.scaleType = origScale.scaleType ? origScale.scaleType : 'continuous';
353                 newScale.sortFn = origScale.sortFn ? origScale.sortFn : null;
354                 newScale.trans = origScale.trans ? origScale.trans : 'linear';
355                 newScale.tickFormat = origScale.tickFormat ? origScale.tickFormat : null;
356                 newScale.tickDigits = origScale.tickDigits ? origScale.tickDigits : null;
357                 newScale.tickLabelMax = origScale.tickLabelMax ? origScale.tickLabelMax : null;
358                 newScale.tickHoverText = origScale.tickHoverText ? origScale.tickHoverText : null;
359                 newScale.tickCls = origScale.tickCls ? origScale.tickCls : null;
360                 newScale.tickRectCls = origScale.tickRectCls ? origScale.tickRectCls : null;
361                 newScale.tickRectHeightOffset = origScale.tickRectHeightOffset ? origScale.tickRectHeightOffset : null;
362                 newScale.tickRectWidthOffset = origScale.tickRectWidthOffset ? origScale.tickRectWidthOffset : null;
363                 newScale.domain = origScale.domain ? origScale.domain : null;
364                 newScale.range = origScale.range ? origScale.range : null;
365                 newScale.fontSize = origScale.fontSize ? origScale.fontSize : null;
366 
367                 newScale.tickClick = origScale.tickClick ? origScale.tickClick : null;
368                 newScale.tickMouseOver = origScale.tickMouseOver ? origScale.tickMouseOver : null;
369                 newScale.tickMouseOut = origScale.tickMouseOut ? origScale.tickMouseOut : null;
370 
371                 if (!origScale.domain &&((origScale.hasOwnProperty('min') && LABKEY.vis.isValid(origScale.min)) ||
372                         (origScale.hasOwnProperty('max') && LABKEY.vis.isValid(origScale.max)))) {
373                     console.log('scale.min and scale.max are deprecated. Please use scale.domain.');
374                     newScale.domain = [origScale.min, origScale.max];
375                     origScale.domain = [origScale.min, origScale.max];
376                 }
377 
378                 if (newScale.scaleType == 'ordinal' || newScale.scaleType == 'categorical') {
379                     newScale.scaleType = 'discrete';
380                 }
381 
382                 scales[newScaleName] = newScale;
383             }
384         }
385         return scales;
386     };
387 
388     var setupDefaultScales = function(scales, aes) {
389         for (var aesthetic in aes) {
390             if (aes.hasOwnProperty(aesthetic)) {
391                 if(!scales[aesthetic]){
392                     // Not all aesthetics get a scale (like hoverText), so we have to be pretty specific.
393                     if(aesthetic === 'x' || aesthetic === 'xTop' || aesthetic === 'yLeft' || aesthetic === 'yRight'
394                             || aesthetic === 'size'){
395                         scales[aesthetic] = {scaleType: 'continuous', trans: 'linear'};
396                     } else if (aesthetic == 'color' || aesthetic == 'outlierColor' || aesthetic == 'pathColor') {
397                         scales['color'] = {scaleType: 'discrete'};
398                     } else if(aesthetic == 'shape' || aesthetic == 'outlierShape'){
399                         scales['shape'] = {scaleType: 'discrete'};
400                     }
401                 }
402             }
403         }
404     };
405 
406     var getDiscreteAxisDomain = function(data, acc) {
407         // If any axis is discerete we need to know the domain before rendering so we render the grid correctly.
408         var domain = [], uniques = {}, i, value;
409 
410         for (i = 0; i < data.length; i++) {
411             value = acc(data[i]);
412             uniques[value] = true;
413         }
414 
415         for (value in uniques) {
416             if(uniques.hasOwnProperty(value)) {
417                 domain.push(value);
418             }
419         }
420 
421         return domain;
422     };
423 
424     var getContinuousDomain = function(aesName, userScale, data, acc, errorAes) {
425         var userMin, userMax, min, max, minAcc, maxAcc;
426 
427         if (userScale && userScale.domain) {
428             userMin = userScale.domain[0];
429             userMax = userScale.domain[1];
430         }
431 
432         if (LABKEY.vis.isValid(userMin)) {
433             min = userMin;
434         } else {
435             if ((aesName == 'yLeft' || aesName == 'yRight') && errorAes) {
436                 minAcc = function(d) {
437                     if (LABKEY.vis.isValid(acc(d))) {
438                         return acc(d) - errorAes.getValue(d);
439                     }
440                     else {
441                         return null;
442                     }
443                 };
444             } else {
445                 minAcc = acc;
446             }
447 
448             min = d3.min(data, minAcc);
449         }
450 
451         if (LABKEY.vis.isValid(userMax)) {
452             max = userMax;
453         } else {
454             if ((aesName == 'yLeft' || aesName == 'yRight') && errorAes) {
455                 maxAcc = function(d) {
456                     return acc(d) + errorAes.getValue(d);
457                 }
458             } else {
459                 maxAcc = acc;
460             }
461             max = d3.max(data, maxAcc);
462         }
463 
464         if (min == max ) {
465             // use *2 and /2 so that we won't end up with <= 0 value for log scale
466             if (userScale && userScale.trans && userScale.trans === 'log') {
467                 max = max * 2;
468                 min = min / 2;
469             }
470             else {
471                 max = max + 1;
472                 min = min - 1;
473             }
474         }
475 
476         /*
477             TODO: Hack to keep time charts from getting in a bad state. They currently rely on us rendering an empty
478             grid when there is an invalid x-axis. We should fix the time chart validation methods and remove this.
479          */
480         if (isNaN(min) && isNaN(max)) {
481             return [0,0];
482         }
483 
484         return [min, max];
485     };
486 
487     var getDomain = function(aesName, userScale, scale, data, acc, errorAes) {
488         var tempDomain, curDomain = scale.domain, domain = [];
489 
490         if (scale.scaleType == 'discrete') {
491             tempDomain = getDiscreteAxisDomain(data, acc);
492 
493             if (scale.sortFn) {
494                 tempDomain.sort(scale.sortFn);
495             }
496 
497             if (!curDomain) {
498                 return tempDomain;
499             }
500 
501             for (var i = 0; i < tempDomain.length; i++) {
502                 if (curDomain.indexOf(tempDomain[i]) == -1) {
503                     curDomain.push(tempDomain[i]);
504                 }
505             }
506 
507             if (scale.sortFn) {
508                 curDomain.sort(scale.sortFn);
509             }
510 
511             return curDomain;
512         } else {
513             tempDomain = getContinuousDomain(aesName, userScale, data, acc, errorAes);
514             if (!curDomain) {
515                 return tempDomain;
516             }
517 
518             if (!LABKEY.vis.isValid(curDomain[0]) || tempDomain[0] < curDomain[0]) {
519                 domain[0] = tempDomain[0];
520             } else {
521                 domain[0] = curDomain[0];
522             }
523 
524             if (!LABKEY.vis.isValid(curDomain[1]) || tempDomain[1] > curDomain[1]) {
525                 domain[1] = tempDomain[1];
526             } else {
527                 domain[1] = curDomain[1];
528             }
529         }
530 
531         return domain;
532     };
533 
534     var requiresDomain = function(name, colorScale) {
535         if (name == 'yLeft' || name == 'yRight' || name == 'x' || name == 'xTop' || name == 'size') {
536             return true;
537         }
538         // We only need the domain of the a color scale if it's a continuous one.
539         return (name == 'color' || name == 'outlierColor') && colorScale && colorScale.scaleType == 'continuous';
540     };
541 
542     var calculateDomains = function(userScales, scales, allAes, allData) {
543         var i, aesName, scale, userScale;
544         for (i = 0; i < allAes.length; i++) {
545             for (aesName in allAes[i]) {
546                 if (allAes[i].hasOwnProperty(aesName) && requiresDomain(aesName, scales.color)) {
547                     if (aesName == 'outlierColor') {
548                         scale = scales.color;
549                         userScale = userScales.color;
550                     } else {
551                         scale = scales[aesName];
552                         userScale = userScales[aesName];
553                     }
554 
555                     scale.domain = getDomain(aesName, userScale, scale, allData[i], allAes[i][aesName].getValue, allAes[i].error);
556                 }
557             }
558         }
559     };
560 
561     var getDefaultRange = function(scaleName, scale) {
562         if (scaleName == 'color' && scale.scaleType == 'continuous') {
563             return ['#222222', '#EEEEEE'];
564         }
565 
566         if (scaleName == 'color' && scale.scaleType == 'discrete') {
567             return LABKEY.vis.Scale.ColorDiscrete();
568         }
569 
570         if (scaleName == 'shape') {
571             return LABKEY.vis.Scale.Shape();
572         }
573 
574         if (scaleName == 'size') {
575             return [1, 5];
576         }
577 
578         return null;
579     };
580 
581     var calculateAxisScaleRanges = function(scales, grid, margins) {
582         var yRange = [grid.bottomEdge, grid.topEdge];
583 
584         if (scales.yRight) {
585             scales.yRight.range = yRange;
586         }
587 
588         if (scales.yLeft) {
589             scales.yLeft.range = yRange;
590         }
591 
592         var setXAxisRange = function(scale) {
593             if (scale.scaleType == 'continuous') {
594                 scale.range = [margins.left, grid.width - margins.right];
595             }
596             else {
597                 // We don't need extra padding in the discrete case because we use rangeBands which take care of that.
598                 scale.range = [grid.leftEdge, grid.rightEdge];
599             }
600         };
601 
602         if (scales.x) {
603             setXAxisRange(scales.x);
604         }
605 
606         if (scales.xTop) {
607             setXAxisRange(scales.xTop);
608         }
609     };
610 
611     var getLogScale = function (domain, range, minPositiveValue) {
612         var scale, scaleWrapper, increment = 0;
613 
614         // Issue 24727: adjust domain range to compensate for log scale fitting error margin
615         // With log scale, log transformation is applied before the mapping (fitting) to result range
616         // Javascript has binary floating points calculation issues. Use a small error constant to compensate.
617         var scaleRoundingEpsilon = 0.0001 * 0.5; // divide by half so that <= 0 value can be distinguashed from > 0 value
618 
619         if (minPositiveValue) {
620             scaleRoundingEpsilon = minPositiveValue * getLogDomainLowerBoundRatio(domain, range, minPositiveValue);
621         }
622 
623         // domain must not include or cross zero
624         if (domain[0] <= scaleRoundingEpsilon) {
625             // Issue 24967: incrementing domain is causing issue with brushing extent
626             // Ideally we'd increment as little as possible
627             increment = scaleRoundingEpsilon - domain[0];
628             domain[0] = domain[0] + increment;
629             domain[1] = domain[1] + increment;
630         }
631         else {
632             domain[0] = domain[0] - scaleRoundingEpsilon;
633             domain[1] = domain[1] + scaleRoundingEpsilon;
634         }
635 
636         scale = d3.scale.log().domain(domain).range(range);
637 
638         scaleWrapper = function(val) {
639             if(val != null) {
640                 if (increment > 0 && val <= scaleRoundingEpsilon) {
641                     // <= 0 points are now part of the main plot, it's illegal to pass negative value to a log scale with positive domain.
642                     // 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
643                     return scale(scaleRoundingEpsilon);
644                 }
645                 // use the original value to calculate the scaled value for all > 0 data
646                 return scale(val);
647             }
648 
649             return null;
650         };
651         scaleWrapper.domain = scale.domain;
652         scaleWrapper.range = scale.range;
653         scaleWrapper.invert = scale.invert;
654         scaleWrapper.base = scale.base;
655         scaleWrapper.clamp = scale.clamp;
656         scaleWrapper.ticks = function(){
657             var allTicks = scale.ticks();
658             var ticksToShow = [];
659 
660             if (allTicks.length < 2) {
661                 //make sure that at least 2 tick marks are shown for reference
662                 // skip rounding down if rounds down to 0, which is not allowed for log
663                 return [Math.ceil(scale.domain()[0]), Math.abs(scale.domain()[1]) < 1 ? scale.domain()[1] : Math.floor(scale.domain()[1])];
664             }
665             else if(allTicks.length < 10){
666                 return allTicks;
667             }
668             else {
669                 for(var i = 0; i < allTicks.length; i++){
670                     if(i % 9 == 0){
671                         ticksToShow.push(allTicks[i]);
672                     }
673                 }
674                 return ticksToShow;
675             }
676         };
677 
678         return scaleWrapper;
679     };
680 
681     // The lower domain bound need to adjusted to so that enough space is reserved for log gutter.
682     // The calculation takes into account the available plot size (range), max and min values (domain) in the plot.
683     var getLogDomainLowerBoundRatio = function(domain, range, minPositiveValue) {
684         // use 0.5 as a base ratio, so that plot distributions on edge grids are not skewed
685         var ratio = 0.5, logGutterSize = 30;
686         var gridNum = Math.ceil(Math.log(domain[1] / minPositiveValue)); // the number of axis ticks, equals order of magnitude diff of positive domain range
687         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
688 
689         if (gridNum > rangeOrder) {
690             for (var i = 0; i < gridNum - rangeOrder; i++) {
691                 ratio *= 0.5;
692             }
693         }
694         else{
695             var gridSize = Math.abs(range[1] - range[0]) / gridNum; // the actual grid size of each grid
696 
697             // adjust ratio so that positive data won't fall into log gutter area
698             if (gridSize/2 < logGutterSize){
699                 ratio = 1 - logGutterSize/gridSize;
700             }
701         }
702         return ratio;
703     };
704 
705     var instantiateScales = function(plot, margins) {
706         var userScales = plot.originalScales, scales = plot.scales, grid = plot.grid, isMainPlot = plot.isMainPlot,
707             xLogGutter = plot.xLogGutter, yLogGutter = plot.yLogGutter, minXPositiveValue = plot.minXPositiveValue, minYPositiveValue = plot.minYPositiveValue;
708 
709         var scaleName, scale, userScale;
710 
711         calculateAxisScaleRanges(scales, grid, margins);
712 
713         if (isMainPlot) {
714             // adjust the plot range to reserve space for log gutter
715             var mainPlotRangeAdjustment = 30;
716             if (xLogGutter) {
717                 if (scales.yLeft && Ext.isArray(scales.yLeft.range)) {
718                     scales.yLeft.range = [scales.yLeft.range[0] + mainPlotRangeAdjustment, scales.yLeft.range[1]];
719                 }
720             }
721             if (yLogGutter) {
722                 if (scales.x && Ext.isArray(scales.x.range)) {
723                     scales.x.range = [scales.x.range[0] - mainPlotRangeAdjustment, scales.x.range[1]];
724                 }
725             }
726         }
727 
728         for (scaleName in scales) {
729             if (scales.hasOwnProperty(scaleName)) {
730                 scale = scales[scaleName];
731                 userScale = userScales[scaleName];
732 
733                 if (scale.scaleType == 'discrete') {
734                     if (scaleName == 'x' || scaleName == 'xTop' || scaleName == 'yLeft' || scaleName == 'yRight'){
735                         // Setup scale with domain (user provided or calculated) and compute range based off grid dimensions.
736                         scale.scale = d3.scale.ordinal().domain(scale.domain).rangeBands(scale.range, 1);
737                     } else {
738                         // Setup scales with user provided range or default range.
739                         if (userScale && userScale.scale) {
740                             scale.scale = userScale.scale;
741                         }
742                         else {
743                             scale.scale = d3.scale.ordinal();
744                         }
745 
746                         if (scale.domain) {
747                             scale.scale.domain(scale.domain);
748                         }
749 
750                         if (!scale.range) {
751                             scale.range = getDefaultRange(scaleName, scale);
752                         }
753 
754                         if (scale.scale.range) {
755                             scale.scale.range(scale.range);
756                         }
757                     }
758                 } else {
759                     if ((scaleName == 'color' || scaleName == 'size') && !scale.range) {
760                         scale.range = getDefaultRange(scaleName, scale);
761                     }
762 
763                     if (scale.range && scale.domain && LABKEY.vis.isValid(scale.domain[0]) && LABKEY.vis.isValid(scale.domain[1])) {
764                         if (scale.trans == 'linear') {
765                             scale.scale = d3.scale.linear().domain(scale.domain).range(scale.range);
766                         } else {
767                             if (scaleName == 'x' || scaleName == 'xTop') {
768                                 scale.scale = getLogScale(scale.domain, scale.range, minXPositiveValue);
769                             }
770                             else {
771                                 scale.scale = getLogScale(scale.domain, scale.range, minYPositiveValue);
772                             }
773                         }
774                     }
775                 }
776             }
777         }
778     };
779 
780     var initializeScales = function(plot, allAes, allData, margins, errorFn) {
781         var userScales = plot.originalScales, scales = plot.scales;
782 
783         for (var i = 0; i < allAes.length; i++) {
784             setupDefaultScales(scales, allAes[i]);
785         }
786 
787         for (var scaleName in scales) {
788             if(scales.hasOwnProperty(scaleName)) {
789                 if (scales[scaleName].scale) {
790                     delete scales[scaleName].scale;
791                 }
792 
793                 if (scales[scaleName].domain && (userScales[scaleName] && !userScales[scaleName].domain)) {
794                     delete scales[scaleName].domain;
795                 }
796 
797                 if (scales[scaleName].range && (userScales[scaleName] && !userScales[scaleName].range)) {
798                     delete scales[scaleName].range;
799                 }
800             }
801         }
802 
803         calculateDomains(userScales, scales, allAes, allData);
804         instantiateScales(plot, margins);
805 
806         if ((scales.x && !scales.x.scale) || (scales.xTop && !scales.xTop.scale)) {
807             errorFn('Unable to create an x scale, rendering aborted.');
808             return false;
809         }
810 
811         if((!scales.yLeft || !scales.yLeft.scale) && (!scales.yRight ||!scales.yRight.scale)){
812             errorFn('Unable to create a y scale, rendering aborted.');
813             return false;
814         }
815 
816         return true;
817     };
818 
819     var compareDomains  = function(domain1, domain2){
820         if(domain1.length != domain2.length){
821             return false;
822         }
823 
824         domain1.sort();
825         domain2.sort();
826 
827         for(var i = 0; i < domain1.length; i++){
828             if(domain1[i] != domain2[i]) {
829                 return false;
830             }
831         }
832 
833         return true;
834     };
835 
836     var generateLegendData = function(legendData, domain, colorFn, shapeFn){
837         for(var i = 0; i < domain.length; i++) {
838             legendData.push({
839                 text: domain[i],
840                 color: colorFn != null ? colorFn(domain[i]) : null,
841                 shape: shapeFn != null ? shapeFn(domain[i]) : null
842             });
843         }
844     };
845 
846     LABKEY.vis.Plot = function(config){
847         if(config.hasOwnProperty('rendererType') && config.rendererType == 'd3') {
848             this.yLogGutter = config.requireYLogGutter ? true : false;
849             this.xLogGutter = config.requireXLogGutter ? true : false;
850             this.isMainPlot = config.isMainPlot ? true : false;
851             this.isShowYAxisGutter = config.isShowYAxis ? true : false;
852             this.isShowXAxisGutter = config.isShowXAxis ? true : false;
853             this.minXPositiveValue = config.minXPositiveValue;
854             this.minYPositiveValue = config.minYPositiveValue;
855 
856             this.renderer = new LABKEY.vis.internal.D3Renderer(this);
857         } else {
858             this.renderer = new LABKEY.vis.internal.RaphaelRenderer(this);
859         }
860 
861         var error = function(msg){
862             if (this.throwErrors){
863                 throw new Error(msg);
864             } else {
865                 console.error(msg);
866                 if(console.trace){
867                     console.trace();
868                 }
869 
870                 this.renderer.renderError(msg);
871             }
872         };
873 
874         this.renderTo = config.renderTo ? config.renderTo : null; // The id of the DOM element to render the plot to, required.
875         this.grid = {
876             width: config.hasOwnProperty('width') ? config.width : null, // height of the grid where shapes/lines/etc gets plotted.
877             height: config.hasOwnProperty('height') ? config.height: null // widht of the grid.
878         };
879         this.originalScales = config.scales ? config.scales : {}; // The scales specified by the user.
880         this.scales = copyUserScales(this.originalScales); // The scales used internally.
881         this.originalAes = config.aes ? config.aes : null; // The original aesthetic specified by the user.
882         this.aes = LABKEY.vis.convertAes(this.originalAes); // The aesthetic object used internally.
883         this.labels = config.labels ? config.labels : {};
884         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'})
885         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).
886         this.clipRect = config.clipRect ? config.clipRect : false;
887         this.legendPos = config.legendPos;
888         this.throwErrors = config.throwErrors || false; // Allows the configuration to specify whether chart errors should be thrown or logged (default).
889         this.brushing = ('brushing' in config && config.brushing != null && config.brushing != undefined) ? config.brushing : null;
890         this.legendData = config.legendData ? config.legendData : null; // An array of rows for the legend text/color/etc. Optional.
891         this.disableAxis = config.disableAxis ? config.disableAxis : {xTop: false, xBottom: false, yLeft: false, yRight: false};
892         this.bgColor = config.bgColor ? config.bgColor : null;
893         this.gridColor = config.gridColor ? config.gridColor : null;
894         this.gridLineColor = config.gridLineColor ? config.gridLineColor : null;
895         this.gridLinesVisible = config.gridLinesVisible ? config.gridLinesVisible : null;
896         this.fontFamily = config.fontFamily ? config.fontFamily : null;
897         this.tickColor = config.tickColor ? config.tickColor : null;
898         this.borderColor = config.borderColor ? config.borderColor : null;
899         this.tickTextColor = config.tickTextColor ? config.tickTextColor : null;
900         this.tickLength = config.hasOwnProperty('tickLength') ? config.tickLength : null;
901         this.tickWidth = config.hasOwnProperty('tickWidth') ? config.tickWidth : null;
902         this.tickOverlapRotation = config.hasOwnProperty('tickOverlapRotation') ? config.tickOverlapRotation : null;
903         this.gridLineWidth = config.hasOwnProperty('gridLineWidth') ? config.gridLineWidth : null;
904         this.borderWidth = config.hasOwnProperty('borderWidth') ? config.borderWidth : null;
905 
906         // Stash the user's margins so when we re-configure margins during re-renders or setAes we don't forget the user's settings.
907         var allAes = [], margins = {}, userMargins = config.margins ? config.margins : {};
908 
909         allAes.push(this.aes);
910 
911         for(var i = 0; i < this.layers.length; i++){
912             if(this.layers[i].aes){
913                 allAes.push(this.layers[i].aes);
914             }
915         }
916 
917         if(this.labels.y){
918             this.labels.yLeft = this.labels.y;
919             this.labels.y = null;
920         }
921 
922         if(this.grid.width == null){
923             error.call(this, "Unable to create plot, width not specified");
924             return;
925         }
926 
927         if(this.grid.height == null){
928             error.call(this, "Unable to create plot, height not specified");
929             return;
930         }
931 
932         if(this.renderTo == null){
933             error.call(this, "Unable to create plot, renderTo not specified");
934             return;
935         }
936 
937         for(var aesthetic in this.aes){
938             if (this.aes.hasOwnProperty(aesthetic)) {
939                 LABKEY.vis.createGetter(this.aes[aesthetic]);
940             }
941         }
942 
943         this.getLegendData = function(){
944             var legendData = [];
945 
946             if ((this.scales.color && this.scales.color.scaleType === 'discrete') && this.scales.shape) {
947                 if(compareDomains(this.scales.color.scale.domain(), this.scales.shape.scale.domain())){
948                     // The color and shape domains are the same. Merge them in the legend.
949                     generateLegendData(legendData, this.scales.color.scale.domain(), this.scales.color.scale, this.scales.shape.scale);
950                 } else {
951                     // The color and shape domains are different.
952                     generateLegendData(legendData, this.scales.color.scale.domain(), this.scales.color.scale, null);
953                     generateLegendData(legendData, this.scales.shape.scale.domain(), null, this.scales.shape.scale);
954                 }
955             } else if(this.scales.color && this.scales.color.scaleType === 'discrete') {
956                 generateLegendData(legendData, this.scales.color.scale.domain(), this.scales.color.scale, null);
957             } else if(this.scales.shape) {
958                 generateLegendData(legendData, this.scales.shape.scale.domain(), null, this.scales.shape.scale);
959             }
960 
961             return legendData;
962         };
963 
964         /**
965          * Renders the plot.
966          */
967         this.render = function(){
968             margins = initMargins(userMargins, this.legendPos, allAes, this.scales);
969             this.grid = initGridDimensions(this.grid, margins);
970             this.renderer.initCanvas(); // Get the canvas prepped for render time.
971             var allData = [this.data];
972             var plot = this;
973             var errorFn = function(msg) {
974                 error.call(plot, msg);
975             };
976             allAes = [this.aes];
977             for (var i = 0; i < this.layers.length; i++) {
978                 // If the layer doesn't have data or aes, then it doesn't need to be considered for any scale calculations.
979                 if (!this.layers[i].data && !this.layers[i].aes) {continue;}
980                 allData.push(this.layers[i].data ? this.layers[i].data : this.data);
981                 allAes.push(this.layers[i].aes ? this.layers[i].aes : this.aes);
982             }
983 
984             if(!initializeScales(this, allAes, allData, margins, errorFn)){  // Sets up the scales.
985                 return false; // if we have a critical error when trying to initialize the scales we don't continue with rendering.
986             }
987 
988             if(!this.layers || this.layers.length < 1){
989                 error.call(this,'No layers added to the plot, nothing to render.');
990                 return false;
991             }
992 
993             this.renderer.renderGrid(); // renders the grid (axes, grid lines).
994             this.renderer.renderLabels();
995 
996             for(var i = 0; i < this.layers.length; i++){
997                 this.layers[i].plot = this; // Add reference to the layer so it can trigger a re-render during setAes.
998                 this.layers[i].render(this.renderer, this.grid, this.scales, this.data, this.aes, i);
999             }
1000 
1001             if(!this.legendPos || (this.legendPos && !(this.legendPos == "none"))){
1002                 this.renderer.renderLegend();
1003             }
1004 
1005             return true;
1006         };
1007 
1008         var setLabel = function(name, value, lookClickable){
1009             if(!this.labels[name]){
1010                 this.labels[name] = {};
1011             }
1012 
1013             this.labels[name].value = value;
1014             this.labels[name].lookClickable = lookClickable;
1015             this.renderer.renderLabel(name);
1016         };
1017 
1018         /**
1019          * Sets the value of the main label and optionally makes it look clickable.
1020          * @param {String} value The string value to set the label to.
1021          * @param {Boolean} lookClickable If true it styles the label to look clickable.
1022          */
1023         this.setMainLabel = function(value, lookClickable){
1024             setLabel.call(this, 'main', value, lookClickable);
1025         };
1026 
1027         /**
1028          * Sets the value of the x-axis label and optionally makes it look clickable.
1029          * @param {String} value The string value to set the label to.
1030          * @param {Boolean} lookClickable If true it styles the label to look clickable.
1031          */
1032         this.setXLabel = function(value, lookClickable){
1033             setLabel.call(this, 'x', value, lookClickable);
1034         };
1035 
1036         /**
1037          * Sets the value of the right y-axis label and optionally makes it look clickable.
1038          * @param {String} value The string value to set the label to.
1039          * @param {Boolean} lookClickable If true it styles the label to look clickable.
1040          */
1041         this.setYRightLabel = function(value, lookClickable){
1042             setLabel.call(this, 'yRight', value, lookClickable);
1043         };
1044 
1045         /**
1046          * Sets the value of the left y-axis label and optionally makes it look clickable.
1047          * @param {String} value The string value to set the label to.
1048          * @param {Boolean} lookClickable If true it styles the label to look clickable.
1049          */
1050         this.setYLeftLabel = this.setYLabel = function(value, lookClickable){
1051             setLabel.call(this, 'yLeft', value, lookClickable);
1052         };
1053 
1054         /**
1055          * Adds a listener to a label.
1056          * @param {String} label string value of label to add a listener to. Valid values are y, yLeft, yRight, x, and main.
1057          * @param {String} listener the name of the listener to listen on.
1058          * @param {Function} fn The callback to b called when the event is fired.
1059          */
1060         this.addLabelListener = function(label, listener, fn){
1061             if(label == 'y') {
1062                 label = 'yLeft';
1063             }
1064             return this.renderer.addLabelListener(label, listener, fn);
1065         };
1066 
1067         /**
1068          * Sets the height of the plot and re-renders if requested.
1069          * @param {Number} h The height in pixels.
1070          * @param {Boolean} render Toggles if plot will be re-rendered or not.
1071          */
1072         this.setHeight = function(h, render){
1073             if(render == null || render == undefined){
1074                 render = true;
1075             }
1076 
1077             this.grid.height = h;
1078 
1079             if(render === true){
1080                 this.render();
1081             }
1082         };
1083 
1084         /**
1085          * Sets the width of the plot and re-renders if requested.
1086          * @param {Number} w The width in pixels.
1087          * @param {Boolean} render Toggles if plot will be re-rendered or not.
1088          */
1089         this.setWidth = function(w, render){
1090             if(render == null || render == undefined){
1091                 render = true;
1092             }
1093 
1094             this.grid.width = w;
1095 
1096             if(render === true){
1097                 this.render();
1098             }
1099         };
1100 
1101         /**
1102          * Changes the size of the plot and renders if requested.
1103          * @param {Number} w width in pixels.
1104          * @param {Number} h height in pixels.
1105          * @param {Boolean} render Toggles if the chart will be re-rendered or not. Defaults to false.
1106          */
1107         this.setSize = function(w, h, render){
1108             this.setWidth(w, false);
1109             this.setHeight(h, render);
1110         };
1111 
1112         /**
1113          * Adds a new layer to the plot.
1114          * @param {@link LABKEY.vis.Layer} layer
1115          */
1116         this.addLayer = function(layer){
1117             layer.parent = this; // Set the parent of each layer to the plot so we can grab things like data from it later.
1118             this.layers.push(layer);
1119         };
1120 
1121         /**
1122          * Clears the grid.
1123          */
1124         this.clearGrid = function(){
1125             this.renderer.clearGrid();
1126         };
1127 
1128         /**
1129          * Sets new margins for the plot and re-renders with the margins.
1130          * @param {Object} newMargins An object with the following properties:
1131          *      <ul>
1132          *          <li><strong>top:</strong> Size of top margin in pixels.</li>
1133          *          <li><strong>bottom:</strong> Size of bottom margin in pixels.</li>
1134          *          <li><strong>left:</strong> Size of left margin in pixels.</li>
1135          *          <li><strong>right:</strong> Size of right margin in pixels.</li>
1136          *      </ul>
1137          */
1138         this.setMargins = function(newMargins, render){
1139             userMargins = newMargins;
1140             margins = initMargins(userMargins, this.legendPos, allAes, this.scales);
1141 
1142             if(render !== undefined && render !== null && render === true) {
1143                 this.render();
1144             }
1145         };
1146 
1147         this.setAes = function(newAes){
1148             // Note: this is only valid for plots using the D3Renderer.
1149             // Used to add or remove aesthetics to a plot. Also availalbe on LABKEY.vis.Layer objects to set aesthetics on
1150             // specific layers only.
1151             // To delete an aesthetic set it to null i.e. plot.setAes({color: null});
1152             LABKEY.vis.mergeAes(this.aes, newAes);
1153             this.render();
1154         };
1155 
1156         this.setBrushing = function(configBrushing) {
1157             this.renderer.setBrushing(configBrushing);
1158         };
1159 
1160         this.clearBrush = function() {
1161             if(this.renderer.clearBrush) {
1162                 this.renderer.clearBrush();
1163             }
1164         };
1165 
1166         this.getBrushExtent = function() {
1167             // Returns an array of arrays. First array is xMin, yMin, second array is xMax, yMax
1168             // If the seleciton is 1D, then the min/max of the non-selected dimension will be null/null.
1169             return this.renderer.getBrushExtent();
1170         };
1171 
1172         this.setBrushExtent = function(extent) {
1173             // Takes a 2D array. First array is xMin, yMin, second array is xMax, yMax. If the seleciton is 1D, then the
1174             // min/max of the non-selected dimension will be null/null.
1175             this.renderer.setBrushExtent(extent);
1176         };
1177 
1178         this.bindBrushing = function(otherPlots) {
1179             this.renderer.bindBrushing(otherPlots);
1180         };
1181 
1182         return this;
1183     };
1184 })();
1185 
1186 /**
1187  * @name LABKEY.vis.BarPlot
1188  * @class BarPlot wrapper to allow a user to easily create a simple bar plot without having to preprocess the data.
1189  * @param {Object} config An object that contains the following properties (in addition to those properties defined
1190  *      in {@link LABKEY.vis.Plot}).
1191  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
1192  * @param {Array} [config.data] The array of individual data points to be grouped for the bar plot. The LABKEY.vis.BarPlot
1193  *      wrapper will aggregate the data in this array based on the xAes function provided to get the individual totals
1194  *      for each bar in the plot.
1195  * @param {Function} [config.xAes] The function to determine which groups will be created for the x-axis of the plot.
1196  * @param {Object} [config.options] (Optional) Display options as defined in {@link LABKEY.vis.Geom.BarPlot}.
1197  *
1198  @example
1199  <div id='bar'></div>
1200  <script type="text/javascript">
1201  // Fake data which will be aggregated by the LABKEY.vis.BarPlot wrapper.
1202  var barPlotData = [
1203     {gender: 'Male', age: '21'}, {gender: 'Male', age: '43'},
1204     {gender: 'Male', age: '24'}, {gender: 'Male', age: '54'},
1205     {gender: 'Female', age: '24'}, {gender: 'Female', age: '33'},
1206     {gender: 'Female', age: '43'}, {gender: 'Female', age: '43'},
1207  ];
1208 
1209  // Create a new bar plot object.
1210  var barChart = new LABKEY.vis.BarPlot({
1211     renderTo: 'bar',
1212     rendererType: 'd3',
1213     width: 900,
1214     height: 300,
1215     labels: {
1216         main: {value: 'Example Bar Plot With Cumulative Totals'},
1217         yLeft: {value: 'Count'},
1218         x: {value: 'Value'}
1219     },
1220     options: {
1221         color: 'black',
1222         fill: '#c0c0c0',
1223         lineWidth: 1.5,
1224         colorTotal: 'black',
1225         fillTotal: 'steelblue',
1226         opacityTotal: .8,
1227         showCumulativeTotals: true
1228     },
1229     xAes: function(row){return row['age']},
1230     data: barPlotData
1231  });
1232 
1233  barChart.render();
1234  </script>
1235  */
1236 (function(){
1237 
1238     LABKEY.vis.BarPlot = function(config){
1239 
1240         if(config.renderTo == null){
1241             throw new Error("Unable to create bar plot, renderTo not specified");
1242         }
1243 
1244         if(config.data == null){
1245             throw new Error("Unable to create bar plot, data array not specified");
1246         }
1247 
1248         if(config.xAes == null){
1249             throw new Error("Unable to create bar plot, xAes function not specified");
1250         }
1251 
1252         var countData = LABKEY.vis.groupCountData(config.data, config.xAes);
1253         var showCumulativeTotals = config.options && config.options.showCumulativeTotals;
1254 
1255         config.layers = [new LABKEY.vis.Layer({
1256             geom: new LABKEY.vis.Geom.BarPlot(config.options),
1257             data: countData,
1258             aes: { x: 'name', y: 'count' }
1259         })];
1260 
1261         if (!config.scales)
1262         {
1263             config.scales = {};
1264             config.scales.x = { scaleType: 'discrete' };
1265             config.scales.y = { domain: [0, showCumulativeTotals ? countData[countData.length-1].total : null] };
1266         }
1267 
1268         if (showCumulativeTotals && !config.margins)
1269         {
1270             config.margins = {right: 125};
1271         }
1272 
1273         return new LABKEY.vis.Plot(config);
1274     };
1275 })();
1276 
1277 /**
1278  * @name LABKEY.vis.PieChart
1279  * @class PieChart which allows a user to programmatically create an interactive pie chart visualization (note: D3 rendering only).
1280  * @description The pie chart visualization is built off of the <a href="http://d3pie.org">d3pie JS library</a>. The config
1281  *      properties listed below are the only required properties to create a base pie chart. For additional display options
1282  *      and interaction options, you can provide any of the properties defined in the <a href="http://d3pie.org/#docs">d3pie docs</a>
1283  *      to the config object.
1284  * @param {Object} config An object that contains the following properties
1285  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
1286  * @param {Array} [config.data] The array of chart segment data. Each object is of the form: { label: "label", value: 123 }.
1287  * @param {Number} [config.width] The chart canvas width in pixels.
1288  * @param {Number} [config.height] The chart canvas height in pixels.
1289  *
1290  @example
1291  Example of a simple pie chart (only required config properties).
1292 
1293  <div id='pie'></div>
1294  <script type="text/javascript">
1295 
1296  </script>
1297  var pieChartData = [
1298      {label: "test1", value: 1},
1299      {label: "test2", value: 2},
1300      {label: "test3", value: 3},
1301      {label: "test4", value: 4}
1302  ];
1303 
1304  var pieChart = new LABKEY.vis.PieChart({
1305     renderTo: "pie",
1306     data: pieChartData,
1307     width: 300,
1308     height: 250
1309  });
1310 
1311  Example of a customized pie chart using some d3pie lib properties.
1312 
1313  <div id='pie2'></div>
1314  <script type="text/javascript">
1315  var pieChartData = [
1316      {label: "test1", value: 1},
1317      {label: "test2", value: 2},
1318      {label: "test3", value: 3},
1319      {label: "test4", value: 4}
1320  ];
1321 
1322  var pieChart2 = new LABKEY.vis.PieChart({
1323     renderTo: "pie2",
1324     data: pieChartData,
1325     width: 300,
1326     height: 250,
1327     // d3pie lib config properties
1328     header: {
1329         title: {
1330             text: 'Pie Chart Example'
1331         }
1332     },
1333     labels: {
1334         outer: {
1335             format: 'label-value2',
1336             pieDistance: 15
1337         },
1338         inner: {
1339             hideWhenLessThanPercentage: 10
1340         },
1341         lines: {
1342             style: 'straight',
1343             color: 'black'
1344         }
1345     },
1346     effects: {
1347         load: {
1348             speed: 2000
1349         },
1350         pullOutSegmentOnClick: {
1351             effect: 'linear',
1352             speed: '1000'
1353         },
1354         highlightLuminosity: -0.75
1355     },
1356     misc: {
1357         colors: {
1358             segments: LABKEY.vis.Scale.DarkColorDiscrete(),
1359             segmentStroke: '#a1a1a1'
1360         },
1361         gradient: {
1362             enabled: true,
1363             percentage: 60
1364         }
1365     },
1366     callbacks: {
1367         onload: function() {
1368             pieChart2.openSegment(3);
1369         }
1370     }
1371  });
1372  </script>
1373  */
1374 (function(){
1375 
1376     LABKEY.vis.PieChart = function(config){
1377 
1378         if(config.renderTo == null){
1379             throw new Error("Unable to create pie chart, renderTo not specified");
1380         }
1381 
1382         if(config.data == null){
1383             throw new Error("Unable to create pie chart, data not specified");
1384         }
1385         else if (Array.isArray(config.data)) {
1386             config.data = {content : config.data};
1387         }
1388 
1389         if(config.width == null && (config.size == null || config.size.canvasWidth == null)){
1390             throw new Error("Unable to create pie chart, width not specified");
1391         }
1392         else if(config.height == null && (config.size == null || config.size.canvasHeight == null)){
1393             throw new Error("Unable to create pie chart, height not specified");
1394         }
1395 
1396         if (config.size == null) {
1397             config.size = {}
1398         }
1399         config.size.canvasWidth = config.width || config.size.canvasWidth;
1400         config.size.canvasHeight = config.height || config.size.canvasHeight;
1401 
1402         // apply default font/colors/etc., it not explicitly set
1403         if (!config.header) config.header = {};
1404         if (!config.header.title) config.header.title = {};
1405         if (!config.header.title.font) config.header.title.font = 'Roboto, arial';
1406         if (!config.header.title.hasOwnProperty('fontSize')) config.header.title.fontSize = 18;
1407         if (!config.header.title.color) config.header.title.color = '#000000';
1408         if (!config.header.subtitle) config.header.subtitle = {};
1409         if (!config.header.subtitle.font) config.header.subtitle.font = 'Roboto, arial';
1410         if (!config.header.subtitle.hasOwnProperty('fontSize')) config.header.subtitle.fontSize = 16;
1411         if (!config.header.subtitle.color) config.header.subtitle.color = '#555555';
1412         if (!config.footer) config.footer = {};
1413         if (!config.footer.font) config.footer.font = 'Roboto, arial';
1414         if (!config.labels) config.labels = {};
1415         if (!config.labels.mainLabel) config.labels.mainLabel = {};
1416         if (!config.labels.mainLabel.font) config.labels.mainLabel.font = 'Roboto, arial';
1417         if (!config.labels.percentage) config.labels.percentage = {};
1418         if (!config.labels.percentage.font) config.labels.percentage.font = 'Roboto, arial';
1419         if (!config.labels.percentage.color) config.labels.percentage.color = '#DDDDDD';
1420         if (!config.labels.outer) config.labels.outer = {};
1421         if (!config.labels.outer.hasOwnProperty('pieDistance')) config.labels.outer.pieDistance = 10;
1422         if (!config.labels.inner) config.labels.inner = {};
1423         if (!config.labels.inner.format) config.labels.inner.format = 'percentage';
1424         if (!config.labels.inner.hasOwnProperty('hideWhenLessThanPercentage')) config.labels.inner.hideWhenLessThanPercentage = 10;
1425         if (!config.labels.lines) config.labels.lines = {};
1426         if (!config.labels.lines.style) config.labels.lines.style = 'straight';
1427         if (!config.labels.lines.color) config.labels.lines.color = '#222222';
1428         if (!config.misc) config.misc = {};
1429         if (!config.misc.colors) config.misc.colors = {};
1430         if (!config.misc.colors.segments) config.misc.colors.segments = LABKEY.vis.Scale.ColorDiscrete();
1431         if (!config.misc.colors.segmentStroke) config.misc.colors.segmentStroke = '#222222';
1432         if (!config.misc.gradient) config.misc.gradient = {};
1433         if (!config.misc.gradient.enabled) config.misc.gradient.enabled = false;
1434         if (!config.misc.gradient.hasOwnProperty('percentage')) config.misc.gradient.percentage = 95;
1435         if (!config.misc.gradient.color) config.misc.gradient.color = "#000000";
1436         if (!config.effects) config.effects = {};
1437         if (!config.effects.pullOutSegmentOnClick) config.effects.pullOutSegmentOnClick = {};
1438         if (!config.effects.pullOutSegmentOnClick.effect) config.effects.pullOutSegmentOnClick.effect = 'none';
1439         if (!config.tooltips) config.tooltips = {};
1440         if (!config.tooltips.type) config.tooltips.type = 'placeholder';
1441         if (!config.tooltips.string) config.tooltips.string = '{label}: {percentage}%';
1442         if (!config.tooltips.styles) config.tooltips.styles = {backgroundOpacity: 1};
1443 
1444         return new d3pie(config.renderTo, config);
1445     };
1446 })();
1447 
1448 /**
1449  * @name LABKEY.vis.LeveyJenningsPlot
1450  * @class LeveyJenningsPlot Wrapper to create a plot which shows data points compared to expected ranges (+/- 3 standard deviations from a mean).
1451  * @description This helper will take the input data and generate a sequencial x-axis so that all data points are the same distance apart.
1452  * @param {Object} config An object that contains the following properties
1453  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
1454  * @param {Number} [config.width] The chart canvas width in pixels.
1455  * @param {Number} [config.height] The chart canvas height in pixels.
1456  * @param {Array} [config.data] The array of chart segment data. Each object is of the form: { label: "label", value: 123 }.
1457  * @param {Object} [config.properties] An object that contains the properties specific to the Levey-Jennings plot
1458  * @param {String} [config.properties.value] The data property name for the value to be plotted on the left y-axis.
1459  * @param {String} [config.properties.valueRight] The data property name for the value to be plotted on the right y-axis.
1460  * @param {String} [config.properties.mean] The data property name for the mean of the expected range.
1461  * @param {String} [config.properties.stdDev] The data property name for the standard deviation of the expected range.
1462  * @param {String} [config.properties.xTickLabel] The data property name for the x-axis tick label.
1463  * @param {Number} [config.properties.xTickTagIndex] (Optional) The index/value of the x-axis label to be tagged (i.e. class="xticktag").
1464  * @param {Boolean} [config.properties.showTrendLine] (Optional) Whether or not to show a line connecting the data points. Default false.
1465  * @param {Boolean} [config.properties.disableRangeDisplay] (Optional) Whether or not to show the mean/stdev ranges in the plot. Defaults to false.
1466  * @param {String} [config.properties.xTick] (Optional) The data property to use for unique x-axis tick marks. Defaults to sequence from 1:data length.
1467  * @param {String} [config.properties.yAxisScale] (Optional) Whether the y-axis should be plotted with linear or log scale. Default linear.
1468  * @param {Array} [config.properties.yAxisDomain] (Optional) Y-axis min/max values. Example: [0,20].
1469  * @param {String} [config.properties.color] (Optional) The data property name for the color to be used for the data point.
1470  * @param {Array} [config.properties.colorRange] (Optional) The array of color values to use for the data points.
1471  * @param {String} [config.groupBy] (optional) The data property name used to group plot lines and points.
1472  * @param {Function} [config.properties.hoverTextFn] (Optional) The hover text to display for each data point. The parameter
1473  *                  to that function will be a row of data with access to all values for that row.
1474  * @param {Function} [config.properties.pointClickFn] (Optional) The function to call on data point click. The parameters to
1475  *                  that function will be the click event and the row of data for the selected point.
1476  */
1477 (function(){
1478 
1479     LABKEY.vis.LeveyJenningsPlot = function(config){
1480 
1481         if(config.renderTo == null) {
1482             throw new Error("Unable to create Levey-Jennings plot, renderTo not specified");
1483         }
1484 
1485         if(config.data == null) {
1486             throw new Error("Unable to create Levey-Jennings plot, data array not specified");
1487         }
1488 
1489         if (config.properties == null || config.properties.value == null || config.properties.xTickLabel == null) {
1490             throw new Error("Unable to create Levey-Jennings plot, properties object not specified. "
1491                     + "Required: value, xTickLabel. Optional: mean, stdDev, color, colorRange, hoverTextFn, "
1492                     + "pointClickFn, showTrendLine, disableRangeDisplay, xTick, yAxisScale, yAxisDomain, xTickTagIndex.");
1493         }
1494 
1495         // get a sorted array of the unique x-axis labels
1496         var uniqueXAxisKeys = {}, uniqueXAxisLabels = [];
1497         for (var i = 0; i < config.data.length; i++) {
1498             if (!uniqueXAxisKeys[config.data[i][config.properties.xTick]]) {
1499                 uniqueXAxisKeys[config.data[i][config.properties.xTick]] = true;
1500             }
1501         }
1502         uniqueXAxisLabels =  Object.keys(uniqueXAxisKeys).sort();
1503 
1504         // create a sequencial index to use for the x-axis value and keep a map from that index to the tick label
1505         // also, pull out the meanStdDev data for the unique x-axis values and calculate average values for the trend line data
1506         var tickLabelMap = {}, index = -1, distinctColorValues = [], meanStdDevData = [],
1507             groupedTrendlineData = [], groupedTrendlineSeriesData = {},
1508             hasYRightMetric = config.properties.valueRight != undefined;
1509 
1510         for (var i = 0; i < config.data.length; i++)
1511         {
1512             var row = config.data[i];
1513 
1514             // track the distinct values in the color variable so that we know if we need the legend or not
1515             if (config.properties.color && distinctColorValues.indexOf(row[config.properties.color]) == -1) {
1516                 distinctColorValues.push(row[config.properties.color]);
1517             }
1518 
1519             // if we are grouping x values based on the xTick property, only increment index if we have a new xTick value
1520             if (config.properties.xTick)
1521             {
1522                 var addValueToTrendLineData = function(dataArr, seqValue, arrKey, fieldName, rowValue, sumField, countField)
1523                 {
1524                     if (dataArr[arrKey] == undefined)
1525                     {
1526                         dataArr[arrKey] = {
1527                             seqValue: seqValue
1528                         };
1529                     }
1530 
1531                     if (dataArr[arrKey][sumField] == undefined)
1532                     {
1533                         dataArr[arrKey][sumField] = 0;
1534                     }
1535                     if (dataArr[arrKey][countField] == undefined)
1536                     {
1537                         dataArr[arrKey][countField] = 0;
1538                     }
1539 
1540                     if (rowValue != undefined)
1541                     {
1542                         dataArr[arrKey][sumField] += rowValue;
1543                         dataArr[arrKey][countField]++;
1544                         dataArr[arrKey][fieldName] = dataArr[arrKey][sumField] / dataArr[arrKey][countField];
1545                     }
1546                 };
1547 
1548                 index = uniqueXAxisLabels.indexOf(row[config.properties.xTick]);
1549 
1550                 // calculate average values for the trend line data (used when grouping x by unique value)
1551                 addValueToTrendLineData(groupedTrendlineData, index, index, config.properties.value, row[config.properties.value], 'sum1', 'count1');
1552                 if (hasYRightMetric)
1553                 {
1554                     addValueToTrendLineData(groupedTrendlineData, index, index, config.properties.valueRight, row[config.properties.valueRight], 'sum2', 'count2');
1555                 }
1556 
1557                 // calculate average values for trend line data for each series (used when grouping x by unique value with a groupBy series property)
1558                 if (config.properties.groupBy && row[config.properties.groupBy]) {
1559                     var series = row[config.properties.groupBy];
1560                     var key = series + '|' + index;
1561 
1562                     addValueToTrendLineData(groupedTrendlineSeriesData, index, key, config.properties.value, row[config.properties.value], 'sum1', 'count1');
1563                     if (hasYRightMetric)
1564                     {
1565                         addValueToTrendLineData(groupedTrendlineSeriesData, index, key, config.properties.valueRight, row[config.properties.valueRight], 'sum2', 'count2');
1566                     }
1567 
1568                     groupedTrendlineSeriesData[key][config.properties.groupBy] = series;
1569                 }
1570             }
1571             else {
1572                 index++;
1573             }
1574 
1575             tickLabelMap[index] = row[config.properties.xTickLabel];
1576             row.seqValue = index;
1577 
1578             if (config.properties.mean && config.properties.stdDev && !meanStdDevData[index]) {
1579                 meanStdDevData[index] = row;
1580             }
1581         }
1582 
1583         // min x-axis tick length is 10 by default
1584         var maxSeqValue = config.data.length > 0 ? config.data[config.data.length - 1].seqValue + 1 : 0;
1585         for (var i = maxSeqValue; i < 10; i++)
1586         {
1587             var temp = {type: 'empty', seqValue: i};
1588             temp[config.properties.xTickLabel] = "";
1589             if (config.properties.color && config.data[0]) {
1590                 temp[config.properties.color] = config.data[0][config.properties.color];
1591             }
1592             config.data.push(temp);
1593         }
1594 
1595         // we only need the color aes if there is > 1 distinct value in the color variable
1596         if (distinctColorValues.length < 2 && config.properties.groupBy == undefined) {
1597             config.properties.color = undefined;
1598         }
1599 
1600         config.tickOverlapRotation = 35;
1601 
1602         config.scales = {
1603             color: {
1604                 scaleType: 'discrete',
1605                 range: config.properties.colorRange
1606             },
1607             x: {
1608                 scaleType: 'discrete',
1609                 tickFormat: function(index) {
1610                     // only show a max of 35 labels on the x-axis to avoid overlap
1611                     if (index % Math.ceil(config.data[config.data.length-1].seqValue / 35) == 0) {
1612                         return tickLabelMap[index];
1613                     }
1614                     else {
1615                         return "";
1616                     }
1617                 },
1618                 tickCls: function(index) {
1619                     var baseTag = 'ticklabel';
1620                     var tagIndex = config.properties.xTickTagIndex;
1621                     if (tagIndex != undefined && tagIndex == index) {
1622                         return baseTag+' xticktag';
1623                     }
1624                     return baseTag;
1625                 }
1626             },
1627             yLeft: {
1628                 scaleType: 'continuous',
1629                 domain: config.properties.yAxisDomain,
1630                 trans: config.properties.yAxisScale || 'linear',
1631                 tickFormat: function(val) {
1632                     return LABKEY.vis.isValid(val) && (val > 100000 || val < -100000) ? val.toExponential() : val;
1633                 }
1634             }
1635         };
1636 
1637         if (hasYRightMetric)
1638         {
1639             config.scales.yRight = {
1640                 scaleType: 'continuous',
1641                 domain: config.properties.yAxisDomain,
1642                 trans: config.properties.yAxisScale || 'linear',
1643                 tickFormat: function(val) {
1644                     return LABKEY.vis.isValid(val) && (val > 100000 || val < -100000) ? val.toExponential() : val;
1645                 }
1646             };
1647         }
1648 
1649         // Issue 23626: map line/point color based on legend data
1650         if (config.legendData && config.properties.color && !config.properties.colorRange)
1651         {
1652             var legendColorMap = {};
1653             for (var i = 0; i < config.legendData.length; i++)
1654             {
1655                 if (config.legendData[i].name)
1656                 {
1657                     legendColorMap[config.legendData[i].name] = config.legendData[i].color;
1658                 }
1659             }
1660 
1661             config.scales.color = {
1662                 scale: function(group) {
1663                     return legendColorMap[group];
1664                 }
1665             };
1666         }
1667 
1668         if(!config.margins) {
1669             config.margins = {};
1670         }
1671 
1672         if(!config.margins.top) {
1673             config.margins.top = config.labels && config.labels.main ? 30 : 10;
1674         }
1675 
1676         if(!config.margins.right) {
1677             config.margins.right = (config.properties.color || (config.legendData && config.legendData.length > 0) ? 190 : 40)
1678                                     + (hasYRightMetric ? 45 : 0);
1679         }
1680 
1681         if(!config.margins.bottom) {
1682             config.margins.bottom = config.labels && config.labels.x ? 75 : 55;
1683         }
1684 
1685         if(!config.margins.left) {
1686             config.margins.left = config.labels && config.labels.y ? 75 : 55;
1687         }
1688 
1689         config.aes = {
1690             x: 'seqValue'
1691         };
1692 
1693         // determine the width the error bars
1694         var barWidth = Math.max(config.width / config.data[config.data.length-1].seqValue / 5, 3);
1695 
1696         // +/- 3 standard deviation displayed using the ErrorBar geom with different colors
1697         var stdDev3Layer = new LABKEY.vis.Layer({
1698             geom: new LABKEY.vis.Geom.ErrorBar({size: 1, color: 'red', dashed: true, altColor: 'darkgrey', width: barWidth}),
1699             data: meanStdDevData,
1700             aes: {
1701                 error: function(row){return row[config.properties.stdDev] * 3;},
1702                 yLeft: config.properties.mean
1703             }
1704         });
1705         var stdDev2Layer = new LABKEY.vis.Layer({
1706             geom: new LABKEY.vis.Geom.ErrorBar({size: 1, color: 'blue', dashed: true, altColor: 'darkgrey', width: barWidth}),
1707             data: meanStdDevData,
1708             aes: {
1709                 error: function(row){return row[config.properties.stdDev] * 2;},
1710                 yLeft: config.properties.mean
1711             }
1712         });
1713         var stdDev1Layer = new LABKEY.vis.Layer({
1714             geom: new LABKEY.vis.Geom.ErrorBar({size: 1, color: 'green', dashed: true, altColor: 'darkgrey', width: barWidth}),
1715             data: meanStdDevData,
1716             aes: {
1717                 error: function(row){return row[config.properties.stdDev];},
1718                 yLeft: config.properties.mean
1719             }
1720         });
1721         var meanLayer = new LABKEY.vis.Layer({
1722             geom: new LABKEY.vis.Geom.ErrorBar({size: 1, color: 'darkgrey', width: barWidth}),
1723             data: meanStdDevData,
1724             aes: {
1725                 error: function(row){return 0;},
1726                 yLeft: config.properties.mean
1727             }
1728         });
1729 
1730         config.layers = config.properties.disableRangeDisplay ? [] : [stdDev3Layer, stdDev2Layer, stdDev1Layer, meanLayer];
1731 
1732         if (config.properties.showTrendLine)
1733         {
1734             var getPathLayerConfig = function(ySide, valueName, colorValue)
1735             {
1736                 var pathLayerConfig = {
1737                     geom: new LABKEY.vis.Geom.Path({
1738                         opacity: .6,
1739                         size: 2
1740                     }),
1741                     aes: {}
1742                 };
1743 
1744                 pathLayerConfig.aes[ySide] = valueName;
1745 
1746                 // if we aren't showing multiple series data via the group by, use the groupedTrendlineData for the path
1747                 if (config.properties.groupBy)
1748                 {
1749                     // convert the groupedTrendlineSeriesData object into an array of the object values
1750                     var seriesDataArr = [];
1751                     for(var i in groupedTrendlineSeriesData) {
1752                         if (groupedTrendlineSeriesData.hasOwnProperty(i)) {
1753                             var d = { seqValue: groupedTrendlineSeriesData[i].seqValue };
1754                             d[config.properties.groupBy] = groupedTrendlineSeriesData[i][config.properties.groupBy] + (hasYRightMetric ? '|' + valueName : '');
1755                             d[valueName] = groupedTrendlineSeriesData[i][valueName];
1756                             seriesDataArr.push(d);
1757                         }
1758                     }
1759                     pathLayerConfig.data = seriesDataArr;
1760 
1761                     pathLayerConfig.aes.pathColor = config.properties.groupBy;
1762                     pathLayerConfig.aes.group = config.properties.groupBy;
1763                 }
1764                 else
1765                 {
1766                     pathLayerConfig.data = groupedTrendlineData;
1767 
1768                     if (colorValue != undefined)
1769                     {
1770                         pathLayerConfig.aes.pathColor = function(data) {
1771                             return colorValue;
1772                         }
1773                     }
1774                 }
1775 
1776                 return pathLayerConfig;
1777             };
1778 
1779             if (hasYRightMetric)
1780             {
1781                 config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.value, 0)));
1782                 config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yRight', config.properties.valueRight, 1)));
1783             }
1784             else
1785             {
1786                 config.layers.push(new LABKEY.vis.Layer(getPathLayerConfig('yLeft', config.properties.value)));
1787             }
1788         }
1789 
1790         // points based on the data value, color and hover text can be added via params to config
1791         var getPointLayerConfig = function(ySide, valueName, colorValue)
1792         {
1793             var pointLayerConfig = {
1794                 geom: new LABKEY.vis.Geom.Point({
1795                     position: config.properties.position,
1796                     size: 3
1797                 }),
1798                 aes: {}
1799             };
1800 
1801             pointLayerConfig.aes[ySide] = valueName;
1802 
1803             if (config.properties.color) {
1804                 pointLayerConfig.aes.color = function(row) {
1805                     return row[config.properties.color] + (hasYRightMetric ? '|' + valueName : '');
1806                 };
1807             }
1808             else if (colorValue != undefined) {
1809                 pointLayerConfig.aes.color = function(row){ return colorValue; };
1810             }
1811 
1812             if (config.properties.shape) {
1813                 pointLayerConfig.aes.shape = config.properties.shape;
1814             }
1815             if (config.properties.hoverTextFn) {
1816                 pointLayerConfig.aes.hoverText = function(row) {
1817                     return config.properties.hoverTextFn.call(this, row, valueName);
1818                 };
1819             }
1820             if (config.properties.pointClickFn) {
1821                 pointLayerConfig.aes.pointClickFn = config.properties.pointClickFn;
1822             }
1823 
1824             // add some mouse over effects to highlight selected point
1825             pointLayerConfig.aes.mouseOverFn = function(event, pointData, layerSel) {
1826                 d3.select(event.srcElement).transition().duration(800).attr("stroke-width", 5).ease("elastic");
1827             };
1828             pointLayerConfig.aes.mouseOutFn = function(event, pointData, layerSel) {
1829                 d3.select(event.srcElement).transition().duration(800).attr("stroke-width", 1).ease("elastic");
1830             };
1831 
1832             return pointLayerConfig;
1833         };
1834 
1835         if (hasYRightMetric)
1836         {
1837             config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.value, 0)));
1838             config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yRight', config.properties.valueRight, 1)));
1839         }
1840         else
1841         {
1842             config.layers.push(new LABKEY.vis.Layer(getPointLayerConfig('yLeft', config.properties.value)));
1843         }
1844 
1845         return new LABKEY.vis.Plot(config);
1846     };
1847 })();
1848 
1849 /**
1850  * @name LABKEY.vis.SurvivalCurvePlot
1851  * @class SurvivalCurvePlot Wrapper to create a plot which shows survival curve step lines and censor points (based on output from R survival package).
1852  * @description This helper will take the input data and generate stepwise data points for use with the Path geom.
1853  * @param {Object} config An object that contains the following properties
1854  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
1855  * @param {Number} [config.width] The chart canvas width in pixels.
1856  * @param {Number} [config.height] The chart canvas height in pixels.
1857  * @param {Array} [config.data] The array of step data for the survival curves.
1858  * @param {String} [config.groupBy] (optional) The data array object property used to group plot lines and points.
1859  * @param {Array} [config.censorData] The array of censor data to overlay on the survival step lines.
1860  * @param {Function} [config.censorHoverText] (optional) Function defining the hover text to display for the censor data points.
1861  */
1862 (function(){
1863 
1864     LABKEY.vis.SurvivalCurvePlot = function(config){
1865 
1866         if (config.renderTo == null){
1867             throw new Error("Unable to create survival curve plot, renderTo not specified.");
1868         }
1869 
1870         if (config.data == null || config.censorData == null){
1871             throw new Error("Unable to create survival curve plot, data and/or censorData array not specified.");
1872         }
1873 
1874         if (config.aes == null || config.aes.x == null || config.aes.yLeft == null) {
1875             throw new Error("Unable to create survival curve plot, aes (x and yLeft) not specified.")
1876         }
1877 
1878         // Convert data array for step-wise line plot
1879         var stepData = [];
1880         var groupBy = config.groupBy;
1881         var aesX = config.aes.x;
1882         var aesY = config.aes.yLeft;
1883 
1884         for (var i=0; i<config.data.length; i++)
1885         {
1886             stepData.push(config.data[i]);
1887 
1888             if ( (i<config.data.length-1) && (config.data[i][groupBy] == config.data[i+1][groupBy])
1889                     && (config.data[i][aesX] != config.data[i+1][aesX])
1890                     && (config.data[i][aesY] != config.data[i+1][aesY]))
1891             {
1892                 var point = {};
1893                 point[groupBy] = config.data[i][groupBy];
1894                 point[aesX] = config.data[i+1][aesX];
1895                 point[aesY] = config.data[i][aesY];
1896                 stepData.push(point);
1897             }
1898         }
1899         config.data = stepData;
1900 
1901         config.layers = [
1902             new LABKEY.vis.Layer({
1903                 geom: new LABKEY.vis.Geom.Path({size:2, opacity:1}),
1904                 aes: {
1905                     pathColor: config.groupBy,
1906                     group: config.groupBy
1907                 }
1908             }),
1909             new LABKEY.vis.Layer({
1910                 geom: new LABKEY.vis.Geom.Point({opacity:1}),
1911                 data: config.censorData,
1912                 aes: {
1913                     color: config.groupBy,
1914                     hoverText: config.censorHoverText,
1915                     shape: config.groupBy
1916                 }
1917 
1918             })
1919         ];
1920 
1921         if (!config.scales) config.scales = {};
1922         config.scales.x = { scaleType: 'continuous', trans: 'linear' };
1923         config.scales.yLeft = { scaleType: 'continuous', trans: 'linear', domain: [0, 1] };
1924 
1925         config.aes.mouseOverFn = function(event, pointData, layerSel) {
1926             mouseOverFn(event, pointData, layerSel, config.groupBy);
1927         };
1928 
1929         config.aes.mouseOutFn = mouseOutFn;
1930 
1931         return new LABKEY.vis.Plot(config);
1932     };
1933 
1934     var mouseOverFn = function(event, pointData, layerSel, subjectColumn) {
1935         var points = layerSel.selectAll('.point path');
1936         var lines = d3.selectAll('path.line');
1937 
1938         var opacityAcc = function(d) {
1939             if (d[subjectColumn] && d[subjectColumn] == pointData[subjectColumn])
1940             {
1941                 return 1;
1942             }
1943             return .3;
1944         };
1945 
1946         points.attr('fill-opacity', opacityAcc).attr('stroke-opacity', opacityAcc);
1947         lines.attr('fill-opacity', opacityAcc).attr('stroke-opacity', opacityAcc);
1948     };
1949 
1950     var mouseOutFn = function(event, pointData, layerSel) {
1951         layerSel.selectAll('.point path').attr('fill-opacity', 1).attr('stroke-opacity', 1);
1952         d3.selectAll('path.line').attr('fill-opacity', 1).attr('stroke-opacity', 1);
1953     };
1954 })();
1955 
1956 /**
1957  * @name LABKEY.vis.TimelinePlot
1958  * @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.
1959  * @param {Object} config An object that contains the following properties
1960  * @param {String} [config.renderTo] The id of the div/span to insert the svg element into.
1961  * @param {Number} [config.width] The chart canvas width in pixels.
1962  * @param {Number} [config.height] The chart canvas height in pixels.
1963  * @param {Array} [config.data] The array of event data including event types and subtypes for the plot.
1964  * @param {String} [config.gridLinesVisible] Possible options are 'y', 'x', and 'both' to determine which sets of
1965  *                  grid lines are rendered on the plot. Default is 'both'.
1966  * @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}
1967  * @param {Date} [config.options.startDate] (Optional) The start date to use to calculate number of days until event date.
1968  * @param {Object} [config.options] (Optional) Display options as defined in {@link LABKEY.vis.Geom.TimelinePlot}.
1969  * @param {Boolean} [config.options.isCollapsed] (Optional) If true, the timeline collapses subtypes into their parent rows. Defaults to True.
1970  * @param {Number} [config.options.rowHeight] (Optional) The height of individual rows in pixels. For expanded timelines,
1971  *                  row height will resize to 75% of this value. Defaults to 1.
1972  * @param {Object} [config.options.highlight] (Optional) Special Data object containing information to highlight a specific
1973  *                  row in the timeline. Must have the same shape & properties as all other input data.
1974  * @param {String} [config.options.highlightRowColor] (Optional) Hex color to specifiy what color the highlighted row will
1975  *                  be if, found in the data. Defaults to #74B0C4.
1976  * @param {String} [config.options.activeEventKey] (Optional) Name of property that is paired with
1977  *                  @param config.options.activeEventIdentifier to identify a unique event in the data.
1978  * @param {String} [config.options.activeEventIdentifier] (Optional) Name of value that is paired with
1979  *                  @param config.options.activeEventKey to identify a unique event in the data.
1980  * @param {String} [config.options.activeEventStrokeColor] (Optional) Hex color to specifiy what color the active event
1981  *                  rect's stroke will be, if found in the data. Defaults to Red.
1982  * @param {Object} [config.options.emphasisEvents] (Optional) Object containing key:[value] pairs whose keys are property
1983  *                  names of a data object and whose value is an array of possible values that should have a highlight
1984 *                   line drawn on the chart when found. Example: {'type': ['death', 'Withdrawal']}
1985  * @param {String} [config.options.tickColor] (Optional) Hex color to specifiy the color of Axis ticks.D efaults to #DDDDDD.
1986  * @param {String} [config.options.emphasisTickColor] (Optional) Hex color to specify the color of emphasis event ticks,
1987  *                  if found in the data. Defaults to #1a969d.
1988  * @param {String} [config.options.timeUnit] (Optional) Unit of time to use when calculating how far an event's date
1989  *                  is from the start date. Default is years. Valid string values include minutes, hours, days, years, and decades.
1990  * @param {Number} [config.options.eventIconSize] (Optional) Size of event square width/height dimensions.
1991  * @param {String} [config.options.eventIconColor] (Optional) Hex color of event square stroke. Defaults to black (#0000000).
1992  * @param {String} [config.options.eventIconFill] (Optional) Hex color of event square inner fill. Defaults to black (#000000)..
1993  * @param {Float} [config.options.eventIconOpacity] (Optional) Float between 0 - 1 (inclusive) to specify how transparent the
1994  *                  fill of event icons will be. Defaults to 1.
1995  * @param {Array} [config.options.rowColorDomain] (Optional) Array of length 2 containing string Hex values for the two
1996  *                  alternating colors of timeline row rectangles. Defaults to ['#f2f2f2', '#ffffff'].
1997  */
1998 (function(){
1999 
2000     LABKEY.vis.TimelinePlot = function(config)
2001     {
2002         if (config.renderTo == undefined || config.renderTo == null) { throw new Error("Unable to create timeline plot, renderTo not specified."); }
2003 
2004         if (config.data == undefined || config.data == null) { throw new Error("Unable to create timeline plot, data array not specified."); }
2005 
2006         if (config.width == undefined || config.width == null) { throw new Error("Unable to create timeline plot, width not specified."); }
2007 
2008         if (!config.aes.y) {
2009             config.aes.y = 'key';
2010         }
2011 
2012         if (!config.options) {
2013             config.options = {};
2014         }
2015 
2016         //default x scale is in years
2017         if (!config.options.timeUnit) {
2018             config.options.timeUnit = 'years';
2019         }
2020 
2021         //set default left margin to make room for event label text
2022         if (!config.margins.left) {
2023             config.margins.left = 200
2024         }
2025 
2026         //default row height value
2027         if (!config.options.rowHeight) {
2028             config.options.rowHeight = 40;
2029         }
2030 
2031         //override default plot values if not set
2032         if (!config.margins.top) {
2033             config.margins.top = 40;
2034         }
2035         if (!config.margins.bottom) {
2036             config.margins.bottom = 50;
2037         }
2038         if (!config.gridLineWidth) {
2039             config.gridLineWidth = 1;
2040         }
2041         if (!config.gridColor) {
2042             config.gridColor = '#FFFFFF';
2043         }
2044         if (!config.borderColor) {
2045             config.borderColor = '#DDDDDD';
2046         }
2047         if (!config.tickColor) {
2048             config.tickColor = '#DDDDDD';
2049         }
2050 
2051         config.rendererType = 'd3';
2052         config.options.marginLeft = config.margins.left;
2053         config.options.parentName = config.aes.parentName;
2054         config.options.childName = config.aes.childName;
2055         config.options.dateKey = config.aes.x;
2056 
2057         config.scales = {
2058             x: {
2059                 scaleType: 'continuous'
2060             },
2061             yLeft: {
2062                 scaleType: 'discrete'
2063             }
2064         };
2065 
2066         var millis;
2067         switch(config.options.timeUnit.toLowerCase())
2068         {
2069             case 'minutes':
2070                 millis = 1000 * 60;
2071                 break;
2072             case 'hours':
2073                 millis = 1000 * 60 * 60;
2074                 break;
2075             case 'days':
2076                 millis = 1000 * 60 * 60 * 24;
2077                 break;
2078             case 'months':
2079                 millis = 1000 * 60 * 60 * 24 * 30.42;
2080                 break;
2081             case 'years':
2082                 millis = 1000 * 60 * 60 * 24 * 365;
2083                 break;
2084             case 'decades':
2085                 millis = 1000 * 60 * 60 * 24 * 365 * 10;
2086                 break;
2087             default:
2088                 millis = 1000;
2089         }
2090 
2091         //find the earliest occurring date in the data if startDate is not already specified
2092         var min = config.options.startDate ? config.options.startDate : null;
2093         if (min == null)
2094         {
2095             for (var i = 0; i < config.data.length; i++)
2096             {
2097                 config.data[i][config.aes.x] = new Date(config.data[i][config.aes.x]);
2098                 if (min == null)
2099                 {
2100                     min = config.data[i][config.aes.x];
2101                 }
2102                 min = config.data[i][config.aes.x] < min ? config.data[i][config.aes.x] : min;
2103             }
2104         }
2105 
2106         //Loop through the data and do calculations for each entry
2107         var max = 0;
2108         var parents = new Set();
2109         var children = new Set();
2110         var types = new Set();
2111         var domain = [];
2112         for (var j = 0; j < config.data.length; j++)
2113         {
2114             //calculate difference in time units
2115             var d = config.data[j];
2116             d[config.aes.x] = config.options.startDate ? new Date(d[config.aes.x]) : d[config.aes.x];
2117             var timeDifference = (d[config.aes.x] - min) / millis;
2118             d[config.options.timeUnit] = timeDifference;
2119 
2120             //update unique counts
2121             parents.add(d[config.aes.parentName]);
2122             children.add(d[config.aes.childName]);
2123 
2124             //update domain
2125             if (!config.options.isCollapsed) {
2126                 var str;
2127                 if (d[config.aes.parentName] != null && d[config.aes.parentName] != 'null' && d[config.aes.parentName] != undefined) {
2128                     str = d[config.aes.parentName];
2129                     if (!types.has(str) && str != undefined) {
2130                         domain.push(str);
2131                         types.add(str);
2132                     }
2133                     d.typeSubtype = str;
2134                 }
2135                 if (d[config.aes.childName] != null && d[config.aes.childName] != 'null' && d[config.aes.childName] != undefined) {
2136                     str += '-' + d[config.aes.childName];
2137                 }
2138                 if (!types.has(str) && str != undefined) {
2139                     domain.push(str);
2140                     types.add(str);
2141                 }
2142 
2143                 //typeSubtype will be a simple unique identifier for this type & subtype of event
2144                 d.typeSubtype = str;
2145             } else {
2146                 if (!types.has(d[config.aes.parentName])) {
2147                     domain.push(d[config.aes.parentName]);
2148                     types.add(d[config.aes.parentName]);
2149                 }
2150             }
2151 
2152             //update max value
2153             max = timeDifference > max ? timeDifference : max;
2154         }
2155         var numParentChildUniques = parents.size + children.size;
2156         var numParentUniques = parents.size;
2157         domain.sort().reverse();
2158 
2159         //For a better looking title
2160         function capitalizeFirstLetter(string) {
2161             return string.charAt(0).toUpperCase() + string.slice(1);
2162         }
2163 
2164         //Update x label to include the start date for better context
2165         config.labels.x = {value: capitalizeFirstLetter(config.options.timeUnit) + " Since " + min.toDateString()};
2166 
2167         if (!config.options.isCollapsed) {
2168             config.scales.yLeft.domain = domain;
2169             config.options.rowHeight = Math.floor(config.options.rowHeight * .75);
2170             config.height = (config.options.rowHeight) * numParentChildUniques;
2171             config.aes.typeSubtype = "typeSubtype";
2172             config.options.rowHeight = (config.height - (config.margins.top + config.margins.bottom)) / numParentChildUniques;
2173         } else {
2174             config.scales.yLeft.domain = domain;
2175             config.height = (config.options.rowHeight) * numParentUniques;
2176             config.options.rowHeight = (config.height - (config.margins.top + config.margins.bottom)) / numParentUniques;
2177         }
2178 
2179         config.scales.x.domain = [0, Math.ceil(max)];
2180         config.aes.x = config.options.timeUnit;
2181         config.layers = [
2182             new LABKEY.vis.Layer({
2183                 geom: new LABKEY.vis.Geom.TimelinePlot(config.options)
2184             })
2185         ];
2186 
2187         return new LABKEY.vis.Plot(config);
2188     };
2189 })();
2190 
2191