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