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