window.Vrs = function($, vrs) { vrs.Classes.Base = function(config) { if (!config.yLabelsFormat && config.yValueFormat) { config.yLabelsFormat = config.yValueFormat; } $.extend(this, config); this.options = $.extend(true, {}, this.options); if (!this.validateConfig(config)) throw "Error in configuration"; this.setYValueFormat(); }; vrs.Classes.Base.prototype = { clearSeriesData: function() { this.setNoDataCase(true); }, setSeriesData: function(data) { if (this.chart == undefined) { throw "setSeriesData() called on a uninitialized chart"; } if(this.hasAllData(data)) { this.setNoDataCase(false); this.data = data; this.computeTickIntervals(this.getXLengthInUnits(data)); this.loadSeriesDataIntoChart(data); this.hideLoading(); if ($(this.chart.container).is(":visible")) { this.drawChart(); if(this.chart.chartWidth > $(this.chart.container).parent().width() || this.chart.chartHeight > $(this.chart.container).parent().height()) this.reflow(); } else { this.isDirty = true; } // this triggers setCursorAt(currentSliderOffset) $("#" + this.chart.options.chart.renderTo).trigger("vrs.ChartVisible"); } else {//no data case this.setNoDataCase(true); } }, getSeriesDataLength: function() { return this.data[this.nameForUnitSystem(this.xDataName)].length; }, computeTickIntervals: function(lengthUnits) { if(!lengthUnits || !isFinite(lengthUnits) || lengthUnits <= 0) { //disable ticks for malformed data, they tend to mess up and slow down the chart rendering this.tickUnitInterval = 0; } else if(lengthUnits < 100) { this.tickUnitInterval = 5; } else if(lengthUnits < 200) { this.tickUnitInterval = 10; } else if(lengthUnits < 500) { this.tickUnitInterval = 25; } else if(lengthUnits < 1000) { this.tickUnitInterval = 50; } else if(lengthUnits < 2000) { this.tickUnitInterval = 200; } else if(lengthUnits < 5000) { this.tickUnitInterval = 500; } else { this.tickUnitInterval = 1000; } if (lengthUnits / this.tickUnitInterval > this.chart.chartWitdh) { //disabled ticks if chart isn't large enough to fit them this.tickUnitInterval = 0; } this.minorTickUnitInterval = this.tickUnitInterval / 10; }, getXLengthInUnits: function(data) { var elementsLength = data[0][this.nameForUnitSystem(this.xDataName)].length; var lastValue = data[0][this.nameForUnitSystem(this.xDataName)][elementsLength - 1]; var firstValue = data[0][this.nameForUnitSystem(this.xDataName)][0]; return Math.abs(lastValue - firstValue); }, hasAllData: function(data) { return data[0][this.nameForUnitSystem(this.xDataName)] && data[0][this.nameForUnitSystem(this.yDataName)] && data[1][this.nameForUnitSystem(this.xDataName)] && data[1][this.nameForUnitSystem(this.yDataName)]; }, setNoDataCase: function(isInNoDataCase) { this.hasData = !isInNoDataCase; if(isInNoDataCase) { this.chart.options.loading.style["opacity"] = 1; //no data opacity for loading; this.chart.showLoading("No data to show for this chart"); } else { this.chart.options.loading.style.opacity = 0.5; //default opacity for loading; } }, drawChart: function() { this.redraw(); this.isDirty = false; }, loadSeriesDataIntoChart: function(data) { throw "Not implemented"; }, removeAllAOIs: function() { //need to remove explicitly all, having the same id seems the easiest way as mentioned here: //https://github.com/highslide-software/highcharts.com/issues/198 this.chart.xAxis[0].removePlotBand("plotband"); }, markAOIs: function(aoisData) { //TODO: the field names should probably arrive through config? var starts = aoisData["aoiStartInds"]; var ends = aoisData["aoiEndInds"]; if(!starts || !ends) { return; } var brakeAoi = aoisData["BrakeAoi"]; var throttleAoi = aoisData["ThrottleAoi"]; this.removeAllAOIs(); for(var i = 0; i < starts.length; i++) { var importantMetric; if (brakeAoi && brakeAoi.indexOf(i) != -1) { importantMetric = "brake"; } else if(throttleAoi && throttleAoi.indexOf(i) != -1) { importantMetric = "throttle"; } else { //Disabling the undetermined viz - leaving in the code, as it is used for debugging //importantMetric = "undetermined"; } //only mark AOI if the chart shows the AOI's metric if(this.showsMetric(importantMetric)) { this.markAOI({ associatedMetric: importantMetric, associatedOffset: this.data[0][this.nameForUnitSystem(this.xDataName)][starts[i]], start: this.data[0][this.nameForUnitSystem(this.xDataName)][starts[i]], end: this.data[0][this.nameForUnitSystem(this.xDataName)][ends[i]], // The start and end above use the current user's unit system (nameForUnitSystem converts from // the default channel name to the current user unit system channel name). However, the distance // should be calculated in the default unit system, because when displayed it will be formatted // to match the user settings (if we compute the distance in the user settings system, we will // have a double conversion) distanceInMetric: this.data[0][this.xDataName][ends[i]] - this.data[0][this.xDataName][starts[i]] }); } } }, markAOI: function(aoiPlotbandInfo) { //NOTE: when updating these, make sure to sync up with AoiMarker.java, method getMetricRelatedColorHex var colorMap = { "brake": "rgba(255,208,207, 0.6)", "throttle": "rgba(198,228,207, 0.6)", "undetermined": "rgba(209,215,218, 0.6)" } var plotbandColor = colorMap[aoiPlotbandInfo.associatedMetric]; this.chart.xAxis[0].addPlotBand({ //custom field, for marking what chart should open on plotband click associatedMetric: aoiPlotbandInfo.associatedMetric, associatedOffset: aoiPlotbandInfo.associatedOffset, color: plotbandColor, from: aoiPlotbandInfo.start, to: aoiPlotbandInfo.end, label: { text: this.formatter(this.xValueFormat, aoiPlotbandInfo.distanceInMetric), align: "center", verticalAlign: "top", y: +20, style: { color: "#212121" } }, id: "plotband" }); var that = this; setTimeout(function() { that.plotBandLabelsToFront(); }, 0); }, plotBandLabelsToFront: function() { for (var i in this.chart.xAxis[0].plotLinesAndBands) { var plotband = this.chart.xAxis[0].plotLinesAndBands[i]; if (plotband.id == "plotband" && plotband.label) { plotband.label.toFront(); } } }, showsMetric: function(metric) { if (metric) { metric = metric.toLowerCase(); for (var i = 0; i < this.yDataNames.length; i++) { if (this.yDataNames[i] && (this.yDataNames[i].toLowerCase() === metric)) { return true; } } } return false; }, setCursorAt: function(x) { this.refreshTooltipAt(x); this.setCursorLineAt(x); }, getXAxisFormatString: function() { return "{value}" + this.getUnits(this.xValueFormat); }, prepareTickIntervalsForRedraw: function() { if(this.hasData) { this.chart.xAxis[0].options.labels.format = this.getXAxisFormatString(); this.chart.xAxis[0].options.tickInterval = this.tickUnitInterval; this.chart.xAxis[0].options.minorTickInterval = this.minorTickUnitInterval; this.chart.xAxis[0].isDirty = true; if(this.yAxisMinorTickInterval) { this.chart.yAxis[0].options.minorTickInterval = this.yAxisMinorTickInterval; this.chart.yAxis[0].isDirty = true; } if(this.yAxisMajorTickInterval) { this.chart.yAxis[0].options.tickInterval = this.yAxisMajorTickInterval this.chart.yAxis[0].isDirty = true; } if(this.yAxisMinorTicksPerMajorTick) { this.chart.yAxis[0].options.minorTickInterval = this.chart.yAxis[0].options.tickInterval / this.yAxisMinorTicksPerMajorTick; this.chart.yAxis[0].isDirty = true; } } }, handleClick: function(event) { window.fireSliderMovedEvent(this.findClosestDataPointIndFromClick(event)); }, findClosestDataPointIndFromClick: function(event) { var xValue = event.xAxis[0].value; //Note: data should be sorted so this could probably be optimized a bit, but this feels //more like a standard algorithm, which reduces code complexity. It's linear either way var closestDataPointInd = 0; var closestDataPointDist = Number.MAX_SAFE_INTEGER; for(var i = 0; i < this.chart.series[0].xData.length; i++) { var dist = Math.abs(xValue - this.chart.series[0].xData[i]); if(dist < closestDataPointDist) { closestDataPointDist = dist; closestDataPointInd = i; } } return closestDataPointInd; }, load: function(divId) { var self = this; this.chart = new Highcharts.Chart($.extend(true, {}, this.options, { chart: { renderTo: divId, reflow: false, events: { click: function(event) { self.handleClick(event); } } } })); if(this.staticYPlotLines) { for(var i = 0; i < this.staticYPlotLines.length; i++) { this.chart.yAxis[0].addPlotLine(this.staticYPlotLines[i]); } } //This mimics the default reflow() behaviour, but allows automatic reflow to happen even after setSize() is done //Got it from here: http://stackoverflow.com/questions/31754511/highcharts-how-to-use-reflow-to-allow-auto-resize-after-changing-size this.chart.reflowNow = function(){ this.containerHeight = this.options.chart.height || window.window.HighchartsAdapter.adapterRun(this.renderTo, 'height'); this.containerWidth = this.options.chart.width || window.window.HighchartsAdapter.adapterRun(this.renderTo, 'width'); this.setSize(this.containerWidth, this.containerHeight, false); this.hasUserSize = null; }; this.chart.chartClass = this; $("#" + divId).addClass(this.type); }, prepareReflow: function() { if(this.chart && this.chart.setSize) { this.chart.setSize(0, this.chart.containerHeight || 0, false); } }, reflow: function() { if (this.isDirty) { this.drawChart(); } this.chart.reflowNow(); // This is a hack to get plotband label on top of everything else. Unfortunately we cannot call // label.toFront() when the plotband is initialized because the chart may not be rendered (it // may be collapsed). So when we actually draw the chart, we need to make sure that we call // label.toFront(). this.plotBandLabelsToFront(); }, reflowToSquare: function() { var container = $(this.chart.container); var renderTo = $(this.chart.renderTo); if (container.width() != container.height() || container.width() != renderTo.width()) { var width = renderTo.width(); this.chart.setSize(width, width, false); } vrs.Classes.Base.prototype.reflow.call(this); }, refreshTooltipAt: function(index) { var rawPoints = _.map(this.getAllSeries(), function(series) { return {x: series.xData[index], y: series.yData[index]}; }); this.setCurrentTooltipValues(rawPoints); }, setCurrentTooltipValues: function(values) { this.currentTooltipValues = this.getTooltipValues(values); }, setCursorLineAt: function(index) { if (!this.hasData) return; this.addOrUpdatePlotLine(this.chart.xAxis[0], this.getVisibleSeries()[0].xData, "cursor", index, this.options.xAxis.plotLines[0].color, this.options.xAxis.plotLines[0].width, this.options.xAxis.plotLines[0].zIndex) }, addOrUpdateHorizontalPlotLine: function(name, index, color, width, zIndex) { this.addOrUpdatePlotLine(this.chart.yAxis[0], this.getVisibleSeries()[0].yData, name, index, color, width, zIndex); }, addOrUpdatePlotLine: function(axis, data, name, index, color, width, zIndex) { axis.removePlotLine(name); axis.addPlotLine({ value: data[index], color: color, width: width, zIndex: zIndex, id: name }); }, destroy: function() { this.chart.destroy(); }, showLoading: function() { this.chart.options.loading.style.opacity = 0.5; this.chart.showLoading("Loading..."); }, hideLoading: function() { this.chart.hideLoading(); }, redraw: function() { this.prepareTickIntervalsForRedraw(); this.chart.redraw(); }, setYValueFormat: function() { var self = this; if (this.yValueFormat != undefined) { this.options.yAxis = _.map(this.options.yAxis, function(axis){ return $.extend(true,axis,{ labels: { formatter: function() { return self.formatter(self.yLabelsFormat, this.value); } } }); }); } }, getTooltipNames: function() { return _.map(this.getAllSeries(), function(series){ return series.name; }); }, getTooltipColors: function() { return _.map(this.getAllSeries(), function(series){ return series.options.color; }); }, getCurrentTooltipValues: function() { return this.currentTooltipValues; }, getTooltipValues: function(rawPoints) { var points = []; for (var i = 0; i < rawPoints.length; ++i) { if(this.hasData) { var format = this.yValueFormat; if (this.yValueFormats && this.yValueFormats[i]) format = this.yValueFormats[i]; points.push(this.formatter(format, rawPoints[i].y)); } else { points.push("ΓΈ"); } } return points; }, getDisplayModes: function() { // by default there is no support for multiple display modes, override in derived classes return []; }, getDisplayMode: function() { return null; // no mode }, setDisplayMode: function(mode) { // nop, override in derived classes }, validateConfig: function(config) { return _.every(this.requiredProperties(), function(prop) { return config.hasOwnProperty(prop); }); }, scaleToSquare: function() { var chart = this.chart; var self = this; var extremeFinder = function(type, field) { return _.chain(self.getVisibleSeries()).map(function(series) { return _[type](series[field]); })[type]().value(); }; var xMin = extremeFinder("min", "xData"); var xMax = extremeFinder("max", "xData"); var yMin = extremeFinder("min", "yData"); var yMax = extremeFinder("max", "yData"); var xRange = xMax - xMin; var yRange = yMax - yMin; var maxRange = Math.max(xRange, yRange) * 1.1; // add 10% buffer if(Math.abs(xRange - yRange) < maxRange/10000) // pretty square already, we don't want to shrink return; // multiple times on attach/detach var xCenter = (xMin + xMax) / 2 var yCenter = (yMin + yMax) / 2 var widthToHeight = this.chart.chartWidth / this.chart.chartHeight; halfMaxRange = maxRange / 2; chart.xAxis[0].setExtremes(xCenter + (-halfMaxRange * widthToHeight), xCenter + (halfMaxRange * widthToHeight), false, false); chart.yAxis[0].setExtremes(yCenter + (-halfMaxRange), yCenter + (halfMaxRange), false, false) }, getAllSeries: function() { return this.chart.series.slice(0); }, getVisibleSeries: function() { // assumption here is that visible series have data and should not be ignored return _.filter(this.chart.series, function(series) { return series.visible; }); }, requiredProperties: function() { return ["xDataName", "yDataName", "yValueFormat", "title", "description"]; }, options: { colors: ['#058DC7', '#50B432', '#ED561B', '#DDDF00', '#24CBE5', '#64E572', '#FF9655', '#FFF263', '#6AF9C4'], chart: { backgroundColor: 'rgba(0, 0, 0, 0)', borderWidth: 0, plotBackgroundColor: 'rgba(0, 0, 0, 0)', plotShadow: false, plotBorderWidth: 0, margin: [10, 0, 10, 45], height: 112 // Height of the chart }, title: { text: '', style: { color: '#646464' } }, subtitle: { text: '', style: { color: '#646464' } }, xAxis: { tickWidth: 1, tickLength: 11, tickColor: '#c0c0c0', minorTickLength: 3, minorTickWidth: 1, lineWidth: 1, lineColor: '#c0c0c0', gridLineWidth: 0, gridLineColor: '#c0c0c0', minorGridLineWidth: 0, minorGridLineColor: '#e0e0e0', labels: { align: 'left', x: 1, y: 11, //TODO: 1 or 2? Depends on specific values we decide to use for #993, but it is //less visually confusing what minorTickInterval is compared to tickInterval when the labels are on //every tick (e.g. easier to count 10 minor ticks between two labeled major ticks, than 20 minor //ticks spanning a total of 3 ticks, middle of which is not labeled) step: 1, style: { color: '#646464', fontSize: '9px', fontFamily: 'Arial, Helvetica, sans-serif' }, staggerLines: 1 }, title: { style: { text: '', color: '#333', fontWeight: 'bold', fontSize: '12px', fontFamily: 'Arial, sans-serif' } }, plotLines: [ { color: '#2979ff', dashStyle: 'line', width: '2', zIndex: 100 } ] }, yAxis: [{ startOnTick: false, endOnTick: false, gridLineWidth: 1, minorTickInterval: null, tickPixelInterval: 20, lineColor: '#646464', lineWidth: 0, tickWidth: 0, tickColor: '#646464', labels: { style: { color: '#646464', font: '11px Arial, Helvetica, sans-serif' } }, title: { text: '', style: { color: '#333', fontWeight: 'bold', fontSize: '12px', fontFamily: 'Arial, sans-serif' } } }], legend: { enabled: false, itemStyle: { font: '9pt Arial, sans-serif', color: 'black' }, itemHoverStyle: { color: '#039' }, itemHiddenStyle: { color: 'gray' } }, labels: { style: { color: '#99b' } }, exporting: { enabled: false }, credits: { enabled: false }, plotOptions: { series: { animation: false, lineWidth: 1, marker: { enabled: false }, states: { hover: { enabled: false } }, shadow: false, color: '#4572A7', enableMouseTracking: false, turboThreshold: 10000 } }, tooltip: { enabled: false } } }; return vrs; }($, Vrs);