1 /* 2 * Copyright (c) 2013-2017 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 Axis Categories', required: true, nonNumericOnly: true}, 24 {name: 'xSub', label: 'Split Categories By', required: false, nonNumericOnly: true}, 25 {name: 'y', label: 'Y Axis', numericOnly: true} 26 ], 27 layoutOptions: {line: true, opacity: true, axisBased: true} 28 }, 29 { 30 name: 'box_plot', 31 title: 'Box', 32 imgUrl: LABKEY.contextPath + '/visualization/images/boxplot.png', 33 fields: [ 34 {name: 'x', label: 'X Axis Categories'}, 35 {name: 'y', label: 'Y Axis', required: true, numericOnly: true}, 36 {name: 'color', label: 'Color', nonNumericOnly: true}, 37 {name: 'shape', label: 'Shape', nonNumericOnly: true} 38 ], 39 layoutOptions: {point: true, box: true, line: true, opacity: true, axisBased: true} 40 }, 41 { 42 name: 'line_plot', 43 title: 'Line', 44 imgUrl: LABKEY.contextPath + '/visualization/images/timechart.png', 45 fields: [ 46 {name: 'x', label: 'X Axis', required: true, numericOrDateOnly: true}, 47 {name: 'y', label: 'Y Axis', required: true, numericOnly: true}, 48 {name: 'series', label: 'Series', nonNumericOnly: true} 49 ], 50 layoutOptions: {opacity: true, axisBased: true, series: true} 51 }, 52 { 53 name: 'pie_chart', 54 title: 'Pie', 55 imgUrl: LABKEY.contextPath + '/visualization/images/piechart.png', 56 fields: [ 57 {name: 'x', label: 'Categories', required: true, nonNumericOnly: true}, 58 // Issue #29046 'Remove "measure" option from pie chart' 59 // {name: 'y', label: 'Measure', numericOnly: true} 60 ], 61 layoutOptions: {pie: true} 62 }, 63 { 64 name: 'scatter_plot', 65 title: 'Scatter', 66 imgUrl: LABKEY.contextPath + '/visualization/images/scatterplot.png', 67 fields: [ 68 {name: 'x', label: 'X Axis', required: true}, 69 {name: 'y', label: 'Y Axis', required: true, numericOnly: true}, 70 {name: 'color', label: 'Color', nonNumericOnly: true}, 71 {name: 'shape', label: 'Shape', nonNumericOnly: true} 72 ], 73 layoutOptions: {point: true, opacity: true, axisBased: true, binnable: true} 74 }, 75 { 76 name: 'time_chart', 77 title: 'Time', 78 hidden: _getStudyTimepointType() == null, 79 imgUrl: LABKEY.contextPath + '/visualization/images/timechart.png', 80 fields: [ 81 {name: 'x', label: 'X Axis', required: true, altSelectionOnly: true, altFieldType: 'LABKEY.vis.TimeChartXAxisField'}, 82 {name: 'y', label: 'Y Axis', required: true, numericOnly: true, allowMultiple: true} 83 ], 84 layoutOptions: {time: true, axisBased: true} 85 } 86 ]; 87 }; 88 89 /** 90 * Gets the chart type (i.e. box or scatter). 91 * @param {String} renderType The selected renderType, this can be SCATTER_PLOT, BOX_PLOT, or BAR_CHART. Determined 92 * at chart creation time in the Generic Chart Wizard. 93 * @param {String} xAxisType The datatype of the x-axis, i.e. String, Boolean, Number. 94 * @returns {String} 95 */ 96 var getChartType = function(renderType, xAxisType) 97 { 98 if (renderType === 'time_chart' || renderType === "bar_chart" || renderType === "pie_chart" 99 || renderType === "box_plot" || renderType === "scatter_plot" || renderType === "line_plot") 100 { 101 return renderType; 102 } 103 104 if (!xAxisType) 105 { 106 // On some charts (non-study box plots) we don't require an x-axis, instead we generate one box plot for 107 // all of the data of your y-axis. If there is no xAxisType, then we have a box plot. Scatter plots require 108 // an x-axis measure. 109 return 'box_plot'; 110 } 111 112 return (xAxisType === 'string' || xAxisType === 'boolean') ? 'box_plot' : 'scatter_plot'; 113 }; 114 115 /** 116 * Generate a default label for the selected measure for the given renderType. 117 * @param renderType 118 * @param measureName - the chart type's measure name 119 * @param properties - properties for the selected column 120 */ 121 var getSelectedMeasureLabel = function(renderType, measureName, properties) 122 { 123 var label = properties ? properties.label || properties.queryName : ''; 124 125 if (label != '' && measureName == 'y' && (renderType == 'bar_chart' || renderType == 'pie_chart')) { 126 if (Ext4.isDefined(properties.aggregate)) { 127 var aggLabel = Ext4.isObject(properties.aggregate) ? properties.aggregate.name 128 : Ext4.String.capitalize(properties.aggregate.toLowerCase()); 129 label = aggLabel + ' of ' + label; 130 } 131 else { 132 label = 'Sum of ' + label; 133 } 134 } 135 136 return label; 137 }; 138 139 /** 140 * Generate a plot title based on the selected measures array or object. 141 * @param renderType 142 * @param measures 143 * @returns {string} 144 */ 145 var getTitleFromMeasures = function(renderType, measures) 146 { 147 var queryLabels = []; 148 149 if (Ext4.isObject(measures)) 150 { 151 if (Ext4.isArray(measures.y)) 152 { 153 Ext4.each(measures.y, function(m) 154 { 155 var measureQueryLabel = m.queryLabel || m.queryName; 156 if (queryLabels.indexOf(measureQueryLabel) == -1) 157 queryLabels.push(measureQueryLabel); 158 }); 159 } 160 else 161 { 162 var m = measures.x || measures.y; 163 queryLabels.push(m.queryLabel || m.queryName); 164 } 165 } 166 167 return queryLabels.join(', '); 168 }; 169 170 /** 171 * Get the sorted set of column metadata for the given schema/query/view. 172 * @param queryConfig 173 * @param successCallback 174 * @param callbackScope 175 */ 176 var getQueryColumns = function(queryConfig, successCallback, callbackScope) 177 { 178 LABKEY.Ajax.request({ 179 url: LABKEY.ActionURL.buildURL('visualization', 'getGenericReportColumns.api'), 180 method: 'GET', 181 params: { 182 schemaName: queryConfig.schemaName, 183 queryName: queryConfig.queryName, 184 viewName: queryConfig.viewName, 185 dataRegionName: queryConfig.dataRegionName, 186 includeCohort: true, 187 includeParticipantCategory : true 188 }, 189 success : function(response){ 190 var columnList = LABKEY.Utils.decode(response.responseText); 191 _queryColumnMetadata(queryConfig, columnList, successCallback, callbackScope) 192 }, 193 scope : this 194 }); 195 }; 196 197 var _queryColumnMetadata = function(queryConfig, columnList, successCallback, callbackScope) 198 { 199 LABKEY.Query.selectRows({ 200 maxRows: 0, // use maxRows 0 so that we just get the query metadata 201 schemaName: queryConfig.schemaName, 202 queryName: queryConfig.queryName, 203 viewName: queryConfig.viewName, 204 parameters: queryConfig.parameters, 205 requiredVersion: 9.1, 206 columns: columnList.columns.all, 207 method: 'POST', // Issue 31744: use POST as the columns list can be very long and cause a 400 error 208 success: function(response){ 209 var columnMetadata = _updateAndSortQueryFields(queryConfig, columnList, response.metaData.fields); 210 successCallback.call(callbackScope, columnMetadata); 211 }, 212 failure : function(response) { 213 // this likely means that the query no longer exists 214 successCallback.call(callbackScope, columnList, []); 215 }, 216 scope : this 217 }); 218 }; 219 220 var _updateAndSortQueryFields = function(queryConfig, columnList, columnMetadata) 221 { 222 var queryFields = [], 223 queryFieldKeys = [], 224 columnTypes = Ext4.isDefined(columnList.columns) ? columnList.columns : {}; 225 226 Ext4.each(columnMetadata, function(column) 227 { 228 var f = Ext4.clone(column); 229 f.schemaName = queryConfig.schemaName; 230 f.queryName = queryConfig.queryName; 231 f.isCohortColumn = false; 232 f.isSubjectGroupColumn = false; 233 234 // issue 23224: distinguish cohort and subject group fields in the list of query columns 235 if (columnTypes['cohort'] && columnTypes['cohort'].indexOf(f.fieldKey) > -1) 236 { 237 f.shortCaption = 'Study: ' + f.shortCaption; 238 f.isCohortColumn = true; 239 } 240 else if (columnTypes['subjectGroup'] && columnTypes['subjectGroup'].indexOf(f.fieldKey) > -1) 241 { 242 f.shortCaption = columnList.subject.nounSingular + ' Group: ' + f.shortCaption; 243 f.isSubjectGroupColumn = true; 244 } 245 246 // Issue 31672: keep track of the distinct query field keys so we don't get duplicates 247 if (f.fieldKey.toLowerCase() != 'lsid' && queryFieldKeys.indexOf(f.fieldKey) == -1) { 248 queryFields.push(f); 249 queryFieldKeys.push(f.fieldKey); 250 } 251 }, this); 252 253 // Sorts fields by their shortCaption, but put subject groups/categories/cohort at the end. 254 queryFields.sort(function(a, b) 255 { 256 if (a.isSubjectGroupColumn != b.isSubjectGroupColumn) 257 return a.isSubjectGroupColumn ? 1 : -1; 258 else if (a.isCohortColumn != b.isCohortColumn) 259 return a.isCohortColumn ? 1 : -1; 260 else if (a.shortCaption != b.shortCaption) 261 return a.shortCaption < b.shortCaption ? -1 : 1; 262 263 return 0; 264 }); 265 266 return queryFields; 267 }; 268 269 /** 270 * Determine a reasonable width for the chart based on the chart type and selected measures / data. 271 * @param chartType 272 * @param measures 273 * @param measureStore 274 * @param defaultWidth 275 * @returns {int} 276 */ 277 var getChartTypeBasedWidth = function(chartType, measures, measureStore, defaultWidth) { 278 var width = defaultWidth; 279 280 if (chartType == 'bar_chart' && Ext4.isObject(measures.x)) { 281 // 15px per bar + 15px between bars + 300 for default margins 282 var xBarCount = measureStore.members(measures.x.name).length; 283 width = Math.max((xBarCount * 15 * 2) + 300, defaultWidth); 284 285 if (Ext4.isObject(measures.xSub)) { 286 // 15px per bar per group + 200px between groups + 600 for default margins 287 var xSubCount = measureStore.members(measures.xSub.name).length; 288 width = (xBarCount * xSubCount * 15) + (xSubCount * 200) + 600; 289 } 290 } 291 else if (chartType == 'box_plot' && Ext4.isObject(measures.x)) { 292 // 20px per box + 20px between boxes + 300 for default margins 293 var xBoxCount = measureStore.members(measures.x.name).length; 294 width = Math.max((xBoxCount * 20 * 2) + 300, defaultWidth); 295 } 296 297 return width; 298 }; 299 300 /** 301 * Given the saved labels object we convert it to include all label types (main, x, and y). Each label type defaults 302 * to empty string (''). 303 * @param {Object} labels The saved labels object. 304 * @returns {Object} 305 */ 306 var generateLabels = function(labels) { 307 return { 308 main: { 309 value: labels.main ? labels.main : '' 310 }, 311 subtitle: { 312 value: labels.subtitle ? labels.subtitle : '' 313 }, 314 footer: { 315 value: labels.footer ? labels.footer : '' 316 }, 317 x: { 318 value: labels.x ? labels.x : '' 319 }, 320 y: { 321 value: labels.y ? labels.y : '' 322 } 323 }; 324 }; 325 326 /** 327 * Generates an object containing {@link LABKEY.vis.Scale} objects used for the chart. 328 * @param {String} chartType The chartType from getChartType. 329 * @param {Object} measures The measures from generateMeasures. 330 * @param {Object} savedScales The scales object from the saved chart config. 331 * @param {Object} aes The aesthetic map object from genereateAes. 332 * @param {Object} measureStore The MeasureStore data using a selectRows API call. 333 * @param {Function} defaultFormatFn used to format values for tick marks. 334 * @returns {Object} 335 */ 336 var generateScales = function(chartType, measures, savedScales, aes, measureStore, defaultFormatFn) { 337 var scales = {}; 338 var data = Ext4.isArray(measureStore.rows) ? measureStore.rows : measureStore.records(); 339 var fields = Ext4.isObject(measureStore.metaData) ? measureStore.metaData.fields : measureStore.getResponseMetadata().fields; 340 var subjectColumn = _getStudySubjectInfo().columnName; 341 var valExponentialDigits = 6; 342 343 if (chartType === "box_plot") 344 { 345 scales.x = { 346 scaleType: 'discrete', // Force discrete x-axis scale for box plots. 347 sortFn: LABKEY.vis.discreteSortFn, 348 tickLabelMax: 25 349 }; 350 351 var yMin = d3.min(data, aes.y); 352 var yMax = d3.max(data, aes.y); 353 var yPadding = ((yMax - yMin) * .1); 354 if (savedScales.y && savedScales.y.trans == "log") 355 { 356 // When subtracting padding we have to make sure we still produce valid values for a log scale. 357 // log([value less than 0]) = NaN. 358 // log(0) = -Infinity. 359 if (yMin - yPadding > 0) 360 { 361 yMin = yMin - yPadding; 362 } 363 } 364 else 365 { 366 yMin = yMin - yPadding; 367 } 368 369 scales.y = { 370 min: yMin, 371 max: yMax + yPadding, 372 scaleType: 'continuous', 373 trans: savedScales.y ? savedScales.y.trans : 'linear' 374 }; 375 } 376 else 377 { 378 var xMeasureType = getMeasureType(measures.x); 379 380 // Force discrete x-axis scale for bar plots. 381 var useContinuousScale = chartType != 'bar_chart' && isNumericType(xMeasureType); 382 383 if (useContinuousScale) 384 { 385 scales.x = { 386 scaleType: 'continuous', 387 trans: savedScales.x ? savedScales.x.trans : 'linear' 388 }; 389 } 390 else 391 { 392 scales.x = { 393 scaleType: 'discrete', 394 sortFn: LABKEY.vis.discreteSortFn, 395 tickLabelMax: 25 396 }; 397 398 //bar chart x-axis subcategories support 399 if (Ext4.isDefined(measures.xSub)) { 400 scales.xSub = { 401 scaleType: 'discrete', 402 sortFn: LABKEY.vis.discreteSortFn, 403 tickLabelMax: 25 404 }; 405 } 406 } 407 408 scales.y = { 409 scaleType: 'continuous', 410 trans: savedScales.y ? savedScales.y.trans : 'linear' 411 }; 412 } 413 414 // if we have no data, show a default y-axis domain 415 if (scales.x && data.length == 0 && scales.x.scaleType == 'continuous') 416 scales.x.domain = [0,1]; 417 if (scales.y && data.length == 0) 418 scales.y.domain = [0,1]; 419 420 for (var i = 0; i < fields.length; i++) { 421 var type = fields[i].displayFieldJsonType ? fields[i].displayFieldJsonType : fields[i].type; 422 var isMeasureXMatch = measures.x && (fields[i].fieldKey == measures.x.name || fields[i].fieldKey == measures.x.fieldKey); 423 var isMeasureYMatch = measures.y && (fields[i].fieldKey == measures.y.name || fields[i].fieldKey == measures.y.fieldKey); 424 var isConvertedYMeasure = isMeasureYMatch && measures.y.converted; 425 426 if (isNumericType(type) || isConvertedYMeasure) { 427 if (isMeasureXMatch) { 428 if (fields[i].extFormatFn) { 429 scales.x.tickFormat = eval(fields[i].extFormatFn); 430 } 431 else if (defaultFormatFn) { 432 scales.x.tickFormat = defaultFormatFn; 433 } 434 } 435 436 if (isMeasureYMatch) { 437 var tickFormatFn; 438 439 if (fields[i].extFormatFn) { 440 tickFormatFn = eval(fields[i].extFormatFn); 441 } 442 else if (defaultFormatFn) { 443 tickFormatFn = defaultFormatFn; 444 } 445 446 scales.y.tickFormat = function(value) { 447 if (Ext4.isNumber(value) && Math.abs(Math.round(value)).toString().length >= valExponentialDigits) { 448 return value.toExponential(); 449 } 450 else if (Ext4.isFunction(tickFormatFn)) { 451 return tickFormatFn(value); 452 } 453 return value; 454 }; 455 } 456 } 457 else if (isMeasureXMatch && measures.x.name == subjectColumn && LABKEY.demoMode) { 458 scales.x.tickFormat = function(){return '******'}; 459 } 460 } 461 462 if (savedScales.x && (savedScales.x.min != null || savedScales.x.max != null)) { 463 scales.x.domain = [savedScales.x.min, savedScales.x.max]; 464 } 465 466 if (Ext4.isDefined(measures.xSub) && savedScales.xSub && (savedScales.xSub.min != null || savedScales.xSub.max != null)) { 467 scales.xSub.domain = [savedScales.xSub.min, savedScales.xSub.max]; 468 } 469 470 if (savedScales.y && (savedScales.y.min != null || savedScales.y.max != null)) { 471 scales.y.domain = [savedScales.y.min, savedScales.y.max]; 472 } 473 474 return scales; 475 }; 476 477 /** 478 * Generates the aesthetic map object needed by the visualization API to render the chart. See {@link LABKEY.vis.Plot} 479 * and {@link LABKEY.vis.Layer}. 480 * @param {String} chartType The chartType from getChartType. 481 * @param {Object} measures The measures from getMeasures. 482 * @param {String} schemaName The schemaName from the saved queryConfig. 483 * @param {String} queryName The queryName from the saved queryConfig. 484 * @returns {Object} 485 */ 486 var generateAes = function(chartType, measures, schemaName, queryName) { 487 var aes = {}, 488 xMeasureType = getMeasureType(measures.x); 489 490 if (chartType == "box_plot" && !measures.x) 491 { 492 aes.x = generateMeasurelessAcc(queryName); 493 } 494 else if (isNumericType(xMeasureType) || (chartType == 'scatter_plot' && measures.x.measure)) 495 { 496 var xMeasureName = measures.x.converted ? measures.x.convertedName : measures.x.name; 497 aes.x = generateContinuousAcc(xMeasureName); 498 } 499 else 500 { 501 var xMeasureName = measures.x.converted ? measures.x.convertedName : measures.x.name; 502 aes.x = generateDiscreteAcc(xMeasureName, measures.x.label); 503 } 504 505 if (measures.y) 506 { 507 var yMeasureName = measures.y.converted ? measures.y.convertedName : measures.y.name; 508 aes.y = generateContinuousAcc(yMeasureName); 509 } 510 511 if (chartType === "scatter_plot" || chartType === "line_plot") 512 { 513 aes.hoverText = generatePointHover(measures); 514 } 515 516 if (chartType === "box_plot") 517 { 518 if (measures.color) { 519 aes.outlierColor = generateGroupingAcc(measures.color.name); 520 } 521 522 if (measures.shape) { 523 aes.outlierShape = generateGroupingAcc(measures.shape.name); 524 } 525 526 aes.hoverText = generateBoxplotHover(); 527 aes.outlierHoverText = generatePointHover(measures); 528 } 529 else if (chartType === 'bar_chart') 530 { 531 var xSubMeasureType = measures.xSub ? getMeasureType(measures.xSub) : null; 532 if (xSubMeasureType) 533 { 534 if (isNumericType(xSubMeasureType)) 535 aes.xSub = generateContinuousAcc(measures.xSub.name); 536 else 537 aes.xSub = generateDiscreteAcc(measures.xSub.name, measures.xSub.label); 538 } 539 } 540 541 // color/shape aes are not dependent on chart type. If we have a box plot with all points enabled, then we 542 // create a second layer for points. So we'll need this no matter what. 543 if (measures.color) { 544 aes.color = generateGroupingAcc(measures.color.name); 545 } 546 547 if (measures.shape) { 548 aes.shape = generateGroupingAcc(measures.shape.name); 549 } 550 551 // also add the color and shape for the line plot series. 552 if (measures.series) { 553 aes.color = generateGroupingAcc(measures.series.name); 554 aes.shape = generateGroupingAcc(measures.series.name); 555 } 556 557 if (measures.pointClickFn) { 558 aes.pointClickFn = generatePointClickFn( 559 measures, 560 schemaName, 561 queryName, 562 measures.pointClickFn 563 ); 564 } 565 566 return aes; 567 }; 568 569 /** 570 * Generates a function that returns the text used for point hovers. 571 * @param {Object} measures The measures object from the saved chart config. 572 * @returns {Function} 573 */ 574 var generatePointHover = function(measures) 575 { 576 return function(row) { 577 var hover = '', sep = '', distinctNames = []; 578 579 Ext4.Object.each(measures, function(key, measure) { 580 if (Ext4.isObject(measure) && distinctNames.indexOf(measure.name) == -1) { 581 hover += sep + measure.label + ': ' + _getRowValue(row, measure.name); 582 sep = ', \n'; 583 584 distinctNames.push(measure.name); 585 } 586 }); 587 588 return hover; 589 }; 590 }; 591 592 /** 593 * Backwards compatibility for function that has been moved to LABKEY.vis.getAggregateData. 594 */ 595 var generateAggregateData = function(data, dimensionName, measureName, aggregate, nullDisplayValue) { 596 return LABKEY.vis.getAggregateData(data, dimensionName, null, measureName, aggregate, nullDisplayValue, false); 597 }; 598 599 var _getRowValue = function(row, propName, valueName) 600 { 601 if (row.hasOwnProperty(propName)) { 602 // backwards compatibility for response row that is not a LABKEY.Query.Row 603 if (!(row instanceof LABKEY.Query.Row)) { 604 return row[propName].displayValue || row[propName].value; 605 } 606 607 var propValue = row.get(propName); 608 if (valueName != undefined && propValue.hasOwnProperty(valueName)) { 609 return propValue[valueName]; 610 } 611 else if (propValue.hasOwnProperty('displayValue')) { 612 return propValue['displayValue']; 613 } 614 return row.getValue(propName); 615 } 616 617 return undefined; 618 }; 619 620 /** 621 * Returns a function used to generate the hover text for box plots. 622 * @returns {Function} 623 */ 624 var generateBoxplotHover = function() { 625 return function(xValue, stats) { 626 return xValue + ':\nMin: ' + stats.min + '\nMax: ' + stats.max + '\nQ1: ' + stats.Q1 + '\nQ2: ' + stats.Q2 + 627 '\nQ3: ' + stats.Q3; 628 }; 629 }; 630 631 /** 632 * Generates an accessor function that returns a discrete value from a row of data for a given measure and label. 633 * Used when an axis has a discrete measure (i.e. string). 634 * @param {String} measureName The name of the measure. 635 * @param {String} measureLabel The label of the measure. 636 * @returns {Function} 637 */ 638 var generateDiscreteAcc = function(measureName, measureLabel) 639 { 640 return function(row) 641 { 642 var value = _getRowValue(row, measureName); 643 if (value === null) 644 value = "Not in " + measureLabel; 645 646 return value; 647 }; 648 }; 649 650 /** 651 * Generates an accessor function that returns a value from a row of data for a given measure. 652 * @param {String} measureName The name of the measure. 653 * @returns {Function} 654 */ 655 var generateContinuousAcc = function(measureName) 656 { 657 return function(row) 658 { 659 var value = _getRowValue(row, measureName, 'value'); 660 661 if (value !== undefined) 662 { 663 if (Math.abs(value) === Infinity) 664 value = null; 665 666 if (value === false || value === true) 667 value = value.toString(); 668 669 return value; 670 } 671 672 return undefined; 673 } 674 }; 675 676 /** 677 * Generates an accesssor function for shape and color measures. 678 * @param {String} measureName The name of the measure. 679 * @returns {Function} 680 */ 681 var generateGroupingAcc = function(measureName) 682 { 683 return function(row) 684 { 685 var value = null; 686 if (Ext4.isArray(row) && row.length > 0) { 687 value = _getRowValue(row[0], measureName); 688 } 689 else { 690 value = _getRowValue(row, measureName); 691 } 692 693 if (value === null || value === undefined) 694 value = "n/a"; 695 696 return value; 697 }; 698 }; 699 700 /** 701 * Generates an accessor for boxplots that do not have an x-axis measure. Generally the measureName passed in is the 702 * queryName. 703 * @param {String} measureName The name of the measure. In this case it is generally the query name. 704 * @returns {Function} 705 */ 706 var generateMeasurelessAcc = function(measureName) { 707 // Used for box plots that do not have an x-axis measure. Instead we just return the queryName for every row. 708 return function(row) { 709 return measureName; 710 } 711 }; 712 713 /** 714 * Generates the function to be executed when a user clicks a point. 715 * @param {Object} measures The measures from the saved chart config. 716 * @param {String} schemaName The schema name from the saved query config. 717 * @param {String} queryName The query name from the saved query config. 718 * @param {String} fnString The string value of the user-provided function to be executed when a point is clicked. 719 * @returns {Function} 720 */ 721 var generatePointClickFn = function(measures, schemaName, queryName, fnString){ 722 var measureInfo = { 723 schemaName: schemaName, 724 queryName: queryName 725 }; 726 727 if (measures.y) 728 measureInfo.yAxis = measures.y.name; 729 if (measures.x) 730 measureInfo.xAxis = measures.x.name; 731 Ext4.each(['color', 'shape', 'series'], function(name) { 732 if (measures[name]) { 733 measureInfo[name + 'Name'] = measures[name].name; 734 } 735 }, this); 736 737 // using new Function is quicker than eval(), even in IE. 738 var pointClickFn = new Function('return ' + fnString)(); 739 return function(clickEvent, data){ 740 pointClickFn(data, measureInfo, clickEvent); 741 }; 742 }; 743 744 /** 745 * Generates the Point Geom used for scatter plots and box plots with all points visible. 746 * @param {Object} chartOptions The saved chartOptions object from the chart config. 747 * @returns {LABKEY.vis.Geom.Point} 748 */ 749 var generatePointGeom = function(chartOptions){ 750 return new LABKEY.vis.Geom.Point({ 751 opacity: chartOptions.opacity, 752 size: chartOptions.pointSize, 753 color: '#' + chartOptions.pointFillColor, 754 position: chartOptions.position 755 }); 756 }; 757 758 /** 759 * Generates the Boxplot Geom used for box plots. 760 * @param {Object} chartOptions The saved chartOptions object from the chart config. 761 * @returns {LABKEY.vis.Geom.Boxplot} 762 */ 763 var generateBoxplotGeom = function(chartOptions){ 764 return new LABKEY.vis.Geom.Boxplot({ 765 lineWidth: chartOptions.lineWidth, 766 outlierOpacity: chartOptions.opacity, 767 outlierFill: '#' + chartOptions.pointFillColor, 768 outlierSize: chartOptions.pointSize, 769 color: '#' + chartOptions.lineColor, 770 fill: chartOptions.boxFillColor == 'none' ? chartOptions.boxFillColor : '#' + chartOptions.boxFillColor, 771 position: chartOptions.position, 772 showOutliers: chartOptions.showOutliers 773 }); 774 }; 775 776 /** 777 * Generates the Barplot Geom used for bar charts. 778 * @param {Object} chartOptions The saved chartOptions object from the chart config. 779 * @returns {LABKEY.vis.Geom.BarPlot} 780 */ 781 var generateBarGeom = function(chartOptions){ 782 return new LABKEY.vis.Geom.BarPlot({ 783 opacity: chartOptions.opacity, 784 color: '#' + chartOptions.lineColor, 785 fill: '#' + chartOptions.boxFillColor, 786 lineWidth: chartOptions.lineWidth 787 }); 788 }; 789 790 /** 791 * Generates the Bin Geom used to bin a set of points. 792 * @param {Object} chartOptions The saved chartOptions object from the chart config. 793 * @returns {LABKEY.vis.Geom.Bin} 794 */ 795 var generateBinGeom = function(chartOptions) { 796 var colorRange = ["#e6e6e6", "#085D90"]; //light-gray and labkey blue is default 797 if (chartOptions.binColorGroup == 'SingleColor') { 798 var color = '#' + chartOptions.binSingleColor; 799 colorRange = ["#FFFFFF", color]; 800 } 801 else if (chartOptions.binColorGroup == 'Heat') { 802 colorRange = ["#fff6bc", "#e23202"]; 803 } 804 805 return new LABKEY.vis.Geom.Bin({ 806 shape: chartOptions.binShape, 807 colorRange: colorRange, 808 size: chartOptions.binShape == 'square' ? 10 : 5 809 }) 810 }; 811 812 /** 813 * Generates a Geom based on the chartType. 814 * @param {String} chartType The chart type from getChartType. 815 * @param {Object} chartOptions The chartOptions object from the saved chart config. 816 * @returns {LABKEY.vis.Geom} 817 */ 818 var generateGeom = function(chartType, chartOptions) { 819 if (chartType == "box_plot") 820 return generateBoxplotGeom(chartOptions); 821 else if (chartType == "scatter_plot" || chartType == "line_plot") 822 return chartOptions.binned ? generateBinGeom(chartOptions) : generatePointGeom(chartOptions); 823 else if (chartType == "bar_chart") 824 return generateBarGeom(chartOptions); 825 }; 826 827 /** 828 * Generate the plot config for the given chart renderType and config options. 829 * @param renderTo 830 * @param chartConfig 831 * @param labels 832 * @param aes 833 * @param scales 834 * @param geom 835 * @param data 836 * @returns {Object} 837 */ 838 var generatePlotConfig = function(renderTo, chartConfig, labels, aes, scales, geom, data) 839 { 840 var renderType = chartConfig.renderType, 841 layers = [], clipRect, 842 plotConfig = { 843 renderTo: renderTo, 844 rendererType: 'd3', 845 width: chartConfig.width, 846 height: chartConfig.height 847 }; 848 849 if (renderType == 'pie_chart') 850 return _generatePieChartConfig(plotConfig, chartConfig, labels, data); 851 852 clipRect = (scales.x && Ext4.isArray(scales.x.domain)) || (scales.y && Ext4.isArray(scales.y.domain)); 853 854 if (renderType == 'bar_chart') 855 { 856 aes = { x: 'label', y: 'value' }; 857 858 if (Ext4.isDefined(chartConfig.measures.xSub)) 859 { 860 aes.xSub = 'subLabel'; 861 aes.color = 'label'; 862 } 863 864 if (!scales.y) { 865 scales.y = {}; 866 } 867 868 if (!scales.y.domain) { 869 var values = Ext4.Array.pluck(data, 'value'), 870 min = Math.min(0, Ext4.Array.min(values)), 871 max = Math.max(0, Ext4.Array.max(values)); 872 873 scales.y.domain = [min, max]; 874 } 875 } 876 else if (renderType == 'box_plot' && chartConfig.pointType == 'all') 877 { 878 layers.push( 879 new LABKEY.vis.Layer({ 880 data: data, 881 geom: LABKEY.vis.GenericChartHelper.generatePointGeom(chartConfig.geomOptions), 882 aes: {hoverText: LABKEY.vis.GenericChartHelper.generatePointHover(chartConfig.measures)} 883 }) 884 ); 885 } 886 else if (renderType == 'line_plot') { 887 var xName = chartConfig.measures.x.name, 888 isDate = isDateType(getMeasureType(chartConfig.measures.x)), 889 pathAes = {}; 890 891 pathAes.sortFn = function(a, b) { 892 // No need to handle the case for a or b or a.getValue() or b.getValue() null as they are 893 // not currently included in this plot. 894 if (isDate){ 895 return new Date(a.getValue(xName)) - new Date(b.getValue(xName)); 896 } 897 return a.getValue(xName) - b.getValue(xName); 898 }; 899 900 if (chartConfig.measures.series) { 901 pathAes.pathColor = generateGroupingAcc(chartConfig.measures.series.name); 902 pathAes.group = generateGroupingAcc(chartConfig.measures.series.name); 903 } 904 905 layers.push( 906 new LABKEY.vis.Layer({ 907 geom: new LABKEY.vis.Geom.Path({ 908 color: '#' + chartConfig.geomOptions.pointFillColor, 909 size: chartConfig.geomOptions.lineWidth?chartConfig.geomOptions.lineWidth:3, 910 opacity:chartConfig.geomOptions.opacity 911 }), 912 aes: pathAes 913 }) 914 ); 915 } 916 917 var margins = _getPlotMargins(renderType, aes, data, plotConfig); 918 if (Ext4.isObject(margins)) { 919 plotConfig.margins = margins; 920 } 921 922 if (chartConfig.measures.color) 923 { 924 scales.color = { 925 colorType: chartConfig.geomOptions.colorPaletteScale, 926 scaleType: 'discrete' 927 } 928 } 929 930 layers.push( 931 new LABKEY.vis.Layer({ 932 data: data, 933 geom: geom 934 }) 935 ); 936 937 plotConfig = Ext4.apply(plotConfig, { 938 clipRect: clipRect, 939 data: data, 940 labels: labels, 941 aes: aes, 942 scales: scales, 943 layers: layers 944 }); 945 946 return plotConfig; 947 }; 948 949 var _getPlotMargins = function(renderType, aes, data, plotConfig) { 950 // issue 29690: for bar and box plots, set default bottom margin based on the number of labels and the max label length 951 if (Ext4.isArray(data) && ((renderType == 'bar_chart' && !Ext4.isDefined(aes.xSub)) || renderType == 'box_plot')) { 952 var maxLen = 0; 953 Ext4.each(data, function(d) { 954 var val = Ext4.isFunction(aes.x) ? aes.x(d) : d[aes.x]; 955 if (Ext4.isString(val)) { 956 maxLen = Math.max(maxLen, val.length); 957 } 958 }); 959 960 if (data.length * maxLen*5 > plotConfig.width - 150) { 961 // min bottom margin: 50, max bottom margin: 275 962 var bottomMargin = Math.min(Math.max(50, maxLen*5), 275); 963 return {bottom: bottomMargin}; 964 } 965 } 966 967 return null; 968 }; 969 970 var _generatePieChartConfig = function(baseConfig, chartConfig, labels, data) 971 { 972 var hasData = data.length > 0; 973 974 return Ext4.apply(baseConfig, { 975 data: hasData ? data : [{label: '', value: 1}], 976 header: { 977 title: { text: labels.main.value }, 978 subtitle: { text: labels.subtitle.value }, 979 titleSubtitlePadding: 1 980 }, 981 footer: { 982 text: hasData ? labels.footer.value : 'No data to display', 983 location: 'bottom-center' 984 }, 985 labels: { 986 mainLabel: { fontSize: 14 }, 987 percentage: { 988 fontSize: 14, 989 color: chartConfig.geomOptions.piePercentagesColor != null ? '#' + chartConfig.geomOptions.piePercentagesColor : undefined 990 }, 991 outer: { pieDistance: 20 }, 992 inner: { 993 format: hasData && chartConfig.geomOptions.showPiePercentages ? 'percentage' : 'none', 994 hideWhenLessThanPercentage: chartConfig.geomOptions.pieHideWhenLessThanPercentage 995 } 996 }, 997 size: { 998 pieInnerRadius: hasData ? chartConfig.geomOptions.pieInnerRadius + '%' : '100%', 999 pieOuterRadius: hasData ? chartConfig.geomOptions.pieOuterRadius + '%' : '90%' 1000 }, 1001 misc: { 1002 gradient: { 1003 enabled: chartConfig.geomOptions.gradientPercentage != 0, 1004 percentage: chartConfig.geomOptions.gradientPercentage, 1005 color: '#' + chartConfig.geomOptions.gradientColor 1006 }, 1007 colors: { 1008 segments: hasData ? LABKEY.vis.Scale[chartConfig.geomOptions.colorPaletteScale]() : ['#333333'] 1009 } 1010 }, 1011 effects: { highlightSegmentOnMouseover: false }, 1012 tooltips: { enabled: true } 1013 }); 1014 }; 1015 1016 /** 1017 * Check if the MeasureStore selectRows API response has data. Return an error string if no data exists. 1018 * @param measureStore 1019 * @param includeFilterMsg true to include a message about removing filters 1020 * @returns {String} 1021 */ 1022 var validateResponseHasData = function(measureStore, includeFilterMsg) 1023 { 1024 var dataArray = Ext4.isDefined(measureStore) ? measureStore.rows || measureStore.records() : []; 1025 if (dataArray.length == 0) 1026 { 1027 return 'The response returned 0 rows of data. The query may be empty or the applied filters may be too strict.' 1028 + (includeFilterMsg ? 'Try removing or adjusting any filters if possible.' : ''); 1029 } 1030 1031 return null; 1032 }; 1033 1034 /** 1035 * Verifies that the axis measure is actually present and has data. Also checks to make sure that data can be used in a log 1036 * scale (if applicable). Returns an object with a success parameter (boolean) and a message parameter (string). If the 1037 * success parameter is false there is a critical error and the chart cannot be rendered. If success is true the chart 1038 * can be rendered. Message will contain an error or warning message if applicable. If message is not null and success 1039 * is true, there is a warning. 1040 * @param {String} chartType The chartType from getChartType. 1041 * @param {Object} chartConfig The saved chartConfig object. 1042 * @param {String} measureName The name of the axis measure property. 1043 * @param {Object} aes The aes object from generateAes. 1044 * @param {Object} scales The scales object from generateScales. 1045 * @param {Array} data The response data from selectRows. 1046 * @param {Boolean} dataConversionHappened Whether we converted any values in the measure data 1047 * @returns {Object} 1048 */ 1049 var validateAxisMeasure = function(chartType, chartConfig, measureName, aes, scales, data, dataConversionHappened) { 1050 var dataIsNull = true, measureUndefined = true, invalidLogValues = false, hasZeroes = false, message = null; 1051 1052 // no need to check measures if we have no data 1053 if (data.length == 0) { 1054 return {success: true, message: message}; 1055 } 1056 1057 for (var i = 0; i < data.length; i ++) 1058 { 1059 var value = aes[measureName](data[i]); 1060 1061 if (value !== undefined) 1062 measureUndefined = false; 1063 1064 if (value !== null) 1065 dataIsNull = false; 1066 1067 if (value && value < 0) 1068 invalidLogValues = true; 1069 1070 if (value === 0 ) 1071 hasZeroes = true; 1072 } 1073 1074 if (measureUndefined) 1075 { 1076 message = 'The measure ' + chartConfig.measures[measureName].label + ' was not found. It may have been renamed or removed.'; 1077 return {success: false, message: message}; 1078 } 1079 1080 if ((chartType == 'scatter_plot' || chartType == 'line_plot' || measureName == 'y') && dataIsNull && !dataConversionHappened) 1081 { 1082 message = 'All data values for ' + chartConfig.measures[measureName].label + ' are null. Please choose a different measure.'; 1083 return {success: false, message: message}; 1084 } 1085 1086 if (scales[measureName] && scales[measureName].trans == "log") 1087 { 1088 if (invalidLogValues) 1089 { 1090 message = "Unable to use a log scale on the " + measureName + "-axis. All " + measureName 1091 + "-axis values must be >= 0. Reverting to linear scale on " + measureName + "-axis."; 1092 scales[measureName].trans = 'linear'; 1093 } 1094 else if (hasZeroes) 1095 { 1096 message = "Some " + measureName + "-axis values are 0. Plotting all " + measureName + "-axis values as value+1."; 1097 var accFn = aes[measureName]; 1098 aes[measureName] = function(row){return accFn(row) + 1}; 1099 } 1100 } 1101 1102 return {success: true, message: message}; 1103 }; 1104 1105 /** 1106 * Deprecated - use validateAxisMeasure 1107 */ 1108 var validateXAxis = function(chartType, chartConfig, aes, scales, data){ 1109 return this.validateAxisMeasure(chartType, chartConfig, 'x', aes, scales, data); 1110 }; 1111 /** 1112 * Deprecated - use validateAxisMeasure 1113 */ 1114 var validateYAxis = function(chartType, chartConfig, aes, scales, data){ 1115 return this.validateAxisMeasure(chartType, chartConfig, 'y', aes, scales, data); 1116 }; 1117 1118 var getMeasureType = function(measure) { 1119 return measure ? (measure.normalizedType || measure.type) : null; 1120 }; 1121 1122 var isNumericType = function(type) 1123 { 1124 var t = Ext4.isString(type) ? type.toLowerCase() : null; 1125 return t == 'int' || t == 'integer' || t == 'float' || t == 'double'; 1126 }; 1127 1128 var isDateType = function(type) 1129 { 1130 var t = Ext4.isString(type) ? type.toLowerCase() : null; 1131 return t == 'date'; 1132 }; 1133 1134 var _getStudySubjectInfo = function() 1135 { 1136 var studyCtx = LABKEY.getModuleContext("study") || {}; 1137 return Ext4.isObject(studyCtx.subject) ? studyCtx.subject : { 1138 tableName: 'Participant', 1139 columnName: 'ParticipantId', 1140 nounPlural: 'Participants', 1141 nounSingular: 'Participant' 1142 }; 1143 }; 1144 1145 var _getStudyTimepointType = function() 1146 { 1147 var studyCtx = LABKEY.getModuleContext("study") || {}; 1148 return Ext4.isDefined(studyCtx.timepointType) ? studyCtx.timepointType : null; 1149 }; 1150 1151 var _getMeasureRestrictions = function (chartType, measure) 1152 { 1153 var measureRestrictions = {}; 1154 Ext4.each(getRenderTypes(), function (renderType) 1155 { 1156 if (renderType.name === chartType) 1157 { 1158 Ext4.each(renderType.fields, function (field) 1159 { 1160 if (field.name === measure) 1161 { 1162 measureRestrictions.numericOnly = field.numericOnly; 1163 measureRestrictions.nonNumericOnly = field.nonNumericOnly; 1164 return false; 1165 } 1166 }); 1167 return false; 1168 } 1169 }); 1170 1171 return measureRestrictions; 1172 }; 1173 1174 /** 1175 * Converts data values passed in to the appropriate type based on measure/dimension information. 1176 * @param chartConfig Chart configuration object 1177 * @param aes Aesthetic mapping functions for each measure/axis 1178 * @param renderType The type of plot or chart (e.g. scatter_plot, bar_chart) 1179 * @param data The response data from SelectRows 1180 * @returns {{processed: {}, warningMessage: *}} 1181 */ 1182 var doValueConversion = function(chartConfig, aes, renderType, data) 1183 { 1184 var measuresForProcessing = {}, measureRestrictions = {}, configMeasure; 1185 for (var measureName in chartConfig.measures) { 1186 if (chartConfig.measures.hasOwnProperty(measureName) && Ext4.isObject(chartConfig.measures[measureName])) { 1187 configMeasure = chartConfig.measures[measureName]; 1188 Ext4.apply(measureRestrictions, _getMeasureRestrictions(renderType, measureName)); 1189 1190 var isGroupingMeasure = measureName === 'color' || measureName === 'shape' || measureName === 'series'; 1191 var isXAxis = measureName === 'x' || measureName === 'xSub'; 1192 var isScatterOrLine = renderType === 'scatter_plot' || renderType === 'line_plot'; 1193 var isBarYCount = renderType === 'bar_chart' && configMeasure.aggregate && (configMeasure.aggregate === 'COUNT' || configMeasure.aggregate.value === 'COUNT'); 1194 1195 if (configMeasure.measure && !isGroupingMeasure && !isBarYCount 1196 && ((!isXAxis && measureRestrictions.numericOnly ) || isScatterOrLine) && !isNumericType(configMeasure.type)) { 1197 measuresForProcessing[measureName] = {}; 1198 measuresForProcessing[measureName].name = configMeasure.name; 1199 measuresForProcessing[measureName].convertedName = configMeasure.name + "_converted"; 1200 measuresForProcessing[measureName].label = configMeasure.label; 1201 configMeasure.normalizedType = 'float'; 1202 configMeasure.type = 'float'; 1203 } 1204 } 1205 } 1206 1207 var response = {processed: {}}; 1208 if (!Ext4.Object.isEmpty(measuresForProcessing)) { 1209 response = _processMeasureData(data, aes, measuresForProcessing); 1210 } 1211 1212 //generate error message for dropped values 1213 var warningMessage = ''; 1214 for (var measure in response.droppedValues) { 1215 if (response.droppedValues.hasOwnProperty(measure) && response.droppedValues[measure].numDropped) { 1216 warningMessage += " The " 1217 + measure + "-axis measure '" 1218 + response.droppedValues[measure].label + "' had " 1219 + response.droppedValues[measure].numDropped + 1220 " value(s) that could not be converted to a number and are not included in the plot."; 1221 } 1222 } 1223 1224 return {processed: response.processed, warningMessage: warningMessage}; 1225 }; 1226 1227 /** 1228 * Does the explicit type conversion for each measure deemed suitable to convert. Currently we only 1229 * attempt to convert strings to numbers for measures. 1230 * @param rows Data from SelectRows 1231 * @param aes Aesthetic mapping function for the measure/dimensions 1232 * @param measuresForProcessing The measures to be converted, if any 1233 * @returns {{droppedValues: {}, processed: {}}} 1234 */ 1235 var _processMeasureData = function(rows, aes, measuresForProcessing) { 1236 var droppedValues = {}, processedMeasures = {}, dataIsNull; 1237 rows.forEach(function(row) { 1238 //convert measures if applicable 1239 if (!Ext4.Object.isEmpty(measuresForProcessing)) { 1240 for (var measure in measuresForProcessing) { 1241 if (measuresForProcessing.hasOwnProperty(measure)) { 1242 dataIsNull = true; 1243 if (!droppedValues[measure]) { 1244 droppedValues[measure] = {}; 1245 droppedValues[measure].label = measuresForProcessing[measure].label; 1246 droppedValues[measure].numDropped = 0; 1247 } 1248 1249 if (aes.hasOwnProperty(measure)) { 1250 var value = aes[measure](row); 1251 if (value !== null) { 1252 dataIsNull = false; 1253 } 1254 row[measuresForProcessing[measure].convertedName] = {value: null}; 1255 if (typeof value !== 'number' && value !== null) { 1256 1257 //only try to convert strings to numbers 1258 if (typeof value === 'string') { 1259 value = value.trim(); 1260 } 1261 else { 1262 //dates, objects, booleans etc. to be assigned value: NULL 1263 value = ''; 1264 } 1265 1266 var n = Number(value); 1267 // empty strings convert to 0, which we must explicitly deny 1268 if (value === '' || isNaN(n)) { 1269 droppedValues[measure].numDropped++; 1270 } 1271 else { 1272 row[measuresForProcessing[measure].convertedName].value = n; 1273 } 1274 } 1275 } 1276 1277 if (!processedMeasures[measure]) { 1278 processedMeasures[measure] = { 1279 converted: false, 1280 convertedName: measuresForProcessing[measure].convertedName, 1281 type: 'float', 1282 normalizedType: 'float' 1283 } 1284 } 1285 1286 processedMeasures[measure].converted = processedMeasures[measure].converted || !dataIsNull; 1287 } 1288 } 1289 } 1290 }); 1291 1292 return {droppedValues: droppedValues, processed: processedMeasures}; 1293 }; 1294 1295 /** 1296 * removes all traces of String -> Numeric Conversion from the given chart config 1297 * @param chartConfig 1298 * @returns {updated ChartConfig} 1299 */ 1300 var removeNumericConversionConfig = function(chartConfig) { 1301 if (chartConfig && chartConfig.measures) { 1302 for (var measureName in chartConfig.measures) { 1303 if (chartConfig.measures.hasOwnProperty(measureName)) { 1304 var measure = chartConfig.measures[measureName]; 1305 if (measure && measure.converted && measure.convertedName) { 1306 measure.converted = null; 1307 measure.convertedName = null; 1308 if (LABKEY.vis.GenericChartHelper.isNumericType(measure.type)) { 1309 measure.type = 'string'; 1310 measure.normalizedType = 'string'; 1311 } 1312 } 1313 } 1314 } 1315 } 1316 1317 return chartConfig; 1318 }; 1319 1320 return { 1321 // NOTE: the @function below is needed or JSDoc will not include the documentation for loadVisDependencies. Don't 1322 // ask me why, I do not know. 1323 /** 1324 * @function 1325 */ 1326 getRenderTypes: getRenderTypes, 1327 getChartType: getChartType, 1328 getSelectedMeasureLabel: getSelectedMeasureLabel, 1329 getTitleFromMeasures: getTitleFromMeasures, 1330 getMeasureType: getMeasureType, 1331 getQueryColumns : getQueryColumns, 1332 getChartTypeBasedWidth : getChartTypeBasedWidth, 1333 isNumericType: isNumericType, 1334 generateLabels: generateLabels, 1335 generateScales: generateScales, 1336 generateAes: generateAes, 1337 doValueConversion: doValueConversion, 1338 removeNumericConversionConfig: removeNumericConversionConfig, 1339 generateAggregateData: generateAggregateData, 1340 generatePointHover: generatePointHover, 1341 generateBoxplotHover: generateBoxplotHover, 1342 generateDiscreteAcc: generateDiscreteAcc, 1343 generateContinuousAcc: generateContinuousAcc, 1344 generateGroupingAcc: generateGroupingAcc, 1345 generatePointClickFn: generatePointClickFn, 1346 generateGeom: generateGeom, 1347 generateBoxplotGeom: generateBoxplotGeom, 1348 generatePointGeom: generatePointGeom, 1349 generatePlotConfig: generatePlotConfig, 1350 validateResponseHasData: validateResponseHasData, 1351 validateAxisMeasure: validateAxisMeasure, 1352 validateXAxis: validateXAxis, 1353 validateYAxis: validateYAxis, 1354 /** 1355 * Loads all of the required dependencies for a Generic Chart. 1356 * @param {Function} callback The callback to be executed when all of the visualization dependencies have been loaded. 1357 * @param {Object} scope The scope to be used when executing the callback. 1358 */ 1359 loadVisDependencies: LABKEY.requiresVisualization 1360 }; 1361 };