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