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