1 /* 2 * Copyright (c) 2013-2016 LabKey Corporation 3 * 4 * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 5 */ 6 if(!LABKEY.vis) { 7 LABKEY.vis = {}; 8 } 9 10 /** 11 * @namespace Namespace used to encapsulate functions related to creating Generic Charts (Box, Scatter, etc.). Used in the 12 * Generic Chart Wizard and when exporting Generic Charts as Scripts. 13 */ 14 LABKEY.vis.GenericChartHelper = new function(){ 15 16 var getRenderTypes = function() { 17 return [ 18 { 19 name: 'bar_chart', 20 title: 'Bar', 21 imgUrl: LABKEY.contextPath + '/visualization/images/barchart.png', 22 fields: [ 23 {name: 'x', label: 'X Categories', required: true, nonNumericOnly: true}, 24 {name: 'y', label: 'Y Axis', numericOnly: true} 25 ], 26 layoutOptions: {line: true, opacity: true, axisBased: true} 27 }, 28 { 29 name: 'box_plot', 30 title: 'Box', 31 imgUrl: LABKEY.contextPath + '/visualization/images/boxplot.png', 32 fields: [ 33 {name: 'x', label: 'X Axis Grouping'}, 34 {name: 'y', label: 'Y Axis', required: true, numericOnly: true}, 35 {name: 'color', label: 'Color', nonNumericOnly: true}, 36 {name: 'shape', label: 'Shape', nonNumericOnly: true} 37 ], 38 layoutOptions: {point: true, box: true, line: true, opacity: true, axisBased: true} 39 }, 40 { 41 name: 'pie_chart', 42 title: 'Pie', 43 imgUrl: LABKEY.contextPath + '/visualization/images/piechart.png', 44 fields: [ 45 {name: 'x', label: 'Categories', required: true, nonNumericOnly: true}, 46 {name: 'y', label: 'Measure', numericOnly: true} 47 ], 48 layoutOptions: {pie: true} 49 }, 50 { 51 name: 'scatter_plot', 52 title: 'Scatter', 53 imgUrl: LABKEY.contextPath + '/visualization/images/scatterplot.png', 54 fields: [ 55 {name: 'x', label: 'X Axis', required: true}, 56 {name: 'y', label: 'Y Axis', required: true, numericOnly: true}, 57 {name: 'color', label: 'Color', nonNumericOnly: true}, 58 {name: 'shape', label: 'Shape', nonNumericOnly: true} 59 ], 60 layoutOptions: {point: true, box: false, line: false, opacity: true, axisBased: true, binnable: true} 61 } 62 ]; 63 }; 64 65 /** 66 * Gets the chart type (i.e. box or scatter). 67 * @param {String} renderType The selected renderType, this can be SCATTER_PLOT, BOX_PLOT, or BAR_CHART. Determined 68 * at chart creation time in the Generic Chart Wizard. 69 * @param {String} xAxisType The datatype of the x-axis, i.e. String, Boolean, Number. 70 * @returns {String} 71 */ 72 var getChartType = function(renderType, xAxisType) { 73 if (renderType === "bar_chart" || renderType === "pie_chart" 74 || renderType === "box_plot" || renderType === "scatter_plot") { 75 return renderType; 76 } 77 78 if(!xAxisType) { 79 // On some charts (non-study box plots) we don't require an x-axis, instead we generate one box plot for 80 // all of the data of your y-axis. If there is no xAxisType, then we have a box plot. Scatter plots require 81 // an x-axis measure. 82 return 'box_plot'; 83 } 84 85 86 87 return (xAxisType === 'string' || xAxisType === 'boolean') ? 'box_plot' : 'scatter_plot'; 88 }; 89 90 /** 91 * Generate a default label for the selected measure for the given renderType. 92 * @param renderType 93 * @param measureName - the chart type's measure name 94 * @param properties - properties for the selected column 95 */ 96 var getDefaultLabel = function(renderType, measureName, properties) 97 { 98 var label = properties ? properties.label || properties.queryName : ''; 99 100 if ((renderType == 'bar_chart' || renderType == 'pie_chart') && measureName == 'y') 101 label = 'Sum of ' + label; 102 103 return label; 104 }; 105 106 /** 107 * Given the saved labels object we convert it to include all label types (main, x, and y). Each label type defaults 108 * to empty string (''). 109 * @param {Object} labels The saved labels object. 110 * @returns {Object} 111 */ 112 var generateLabels = function(labels) { 113 return { 114 main: { 115 value: labels.main ? labels.main : '' 116 }, 117 subtitle: { 118 value: labels.subtitle ? labels.subtitle : '' 119 }, 120 footer: { 121 value: labels.footer ? labels.footer : '' 122 }, 123 x: { 124 value: labels.x ? labels.x : '' 125 }, 126 y: { 127 value: labels.y ? labels.y : '' 128 } 129 }; 130 }; 131 132 /** 133 * Generates an object containing {@link LABKEY.vis.Scale} objects used for the chart. 134 * @param {String} chartType The chartType from getChartType. 135 * @param {Object} measures The measures from generateMeasures. 136 * @param {Object} savedScales The scales object from the saved chart config. 137 * @param {Object} aes The aesthetic map object from genereateAes. 138 * @param {Object} responseData The data from selectRows. 139 * @param {Function} defaultFormatFn used to format values for tick marks. 140 * @returns {Object} 141 */ 142 var generateScales = function(chartType, measures, savedScales, aes, responseData, defaultFormatFn) { 143 var scales = {}; 144 var data = responseData.rows; 145 var fields = responseData.metaData.fields; 146 var subjectColumn = 'ParticipantId'; 147 148 if (LABKEY.moduleContext.study && LABKEY.moduleContext.study.subject) 149 subjectColumn = LABKEY.moduleContext.study.subject.columnName; 150 151 if (chartType === "box_plot") 152 { 153 scales.x = { 154 scaleType: 'discrete', // Force discrete x-axis scale for box plots. 155 sortFn: LABKEY.vis.discreteSortFn, 156 tickLabelMax: 25 157 }; 158 159 var yMin = d3.min(data, aes.y); 160 var yMax = d3.max(data, aes.y); 161 var yPadding = ((yMax - yMin) * .1); 162 if (savedScales.y && savedScales.y.trans == "log") 163 { 164 // When subtracting padding we have to make sure we still produce valid values for a log scale. 165 // log([value less than 0]) = NaN. 166 // log(0) = -Infinity. 167 if (yMin - yPadding > 0) 168 { 169 yMin = yMin - yPadding; 170 } 171 } 172 else 173 { 174 yMin = yMin - yPadding; 175 } 176 177 scales.y = { 178 min: yMin, 179 max: yMax + yPadding, 180 scaleType: 'continuous', 181 trans: savedScales.y ? savedScales.y.trans : 'linear' 182 }; 183 } 184 else 185 { 186 var xMeasureType = _getMeasureType(measures.x); 187 if (xMeasureType == "float" || xMeasureType == "int") 188 { 189 scales.x = { 190 scaleType: 'continuous', 191 trans: savedScales.x ? savedScales.x.trans : 'linear' 192 }; 193 } else 194 { 195 scales.x = { 196 scaleType: 'discrete', 197 sortFn: LABKEY.vis.discreteSortFn, 198 tickLabelMax: 25 199 }; 200 } 201 202 scales.y = { 203 scaleType: 'continuous', 204 trans: savedScales.y ? savedScales.y.trans : 'linear' 205 }; 206 207 } 208 209 for (var i = 0; i < fields.length; i++) { 210 var type = fields[i].displayFieldJsonType ? fields[i].displayFieldJsonType : fields[i].type; 211 212 if (type == 'int' || type == 'float') { 213 if (measures.x && fields[i].name == measures.x.name) { 214 if (fields[i].extFormatFn) { 215 scales.x.tickFormat = eval(fields[i].extFormatFn); 216 } else if (defaultFormatFn) { 217 scales.x.tickFormat = defaultFormatFn; 218 } 219 } 220 221 if (measures.y && fields[i].name == measures.y.name) { 222 if (fields[i].extFormatFn) { 223 scales.y.tickFormat = eval(fields[i].extFormatFn); 224 } else if (defaultFormatFn) { 225 scales.y.tickFormat = defaultFormatFn; 226 } 227 } 228 } else if (measures.x && fields[i].name == measures.x.name && measures.x.name == subjectColumn && LABKEY.demoMode) { 229 scales.x.tickFormat = function(){return '******'}; 230 } 231 } 232 233 if (savedScales.x && (savedScales.x.min != null || savedScales.x.max != null)) { 234 scales.x.domain = [savedScales.x.min, savedScales.x.max] 235 } 236 237 if (savedScales.y && (savedScales.y.min != null || savedScales.y.max != null)) { 238 scales.y.domain = [savedScales.y.min, savedScales.y.max] 239 } 240 241 return scales; 242 }; 243 244 /** 245 * Generates the aesthetic map object needed by the visualization API to render the chart. See {@link LABKEY.vis.Plot} 246 * and {@link LABKEY.vis.Layer}. 247 * @param {String} chartType The chartType from getChartType. 248 * @param {Object} measures The measures from getMeasures. 249 * @param {String} schemaName The schemaName from the saved queryConfig. 250 * @param {String} queryName The queryName from the saved queryConfig. 251 * @returns {Object} 252 */ 253 var generateAes = function(chartType, measures, schemaName, queryName) { 254 var aes = {}, 255 xMeasureType = _getMeasureType(measures.x), 256 yMeasureType = _getMeasureType(measures.y); 257 258 if(chartType == "box_plot" && !measures.x) { 259 aes.x = generateMeasurelessAcc(queryName); 260 } else if (xMeasureType == "float" || xMeasureType == "int") { 261 aes.x = generateContinuousAcc(measures.x.name); 262 } else { 263 aes.x = generateDiscreteAcc(measures.x.name, measures.x.label); 264 } 265 266 if (measures.y) 267 { 268 if (yMeasureType == "float" || yMeasureType == "int") 269 aes.y = generateContinuousAcc(measures.y.name); 270 else 271 aes.y = generateDiscreteAcc(measures.y.name, measures.y.label); 272 } 273 274 if (chartType === "scatter_plot") { 275 aes.hoverText = generatePointHover(measures); 276 } else if (chartType === "box_plot") { 277 if (measures.color) { 278 aes.outlierColor = generateGroupingAcc(measures.color.name); 279 } 280 281 if (measures.shape) { 282 aes.outlierShape = generateGroupingAcc(measures.shape.name); 283 } 284 285 aes.hoverText = generateBoxplotHover(); 286 aes.outlierHoverText = generatePointHover(measures); 287 } 288 289 // color/shape aes are not dependent on chart type. If we have a box plot with all points enabled, then we 290 // create a second layer for points. So we'll need this no matter what. 291 if (measures.color) { 292 aes.color = generateGroupingAcc(measures.color.name); 293 } 294 295 if (measures.shape) { 296 aes.shape = generateGroupingAcc(measures.shape.name); 297 } 298 299 if (measures.pointClickFn) { 300 aes.pointClickFn = generatePointClickFn( 301 measures, 302 schemaName, 303 queryName, 304 measures.pointClickFn 305 ); 306 } 307 308 return aes; 309 }; 310 311 /** 312 * Generates a function that returns the text used for point hovers. 313 * @param {Object} measures The measures object from the saved chart config. 314 * @returns {Function} 315 */ 316 var generatePointHover = function(measures){ 317 return function(row) { 318 var hover; 319 320 if(measures.x) { 321 hover = measures.x.label + ': '; 322 323 if(row[measures.x.name].displayValue){ 324 hover = hover + row[measures.x.name].displayValue; 325 } else { 326 hover = hover + row[measures.x.name].value; 327 } 328 } 329 330 hover = hover + ', \n' + measures.y.label + ': ' + row[measures.y.name].value; 331 332 if(measures.color){ 333 hover = hover + ', \n' + measures.color.label + ': '; 334 if(row[measures.color.name]){ 335 if(row[measures.color.name].displayValue){ 336 hover = hover + row[measures.color.name].displayValue; 337 } else { 338 hover = hover + row[measures.color.name].value; 339 } 340 } 341 } 342 343 if(measures.shape && !(measures.color && measures.color.name == measures.shape.name)){ 344 hover = hover + ', \n' + measures.shape.label + ': '; 345 if(row[measures.shape.name]){ 346 if(row[measures.shape.name].displayValue){ 347 hover = hover + row[measures.shape.name].displayValue; 348 } else { 349 hover = hover + row[measures.shape.name].value; 350 } 351 } 352 } 353 return hover; 354 }; 355 }; 356 357 /** 358 * Returns a function used to generate the hover text for box plots. 359 * @returns {Function} 360 */ 361 var generateBoxplotHover = function() { 362 return function(xValue, stats) { 363 return xValue + ':\nMin: ' + stats.min + '\nMax: ' + stats.max + '\nQ1: ' + stats.Q1 + '\nQ2: ' + stats.Q2 + 364 '\nQ3: ' + stats.Q3; 365 }; 366 }; 367 368 /** 369 * Generates an accessor function that returns a discrete value from a row of data for a given measure and label. 370 * Used when an axis has a discrete measure (i.e. string). 371 * @param {String} measureName The name of the measure. 372 * @param {String} measureLabel The label of the measure. 373 * @returns {Function} 374 */ 375 var generateDiscreteAcc = function(measureName, measureLabel) { 376 return function(row){ 377 var valueObj = row[measureName]; 378 var value = null; 379 380 if(valueObj){ 381 value = valueObj.displayValue ? valueObj.displayValue : valueObj.value; 382 } else { 383 return undefined; 384 } 385 386 if(value === null){ 387 value = "Not in " + measureLabel; 388 } 389 390 return value; 391 }; 392 }; 393 394 /** 395 * Generates an accessor function that returns a value from a row of data for a given measure. 396 * @param {String} measureName The name of the measure. 397 * @returns {Function} 398 */ 399 var generateContinuousAcc = function(measureName){ 400 return function(row){ 401 var value = null; 402 403 if(row[measureName]){ 404 value = row[measureName].value; 405 406 if(Math.abs(value) === Infinity){ 407 value = null; 408 } 409 410 if(value === false || value === true){ 411 value = value.toString(); 412 } 413 414 return value; 415 } else { 416 return undefined; 417 } 418 } 419 }; 420 421 /** 422 * Generates an accesssor function for shape and color measures. 423 * @param {String} measureName The name of the measure. 424 * @returns {Function} 425 */ 426 var generateGroupingAcc = function(measureName){ 427 return function(row) { 428 var measureObj = row[measureName]; 429 var value = null; 430 431 if(measureObj){ 432 value = measureObj.displayValue ? measureObj.displayValue : measureObj.value; 433 } 434 435 if(value === null || value === undefined){ 436 value = "n/a"; 437 } 438 439 return value; 440 }; 441 }; 442 443 /** 444 * Generates an accessor for boxplots that do not have an x-axis measure. Generally the measureName passed in is the 445 * queryName. 446 * @param {String} measureName The name of the measure. In this case it is generally the query name. 447 * @returns {Function} 448 */ 449 var generateMeasurelessAcc = function(measureName) { 450 // Used for boxplots that do not have an x-axis measure. Instead we just return the 451 // queryName for every row. 452 return function(row) { 453 return measureName; 454 } 455 }; 456 457 /** 458 * Generates the function to be executed when a user clicks a point. 459 * @param {Object} measures The measures from the saved chart config. 460 * @param {String} schemaName The schema name from the saved query config. 461 * @param {String} queryName The query name from the saved query config. 462 * @param {String} fnString The string value of the user-provided function to be executed when a point is clicked. 463 * @returns {Function} 464 */ 465 var generatePointClickFn = function(measures, schemaName, queryName, fnString){ 466 var measureInfo = { 467 schemaName: schemaName, 468 queryName: queryName 469 }; 470 471 if (measures.y) 472 measureInfo.yAxis = measures.y.name; 473 if (measures.x) 474 measureInfo.xAxis = measures.x.name; 475 if (measures.shape) 476 measureInfo.shapeName = measures.shape.name; 477 if (measures.color) 478 measureInfo.pointName = measures.color.name; 479 480 // using new Function is quicker than eval(), even in IE. 481 var pointClickFn = new Function('return ' + fnString)(); 482 return function(clickEvent, data){ 483 pointClickFn(data, measureInfo, clickEvent); 484 }; 485 }; 486 487 /** 488 * Generates the Point Geom used for scatter plots and box plots with all points visible. 489 * @param {Object} chartOptions The saved chartOptions object from the chart config. 490 * @returns {LABKEY.vis.Geom.Point} 491 */ 492 var generatePointGeom = function(chartOptions){ 493 return new LABKEY.vis.Geom.Point({ 494 opacity: chartOptions.opacity, 495 size: chartOptions.pointSize, 496 color: '#' + chartOptions.pointFillColor, 497 position: chartOptions.position 498 }); 499 }; 500 501 /** 502 * Generates the Boxplot Geom used for box plots. 503 * @param {Object} chartOptions The saved chartOptions object from the chart config. 504 * @returns {LABKEY.vis.Geom.Boxplot} 505 */ 506 var generateBoxplotGeom = function(chartOptions){ 507 return new LABKEY.vis.Geom.Boxplot({ 508 lineWidth: chartOptions.lineWidth, 509 outlierOpacity: chartOptions.opacity, 510 outlierFill: '#' + chartOptions.pointFillColor, 511 outlierSize: chartOptions.pointSize, 512 color: '#' + chartOptions.lineColor, 513 fill: chartOptions.boxFillColor == 'none' ? chartOptions.boxFillColor : '#' + chartOptions.boxFillColor, 514 position: chartOptions.position, 515 showOutliers: chartOptions.showOutliers 516 }); 517 }; 518 519 /** 520 * Generates the Barplot Geom used for bar charts. 521 * @param {Object} chartOptions The saved chartOptions object from the chart config. 522 * @returns {LABKEY.vis.Geom.BarPlot} 523 */ 524 var generateBarGeom = function(chartOptions){ 525 return new LABKEY.vis.Geom.BarPlot({ 526 opacity: chartOptions.opacity, 527 color: '#' + chartOptions.lineColor, 528 fill: '#' + chartOptions.boxFillColor, 529 lineWidth: chartOptions.lineWidth 530 }); 531 }; 532 533 /** 534 * Generates the Bin Geom used to bin a set of points. 535 * @param {Object} chartOptions The saved chartOptions object from the chart config. 536 * @returns {LABKEY.vis.Geom.Bin} 537 */ 538 var generateBinGeom = function(chartOptions) { 539 var colorRange = ["#e6e6e6", "#085D90"]; //light-gray and labkey blue is default 540 if (chartOptions.binColorGroup == 'SingleColor') { 541 var color = '#' + chartOptions.binSingleColor; 542 colorRange = ["#FFFFFF", color]; 543 } 544 else if (chartOptions.binColorGroup == 'Heat') { 545 colorRange = ["#fff6bc", "#e23202"]; 546 } 547 548 return new LABKEY.vis.Geom.Bin({ 549 shape: chartOptions.binShape, 550 colorRange: colorRange, 551 size: chartOptions.binShape == 'square' ? 10 : 5 552 }) 553 }; 554 555 /** 556 * Generates a Geom based on the chartType. 557 * @param {String} chartType The chart type from getChartType. 558 * @param {Object} chartOptions The chartOptions object from the saved chart config. 559 * @returns {LABKEY.vis.Geom} 560 */ 561 var generateGeom = function(chartType, chartOptions) { 562 if (chartType == "box_plot") 563 return generateBoxplotGeom(chartOptions); 564 else if (chartType == "scatter_plot") 565 return chartOptions.binned ? generateBinGeom(chartOptions) : generatePointGeom(chartOptions); 566 else if (chartType == "bar_chart") 567 return generateBarGeom(chartOptions); 568 }; 569 570 /** 571 * 572 * @param {Array} data The response data from selectRows. 573 * @param {String} dimensionName The grouping variable to get distinct members from. 574 * @param {String} measureName The variable to calculate aggregate values over. Nullable. 575 * @param {String} aggregate MIN/MAX/SUM/COUNT/etc. Defaults to COUNT. 576 * @param {String} nullDisplayValue The display value to use for null dimension values. Defaults to 'null'. 577 */ 578 var generateAggregateData = function(data, dimensionName, measureName, aggregate, nullDisplayValue) 579 { 580 var uniqueDimValues = {}; 581 for (var i = 0; i < data.length; i++) 582 { 583 var dimVal = null; 584 if (typeof data[i][dimensionName] == 'object') 585 dimVal = data[i][dimensionName].hasOwnProperty('displayValue') ? data[i][dimensionName].displayValue : data[i][dimensionName].value; 586 587 var measureVal = null; 588 if (measureName != undefined && measureName != null && typeof data[i][measureName] == 'object') 589 measureVal = data[i][measureName].value; 590 591 if (uniqueDimValues[dimVal] == undefined) 592 uniqueDimValues[dimVal] = {count: 0, sum: 0}; 593 594 uniqueDimValues[dimVal].count++; 595 if (!isNaN(measureVal)) 596 uniqueDimValues[dimVal].sum += measureVal; 597 } 598 599 var keys = Object.keys(uniqueDimValues), results = []; 600 for (var i = 0; i < keys.length; i++) 601 { 602 var row = { 603 label: keys[i] == null || keys[i] == 'null' ? nullDisplayValue || 'null' : keys[i] 604 }; 605 606 // TODO add support for more aggregates 607 if (aggregate == undefined || aggregate == null || aggregate == 'COUNT') 608 row.value = uniqueDimValues[keys[i]].count; 609 else if (aggregate == 'SUM') 610 row.value = uniqueDimValues[keys[i]].sum; 611 else 612 throw 'Aggregate ' + aggregate + ' is not yet supported.'; 613 614 results.push(row); 615 } 616 return results; 617 }; 618 619 /** 620 * Generate the plot config for the given chart renderType and config options. 621 * @param renderTo 622 * @param chartConfig 623 * @param labels 624 * @param aes 625 * @param scales 626 * @param data 627 * @returns {Object} 628 */ 629 var generatePlotConfig = function(renderTo, chartConfig, labels, aes, scales, geom, data) 630 { 631 var renderType = chartConfig.renderType, 632 layers = [], clipRect, 633 plotConfig = { 634 renderTo: renderTo, 635 rendererType: 'd3', 636 width: chartConfig.width, 637 height: chartConfig.height 638 }; 639 640 if (renderType == 'pie_chart') 641 return _generatePieChartConfig(plotConfig, chartConfig, labels, data); 642 643 clipRect = (scales.x && Ext4.isArray(scales.x.domain)) || (scales.y && Ext4.isArray(scales.y.domain)); 644 645 if (renderType == 'bar_chart') 646 { 647 aes = { x: 'label', y: 'value' }; 648 649 if (scales.y.domain) { 650 scales.y = { domain: scales.y.domain }; 651 } else { 652 var values = Ext4.Array.pluck(data, 'value'), 653 min = Math.min(0, Ext4.Array.min(values)), 654 max = Math.max(0, Ext4.Array.max(values)); 655 scales.y = { domain: [min, max] }; 656 } 657 } 658 else if (renderType == 'box_plot' && chartConfig.pointType == 'all') 659 { 660 layers.push( 661 new LABKEY.vis.Layer({ 662 data: data, 663 geom: LABKEY.vis.GenericChartHelper.generatePointGeom(chartConfig.geomOptions), 664 aes: {hoverText: LABKEY.vis.GenericChartHelper.generatePointHover(chartConfig.measures)} 665 }) 666 ); 667 } 668 669 layers.push( 670 new LABKEY.vis.Layer({ 671 data: data, 672 geom: geom 673 }) 674 ); 675 676 plotConfig = Ext4.apply(plotConfig, { 677 clipRect: clipRect, 678 data: data, 679 labels: labels, 680 aes: aes, 681 scales: scales, 682 layers: layers 683 }); 684 685 return plotConfig; 686 }; 687 688 var _generatePieChartConfig = function(baseConfig, chartConfig, labels, data) 689 { 690 return Ext4.apply(baseConfig, { 691 data: data, 692 header: { 693 title: { text: labels.main.value }, 694 subtitle: { text: labels.subtitle.value }, 695 titleSubtitlePadding: 1 696 }, 697 footer: { 698 text: labels.footer.value, 699 location: 'bottom-center' 700 }, 701 labels: { 702 mainLabel: { fontSize: 14 }, 703 percentage: { 704 fontSize: 14, 705 color: chartConfig.geomOptions.piePercentagesColor != null ? '#' + chartConfig.geomOptions.piePercentagesColor : undefined 706 }, 707 outer: { pieDistance: 20 }, 708 inner: { 709 format: chartConfig.geomOptions.showPiePercentages ? 'percentage' : 'none', 710 hideWhenLessThanPercentage: chartConfig.geomOptions.pieHideWhenLessThanPercentage 711 } 712 }, 713 size: { 714 pieInnerRadius: chartConfig.geomOptions.pieInnerRadius + '%', 715 pieOuterRadius: chartConfig.geomOptions.pieOuterRadius + '%' 716 }, 717 misc: { 718 gradient: { 719 enabled: chartConfig.geomOptions.gradientPercentage != 0, 720 percentage: chartConfig.geomOptions.gradientPercentage, 721 color: '#' + chartConfig.geomOptions.gradientColor 722 }, 723 colors: { 724 segments: LABKEY.vis.Scale[chartConfig.geomOptions.colorPaletteScale]() 725 } 726 }, 727 effects: { highlightSegmentOnMouseover: false }, 728 tooltips: { enabled: true } 729 }); 730 }; 731 732 /** 733 * Check if the selectRows API response has data. Return an error string if no data exists. 734 * @param response 735 * @param includeFilterMsg true to include a message about removing filters 736 * @returns {String} 737 */ 738 var validateResponseHasData = function(response, includeFilterMsg) 739 { 740 if (!response || !response.rows || response.rows.length == 0) 741 { 742 return 'The response returned 0 rows of data. The query may be empty or the applied filters may be too strict.' 743 + (includeFilterMsg ? 'Try removing or adjusting any filters if possible.' : ''); 744 } 745 746 return null; 747 }; 748 749 /** 750 * Verifies that the axis measure is actually present and has data. Also checks to make sure that data can be used in a log 751 * scale (if applicable). Returns an object with a success parameter (boolean) and a message parameter (string). If the 752 * success parameter is false there is a critical error and the chart cannot be rendered. If success is true the chart 753 * can be rendered. Message will contain an error or warning message if applicable. If message is not null and success 754 * is true, there is a warning. 755 * @param {String} chartType The chartType from getChartType. 756 * @param {Object} chartConfig The saved chartConfig object. 757 * @param {String} measureName The name of the axis measure property. 758 * @param {Object} aes The aes object from generateAes. 759 * @param {Object} scales The scales object from generateScales. 760 * @param {Array} data The response data from selectRows. 761 * @returns {Object} 762 */ 763 var validateAxisMeasure = function(chartType, chartConfig, measureName, aes, scales, data){ 764 765 var dataIsNull = true, measureUndefined = true, invalidLogValues = false, hasZeroes = false, message = null; 766 767 for (var i = 0; i < data.length; i ++) 768 { 769 var value = aes[measureName](data[i]); 770 771 if (value !== undefined) 772 measureUndefined = false; 773 774 if (value !== null) 775 dataIsNull = false; 776 777 if (value && value < 0) 778 invalidLogValues = true; 779 780 if (value === 0 ) 781 hasZeroes = true; 782 } 783 784 if (measureUndefined) 785 { 786 message = 'The measure ' + chartConfig.measures[measureName].label + ' was not found. It may have been renamed or removed.'; 787 return {success: false, message: message}; 788 } 789 790 if ((chartType == 'scatter_plot' || measureName == 'y') && dataIsNull) 791 { 792 message = 'All data values for ' + chartConfig.measures[measureName].label + ' are null. Please choose a different measure.'; 793 return {success: false, message: message}; 794 } 795 796 if (scales[measureName] && scales[measureName].trans == "log") 797 { 798 if (invalidLogValues) 799 { 800 message = "Unable to use a log scale on the y-axis. All y-axis values must be >= 0. Reverting to linear scale on y-axis."; 801 scales[measureName].trans = 'linear'; 802 } 803 else if (hasZeroes) 804 { 805 message = "Some " + measureName + "-axis values are 0. Plotting all " + measureName + "-axis values as value+1."; 806 var accFn = aes[measureName]; 807 aes[measureName] = function(row){return accFn(row) + 1}; 808 } 809 } 810 811 return {success: true, message: message}; 812 }; 813 814 /** 815 * Deprecated - use validateAxisMeasure 816 */ 817 var validateXAxis = function(chartType, chartConfig, aes, scales, data){ 818 return this.validateAxisMeasure(chartType, chartConfig, 'x', aes, scales, data); 819 }; 820 /** 821 * Deprecated - use validateAxisMeasure 822 */ 823 var validateYAxis = function(chartType, chartConfig, aes, scales, data){ 824 return this.validateAxisMeasure(chartType, chartConfig, 'y', aes, scales, data); 825 }; 826 827 var _getMeasureType = function(measure) { 828 return measure ? (measure.normalizedType || measure.type) : null; 829 }; 830 831 return { 832 // NOTE: the @function below is needed or JSDoc will not include the documentation for loadVisDependencies. Don't 833 // ask me why, I do not know. 834 /** 835 * @function 836 */ 837 getRenderTypes: getRenderTypes, 838 getMeasureType: _getMeasureType, 839 getChartType: getChartType, 840 getDefaultLabel: getDefaultLabel, 841 generateLabels: generateLabels, 842 generateScales: generateScales, 843 generateAes: generateAes, 844 generatePointHover: generatePointHover, 845 generateBoxplotHover: generateBoxplotHover, 846 generateDiscreteAcc: generateDiscreteAcc, 847 generateContinuousAcc: generateContinuousAcc, 848 generateGroupingAcc: generateGroupingAcc, 849 generatePointClickFn: generatePointClickFn, 850 generateGeom: generateGeom, 851 generateBoxplotGeom: generateBoxplotGeom, 852 generatePointGeom: generatePointGeom, 853 generateAggregateData: generateAggregateData, 854 generatePlotConfig: generatePlotConfig, 855 validateResponseHasData: validateResponseHasData, 856 validateAxisMeasure: validateAxisMeasure, 857 validateXAxis: validateXAxis, 858 validateYAxis: validateYAxis, 859 /** 860 * Loads all of the required dependencies for a Generic Chart. 861 * @param {Function} callback The callback to be executed when all of the visualization dependencies have been loaded. 862 * @param {Object} scope The scope to be used when executing the callback. 863 */ 864 loadVisDependencies: LABKEY.requiresVisualization 865 }; 866 };