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