/* * RHQ Management Platform * Copyright (C) 2005-2012 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.coregui.client.inventory.common.graph.graphtype; import org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph; /** * Contains the javascript chart definition for a d3 Stacked Bar graph chart. * The purpose of this class is the fill the div element with id: '#rChart-[resourceId]-[metricId]'. * with SVG instructions to draw a d3 graph (in this case a stacked bar graph). * * @author Mike Thompson */ public class StackedBarMetricGraphImpl extends AbstractMetricGraph { public StackedBarMetricGraphImpl() { super(); } /** * The magic JSNI to draw the charts with $wnd.d3.js */ @Override public native void drawJsniChart() /*-{ "use strict"; //console.log("Draw Stacked Bar jsni chart"); var global = this, // create a chartContext object (from rhq.js) with the data required to render to a chart // this same data could be passed to different chart types // This way, we are decoupled from the dependency on globals and JSNI and kept all the java interaction right here. chartContext = new $wnd.ChartContext(global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartId()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartHeight()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getJsonMetrics()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getXAxisTitle()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartTitle()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getYAxisUnits()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartTitleMinLabel()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartTitleAvgLabel()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartTitlePeakLabel()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartDateLabel()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartTimeLabel()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartDownLabel()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartUnknownLabel()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartNoDataLabel()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartHoverStartLabel()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartHoverEndLabel()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartHoverPeriodLabel()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartHoverBarLabel()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartHoverTimeFormat()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartHoverDateFormat()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::isPortalGraph()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getPortalId()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getButtonBarDateTimeFormat()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartSingleValueLabel()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getXAxisTimeFormatHours()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getXAxisTimeFormatHoursMinutes()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::isHideLegend()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartAverage()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartMin()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::getChartMax()(), global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::isSummaryGraph()() ); // Define the Stacked Bar Graph function using the module pattern var metricStackedBarGraph = function () { // privates var margin = {top: 10, right: 5, bottom: 5, left: 90}, margin2 = {top: 150, right: 5, bottom: 5, left: 90}, width = 750 - margin.left - margin.right, adjustedChartHeight = chartContext.chartHeight - 50, height = adjustedChartHeight - margin.top - margin.bottom, smallChartThresholdInPixels = 600, titleHeight = 30, titleSpace = 10, tooltipTimeout = 15000, barOffset = 2, chartData, interpolation = "basis", avgFiltered, avg, minFiltered, min, peakFiltered, peak, oobMax, legendUnDefined, lowBound, newLow = 0, highBound, calcBarWidth, yScale, timeScale, yAxis, xAxis, brush, brushGroup, timeScaleForBrush, chart, context, svg; // adjust the min scale so blue low line is not in axis function determineLowBound(min) { newLow = min; if (newLow < 0) { return 0; } else { return newLow; } } function getChartWidth() { return $wnd.jQuery("#" + chartContext.chartHandle).width(); } function useSmallCharts() { //console.log("getChartWidth: "+ getChartWidth()); return getChartWidth() <= smallChartThresholdInPixels; } function determineScale() { var xTicks, xTickSubDivide, numberOfBarsForSmallGraph = 20; if (chartContext.data.length > 0) { // if window is too small server up small chart if (useSmallCharts()) { //console.log("Using Small Charts Profile for width: "+getChartWidth()); width = 250; xTicks = 3; xTickSubDivide = 2; chartData = chartContext.data.slice(chartContext.data.length - numberOfBarsForSmallGraph, chartContext.data.length); } else { //console.log("Using Large Charts Profile, width: "+ width); // we use the width already defined above xTicks = 8; xTickSubDivide = 5; chartData = chartContext.data; } avgFiltered = chartContext.data.filter(function (d) { if (d.nodata !== 'true') { return d.avg; } }); avg = $wnd.d3.mean(avgFiltered.map(function (d) { return d.avg; })); peakFiltered = chartContext.data.filter(function (d) { if (d.nodata !== 'true') { return d.high; } }); peak = $wnd.d3.max(peakFiltered.map(function (d) { return d.high; })); minFiltered = chartContext.data.filter(function (d) { if (d.nodata !== 'true') { return d.low; } }); min = $wnd.d3.min(minFiltered.map(function (d) { return d.low; })); lowBound = determineLowBound(min); highBound = peak + ((peak - min) * 0.1); oobMax = $wnd.d3.max(chartContext.data.map(function (d) { if (typeof d.baselineMax === 'undefined') { return 0; } else { return +d.baselineMax; } })); calcBarWidth = function () { return (width / chartData.length - barOffset ) }; yScale = function() { // BZ 1146266 var low = $wnd.d3.min(chartContext.data, function(d) {return d.low;}), high = $wnd.d3.max(chartContext.data, function(d) {return d.high;}), reserve; if (low === high) { // if both global extremes are the same, make some interval around the value to make it work // because domain() excepts an array with some interval denoting an input value space // here we create ['0.80 * the value', 1.2 * 'the value'] i.e. 20% from both sides reserve = Math.max(Math.round(low * 0.2), 1); } return $wnd.d3.scale.linear() .clamp(true) .rangeRound([height, 0]) .domain([low - (low !== high ? 0 : reserve), high + (low !== high ? 0 : reserve)]); }; yAxis = $wnd.d3.svg.axis() .scale(yScale()) .tickSubdivide(1) .ticks(5) .tickSize(4, 4, 0) .orient("left"); timeScale = $wnd.d3.time.scale() .range([0, width]) .domain($wnd.d3.extent(chartData, function (d) { return d.x; })); timeScaleForBrush = $wnd.d3.time.scale() .range([0, width]) .domain($wnd.d3.extent(chartData, function (d) { return d.x; })); xAxis = $wnd.d3.svg.axis() .scale(timeScale) .ticks(xTicks) .tickSubdivide(xTickSubDivide) .tickSize(4, 4, 0) .orient("bottom"); // create the actual chart group chart = $wnd.d3.select("#" + chartContext.chartSelection); svg = chart.append("g") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top - titleHeight - titleSpace + margin.bottom) .attr("transform", "translate(" + margin.left + "," + (+titleHeight + titleSpace + margin.top) + ")"); context = svg.append("g") .attr("class", "context") .attr("width", width + margin.left + margin.right) .attr("height", 150) .attr("transform", "translate(" + margin2.left + "," + (+titleHeight + titleSpace + margin.top + 90) + ")"); legendUnDefined = (chartContext.chartAverage === ""); if ((!chartContext.hideLegend && !useSmallCharts() && !legendUnDefined )) { createMinAvgPeakSidePanel(chartContext.minChartTitle, chartContext.chartMin, chartContext.avgChartTitle, chartContext.chartAverage, chartContext.peakChartTitle, chartContext.chartMax ); } } } function createMinAvgPeakSidePanel(minLabel, minValue, avgLabel, avgValue, highLabel, highValue ) { var xLabel = 772, xValue = 820, yBase = 100, yInc = 25; // title/header chart.append("g").append("rect") .attr("class", "rightSidePanel") .attr("x", xLabel - 10) .attr("y", margin.top + 70) .attr("rx", 10) .attr("ry", 10) .attr("height", 80) .attr("width", 135) .attr("opacity", "0.3") .attr("fill", "#E8E8E8"); // high chart.append("text") .attr("class", "highLabel") .attr("x", xLabel) .attr("y", yBase) .text(highLabel + " - "); if(typeof highValue !== 'undefined'){ chart.append("text") .attr("class", "highText") .attr("x", xValue) .attr("y", yBase) .text(highValue); } //avg chart.append("text") .attr("class", "avgLabel") .attr("x", xLabel) .attr("y", yBase + yInc) .text(avgLabel + " - "); if(typeof avgValue !== 'undefined'){ chart.append("text") .attr("class", "avgText") .attr("x", xValue) .attr("y", yBase + yInc) .text(avgValue); } // min chart.append("text") .attr("class", "minLabel") .attr("x", xLabel) .attr("y", yBase + 2 * yInc) .text(minLabel + " - "); if(typeof minValue !== 'undefined'){ chart.append("text") .attr("class", "minText") .attr("x", xValue) .attr("y", yBase + 2 * yInc) .text(minValue); } } function createHeader(titleName) { var title = chart.append("g").append("rect") .attr("class", "title") .attr("x", 30) .attr("y", margin.top) .attr("height", titleHeight) .attr("width", width + 30 + margin.left) .attr("fill", "none"); chart.append("text") .attr("class", "titleName") .attr("x", 40) .attr("y", 37) .attr("font-size", "12") .attr("font-weight", "bold") .attr("text-anchor", "left") .text(titleName) .attr("fill", "#003168"); return title; } function showFullMetricBarHover(d){ var timeFormatter = $wnd.d3.time.format(chartContext.chartHoverTimeFormat), dateFormatter = $wnd.d3.time.format(chartContext.chartHoverDateFormat), startDate = new Date(+d.x), metricGraphTooltipDiv = $wnd.d3.select("#metricGraphTooltip"); metricGraphTooltipDiv.style("left", + ($wnd.d3.event.pageX) +15 + "px") .style("top", ($wnd.d3.event.pageY)+"px"); metricGraphTooltipDiv.select("#metricGraphTooltipTimeLabel") .text(chartContext.timeLabel); metricGraphTooltipDiv.select("#metricGraphTooltipTimeValue") .text(timeFormatter(startDate)); metricGraphTooltipDiv.select("#metricGraphTooltipDateLabel") .text(chartContext.dateLabel); metricGraphTooltipDiv.select("#metricGraphTooltipDateValue") .text(dateFormatter(startDate)); metricGraphTooltipDiv.select("#metricGraphTooltipDurationLabel") .text(chartContext.hoverBarLabel); metricGraphTooltipDiv.select("#metricGraphTooltipDurationValue") .text(d.barDuration); metricGraphTooltipDiv.select("#metricGraphTooltipMaxLabel") .text(chartContext.peakChartTitle); metricGraphTooltipDiv.select("#metricGraphTooltipMaxValue") .text(d.highFormatted); metricGraphTooltipDiv.select("#metricGraphTooltipAvgLabel") .text(chartContext.avgChartTitle); metricGraphTooltipDiv.select("#metricGraphTooltipAvgValue") .text(d.avgFormatted); metricGraphTooltipDiv.select("#metricGraphTooltipMinLabel") .text(chartContext.minChartTitle); metricGraphTooltipDiv.select("#metricGraphTooltipMinValue") .text(d.lowFormatted); //Show the tooltip $wnd.jQuery('#metricGraphTooltip').show(); setTimeout(function(){$wnd.jQuery('#metricGraphTooltip').hide();},tooltipTimeout); } function showNoDataBarHover(d){ var timeFormatter = $wnd.d3.time.format(chartContext.chartHoverTimeFormat), dateFormatter = $wnd.d3.time.format(chartContext.chartHoverDateFormat), startDate = new Date(+d.x), noDataTooltipDiv = $wnd.d3.select("#noDataTooltip"); noDataTooltipDiv.style("left", + ($wnd.d3.event.pageX) + 15 + "px") .style("top", ($wnd.d3.event.pageY)+"px"); noDataTooltipDiv.select("#noDataTooltipTimeLabel") .text(chartContext.timeLabel); noDataTooltipDiv.select("#noDataTooltipTimeValue") .text(timeFormatter(startDate)); noDataTooltipDiv.select("#noDataTooltipDateLabel") .text(chartContext.dateLabel); noDataTooltipDiv.select("#noDataTooltipDateValue") .text(dateFormatter(startDate)); noDataTooltipDiv.select("#noDataLabel") .text(chartContext.noDataLabel); //Show the tooltip $wnd.jQuery('#noDataTooltip').show(); setTimeout(function(){$wnd.jQuery('#noDataTooltip').hide();},tooltipTimeout); } function createStackedBars() { var pixelsOffHeight = 0; // The gray bars at the bottom leading up svg.selectAll("rect.leaderBar") .data(chartData) .enter().append("rect") .attr("class", "leaderBar") .attr("x", function (d) { return timeScale(d.x); }) .attr("y", function (d) { if (d.down || d.unknown || d.nodata) { return yScale()(highBound); } else { return yScale()(d.low); } }) .attr("height", function (d) { if (d.down || d.unknown || d.nodata) { return height - yScale()(highBound) - pixelsOffHeight; } else { return height - yScale()(d.low) - pixelsOffHeight; } }) .attr("width", function () { return calcBarWidth(); }) .attr("opacity", ".9") .attr("fill", function (d) { if (d.down || d.unknown || d.nodata) { return "url(#noDataStripes)"; } else { return "#d3d3d6"; } }).on("mouseover",function (d) { if (d.down || d.unknown || d.nodata) { showNoDataBarHover(d); } else { if(+d.high === +d.low){ showSingleValueMetricBarHover(d); } else { showFullMetricBarHover(d); } } }).on("mouseout", function (d) { if (d.down || d.unknown || d.nodata) { $wnd.jQuery('#noDataTooltip').hide(); }else { if(+d.high === +d.low){ $wnd.jQuery('#singleValueTooltip').hide(); } else { $wnd.jQuery('#metricGraphTooltip').hide(); } } }); // upper portion representing avg to high svg.selectAll("rect.high") .data(chartData) .enter().append("rect") .attr("class", "high") .attr("x", function (d) { return timeScale(d.x); }) .attr("y", function (d) { return isNaN(d.high) ? yScale()(lowBound) : yScale()(d.high); }) .attr("height", function (d) { if (d.down || d.unknown || d.nodata) { return 0; } else { return yScale()(d.avg) - yScale()(d.high); } }) .attr("width", function () { return calcBarWidth(); }) .attr("data-rhq-value", function (d) { return d.avg; }) .attr("opacity", 0.9) .on("mouseover",function (d) { showFullMetricBarHover(d); }).on("mouseout", function (d) { if (d.down || d.unknown || d.nodata) { $wnd.jQuery('#noDataTooltip').hide(); }else { $wnd.jQuery('#metricGraphTooltip').hide(); } }); // lower portion representing avg to low svg.selectAll("rect.low") .data(chartData) .enter().append("rect") .attr("class", "low") .attr("x", function (d) { return timeScale(d.x); }) .attr("y", function (d) { return isNaN(d.avg) ? height : yScale()(d.avg); }) .attr("height", function (d) { if (d.down || d.unknown || d.nodata) { return 0; } else { return yScale()(d.low) - yScale()(d.avg); } }) .attr("width", function () { return calcBarWidth(); }) .attr("opacity", 0.9) .on("mouseover",function (d) { showFullMetricBarHover(d); }).on("mouseout", function (d) { if (d.down || d.unknown || d.nodata) { $wnd.jQuery('#noDataTooltip').hide(); }else { $wnd.jQuery('#metricGraphTooltip').hide(); } }); function showSingleValueMetricBarHover(d){ var timeFormatter = $wnd.d3.time.format(chartContext.chartHoverTimeFormat), dateFormatter = $wnd.d3.time.format(chartContext.chartHoverDateFormat), startDate = new Date(+d.x), singleValueGraphTooltipDiv = $wnd.d3.select("#singleValueTooltip"); singleValueGraphTooltipDiv.style("left", + ($wnd.d3.event.pageX) + 15 + "px") .style("top", ($wnd.d3.event.pageY)+"px"); singleValueGraphTooltipDiv.select("#singleValueTooltipTimeLabel") .text(chartContext.timeLabel); singleValueGraphTooltipDiv.select("#singleValueTooltipTimeValue") .text(timeFormatter(startDate)); singleValueGraphTooltipDiv.select("#singleValueTooltipDateLabel") .text(chartContext.dateLabel); singleValueGraphTooltipDiv.select("#singleValueTooltipDateValue") .text(dateFormatter(startDate)); singleValueGraphTooltipDiv.select("#singleValueTooltipValueLabel") .text(chartContext.singleValueLabel); singleValueGraphTooltipDiv.select("#singleValueTooltipValue") .text(d.avgFormatted); //Show the tooltip $wnd.jQuery('#singleValueTooltip').show(); setTimeout(function(){$wnd.jQuery('#singleValueTooltip').hide();},tooltipTimeout); } // if high == low put a "cap" on the bar to show non-aggregated bar svg.selectAll("rect.singleValue") .data(chartData) .enter().append("rect") .attr("class", "singleValue") .attr("x", function (d) { return timeScale(d.x); }) .attr("y", function (d) { return isNaN(d.avg) ? height : yScale()(d.avg) - 2; }) .attr("height", function (d) { if (d.down || d.unknown || d.nodata) { return 0; } else { if (d.low === d.high) { return yScale()(d.low) - yScale()(d.avg) + 2; } else { return 0; } } }) .attr("width", function () { return calcBarWidth(); }) .attr("opacity", 0.9) .attr("fill", function (d) { if (d.low === d.high) { return "#50505a"; } else { return "#70c4e2"; } }).on("mouseover",function (d) { showSingleValueMetricBarHover(d); }).on("mouseout", function () { $wnd.jQuery('#singleValueTooltip').hide(); }); } function createYAxisGridLines() { // create the y axis grid lines svg.append("g").classed("grid y_grid", true) .call($wnd.d3.svg.axis() .scale(yScale()) .orient("left") .ticks(10) .tickSize(-width, 0, 0) .tickFormat("") ); } function createXandYAxes() { var xAxisGroup; xAxis.tickFormat($wnd.rhqCommon.getD3CustomTimeFormat(chartContext.chartXaxisTimeFormatHours, chartContext.chartXaxisTimeFormatHoursMinutes)); // create x-axis xAxisGroup = svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis); if(!chartContext.isPortalGraph){ xAxisGroup.append("g") .attr("class", "x brush") .call(brush) .selectAll("rect") .attr("y", -6) .attr("height", 30); } // create y-axis svg.append("g") .attr("class", "y axis") .call(yAxis) .append("text") .attr("transform", "rotate(-90),translate( -60,-30)") .attr("y", -30) .style("text-anchor", "end") .text(chartContext.yAxisUnits === "NONE" ? "" : chartContext.yAxisUnits); } function createAvgLines() { var showBarAvgTrendline = global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::showBarAvgTrendLine()(), barAvgLine = $wnd.d3.svg.line() .interpolate("linear") .defined(function (d) { return !d.nodata; }) .x(function (d) { return timeScale(d.x) + (calcBarWidth() / 2); }) .y(function (d) { if (showBarAvgTrendline) { return yScale()(d.avg); } else { return NaN; } }); // Bar avg line svg.append("path") .datum(chartData) .attr("class", "barAvgLine") .attr("fill", "none") .attr("stroke", "#2e376a") .attr("stroke-width", "1.5") .attr("stroke-opacity", ".7") .attr("d", barAvgLine); } function createOOBLines() { var unitsPercentMultiplier = chartContext.yAxisUnits === '%' ? 100 : 1, minBaselineLine = $wnd.d3.svg.line() .interpolate(interpolation) .x(function (d) { return timeScale(d.x); }) .y(function (d) { return yScale()(d.baselineMin * unitsPercentMultiplier); }), maxBaselineLine = $wnd.d3.svg.line() .interpolate(interpolation) .x(function (d) { return timeScale(d.x); }) .y(function (d) { return yScale()(d.baselineMax * unitsPercentMultiplier); }); // min baseline Line svg.append("path") .datum(chartData) .attr("class", "minBaselineLine") .attr("fill", "none") .attr("stroke", "purple") .attr("stroke-width", "1") .attr("stroke-dasharray", "20,10,5,5,5,10") .attr("stroke-opacity", ".9") .attr("d", minBaselineLine); // max baseline Line svg.append("path") .datum(chartData) .attr("class", "maxBaselineLine") .attr("fill", "none") .attr("stroke", "orange") .attr("stroke-width", "1") .attr("stroke-dasharray", "20,10,5,5,5,10") .attr("stroke-opacity", ".7") .attr("d", maxBaselineLine); } function createXAxisBrush(){ brush = $wnd.d3.svg.brush() .x(timeScaleForBrush) .on("brushstart", brushStart) .on("brush", brushMove) .on("brushend", brushEnd); brushGroup = svg.append("g") .attr("class", "brush") .call(brush); brushGroup.selectAll(".resize").append("path"); brushGroup.selectAll("rect") .attr("height", height); function brushStart() { svg.classed("selecting", true); } function brushMove() { var s = brush.extent(); updateDateRangeDisplay($wnd.moment(s[0]), $wnd.moment(s[1])); } function brushEnd() { var s = brush.extent(); var startTime = Math.round(s[0].getTime()); var endTime = Math.round(s[1].getTime() ); svg.classed("selecting", !$wnd.d3.event.target.empty()); // ignore selections less than 1 minute if(endTime - startTime >= 60000){ global.@org.rhq.coregui.client.inventory.common.graph.AbstractMetricGraph::dragSelectionRefresh(DD)(startTime, endTime); } } function updateDateRangeDisplay(startDate, endDate ) { var formattedDateRange = startDate.format(chartContext.buttonBarDateTimeFormat) + ' - ' + endDate.format(chartContext.buttonBarDateTimeFormat); var timeRange = endDate.from(startDate,true); $wnd.jQuery('.graphDateTimeRangeLabel').text(formattedDateRange+'('+timeRange+')'); } } return { // Public API draw: function (chartContext) { // Guard condition that can occur when a portlet has not been configured yet if (chartContext.data.length > 0) { //console.log("Creating Chart: "+ chartContext.chartSelection + " --> "+ chartContext.chartTitle); determineScale(); createHeader(chartContext.chartTitle); createYAxisGridLines(); if(!chartContext.isPortalGraph){ createXAxisBrush(); } createStackedBars(); createXandYAxes(); createAvgLines(); if (oobMax > 0) { //console.log("OOB Data Exists!"); createOOBLines(); } } } }; // end public closure }(); if(typeof chartContext.data !== 'undefined' && chartContext.data !== null && chartContext.data.length > 0){ metricStackedBarGraph.draw(chartContext); } }-*/; }