UNPKG

splunk-sdk

Version:

SDK for usage with the Splunk REST API

1,291 lines (1,162 loc) 324 kB
// Copyright 2011 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. (function() { var Splunk = require('./splunk'); var i18n = require('./i18n'); var Highcharts = require('./highcharts').Highcharts; require('./util'); require('./lowpro_for_jquery'); var format_decimal = i18n.format_decimal; var format_percent = i18n.format_percent; var format_scientific = i18n.format_scientific; var format_date = i18n.format_date; var format_datetime = i18n.format_datetime; var format_time = i18n.format_time; var format_datetime_microseconds = i18n.format_datetime_microseconds; var format_time_microseconds = i18n.format_time_microseconds; var format_datetime_range = i18n.format_datetime_range; var locale_name = i18n.locale_name; var locale_uses_day_before_month = i18n.locale_uses_day_before_month; exports.Splunk = Splunk; //////////////////////////////////////////////////////////////////////// // Splunk.JSCharting // // Adding some basic methods/fields to the JSCharting namespace for creating charts // and manipulating data from splunkd Splunk.JSCharting = { // this is copied from the Highcharts source, line 38 hasSVG: !!document.createElementNS && !!document.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGRect, createChart: function(container, properties) { // this is a punt to verify that container is a valid dom element // not an exhaustive check, but verifies the existence of the first // methods HC will call in an attempt to catch the problem here if(!container.appendChild || !container.cloneNode) { throw new Error("Invalid argument to createChart, container must be a valid DOM element"); } var getConstructorByType = function(chartType) { switch(chartType) { case 'line': return Splunk.JSCharting.LineChart; case 'area': return Splunk.JSCharting.AreaChart; case 'column': return Splunk.JSCharting.ColumnChart; case 'bar': return Splunk.JSCharting.BarChart; case 'pie': return Splunk.JSCharting.PieChart; case 'scatter': return Splunk.JSCharting.ScatterChart; case 'hybrid': return Splunk.JSCharting.HybridChart; case 'radialGauge': return Splunk.JSCharting.RadialGauge; case 'fillerGauge': return (properties['chart.orientation'] === 'x') ? Splunk.JSCharting.HorizontalFillerGauge : Splunk.JSCharting.VerticalFillerGauge; case 'markerGauge': return (properties['chart.orientation'] === 'x') ? Splunk.JSCharting.HorizontalMarkerGauge : Splunk.JSCharting.VerticalMarkerGauge; default: return Splunk.JSCharting.ColumnChart; } }, chartConstructor = getConstructorByType(properties.chart); // split series only applies to bar/column/line/area charts if(properties['layout.splitSeries'] === 'true' && (!properties.chart || properties.chart in {bar: true, column: true, line: true, area: true})) { return new Splunk.JSCharting.SplitSeriesChart(container, chartConstructor); } return new chartConstructor(container); }, extractFieldInfo: function(rawData) { if(!rawData || !rawData.columns) { return { fieldNames: [] }; } var i, loopField, xAxisKey, xAxisSeriesIndex, spanSeriesIndex, xAxisKeyFound = false, isTimeData = false, fieldNames = []; // SPL-56805, check for the _time field, if it's there use it as the x-axis field var _timeIndex = $.inArray('_time', rawData.fields); if(_timeIndex > -1) { xAxisKey = '_time'; xAxisSeriesIndex = _timeIndex; xAxisKeyFound = true; } for(i = 0; i < rawData.columns.length; i++) { loopField = rawData.fields[i]; if(loopField == '_span') { spanSeriesIndex = i; continue; } if(loopField.charAt(0) == '_' && loopField != "_time") { continue; } if(!xAxisKeyFound) { xAxisKey = loopField; xAxisSeriesIndex = i; xAxisKeyFound = true; } if(xAxisKey && loopField !== xAxisKey) { fieldNames.push(loopField); } } if(xAxisKey === '_time' && ($.inArray('_span', rawData.fields) > -1 || rawData.columns[xAxisSeriesIndex].length === 1)) { // we only treat the data as time data if it has been discretized by the back end // (indicated by the existence of a '_span' field) isTimeData = true; } return { fieldNames: fieldNames, xAxisKey: xAxisKey, xAxisSeriesIndex: xAxisSeriesIndex, spanSeriesIndex: spanSeriesIndex, isTimeData: isTimeData }; }, extractChartReadyData: function(rawData, fieldInfo) { if(!rawData || !rawData.columns) { return false; } var i, j, xAxisKey = fieldInfo.xAxisKey, xAxisSeriesIndex = fieldInfo.xAxisSeriesIndex, xSeries = rawData.columns[xAxisSeriesIndex], _spanSeries, xAxisType, categories, loopSeries, loopYVal, loopDataPoint, series = {}; if(xAxisKey === '_time' && ($.inArray('_span', rawData.fields) > -1 || xSeries.length === 1)) { xAxisType = "time"; for(i = 0; i < rawData.columns.length; i++) { if(rawData.fields[i] === '_span') { _spanSeries = rawData.columns[i]; break; } } } else { xAxisType = "category"; categories = $.extend(true, [], xSeries); } // extract the data for(i = 0; i < rawData.columns.length; i++) { loopSeries = rawData.columns[i]; series[rawData.fields[i]] = []; for(j = 0; j < loopSeries.length; j++) { loopYVal = this.MathUtils.parseFloat(loopSeries[j]); loopDataPoint = { name: xSeries[j], y: loopYVal, rawY: loopYVal }; if(xAxisType === "time" && _spanSeries) { loopDataPoint._span = _spanSeries[j]; } series[rawData.fields[i]].push(loopDataPoint); } } return { series: series, fieldNames: fieldInfo.fieldNames, xAxisKey: fieldInfo.xAxisKey, xAxisType: xAxisType, categories: categories, xSeries: xSeries, _spanSeries: _spanSeries }; } }; //////////////////////////////////////////////////////////////////////////////// // Splunk.JSCharting.AbstractVisualization Splunk.JSCharting.AbstractVisualization = $.klass({ hasSVG: Splunk.JSCharting.hasSVG, initialize: function(container) { // some shortcuts to the util packages this.mathUtils = Splunk.JSCharting.MathUtils; this.parseUtils = Splunk.JSCharting.ParsingUtils; this.colorUtils = Splunk.JSCharting.ColorUtils; this.Throttler = Splunk.JSCharting.Throttler; this.eventMap = {}; this.renderTo = container; this.chartWidth = $(this.renderTo).width(); this.chartHeight = $(this.renderTo).height(); this.backgroundColor = "#ffffff"; this.foregroundColor = "#000000"; this.fontColor = "#000000"; this.testMode = false; this.exportMode = false; }, applyProperties: function(properties) { for(var key in properties) { if(properties.hasOwnProperty(key)) { this.applyPropertyByName(key, properties[key], properties); } } this.performPropertyCleanup(); }, applyPropertyByName: function(key, value, properties) { switch(key) { case 'backgroundColor': this.backgroundColor = value; break; case 'foregroundColor': this.foregroundColor = value; break; case 'fontColor': this.fontColor = value; break; case 'testMode': this.testMode = (value === true); break; case 'exportMode': if(value === "true") { this.exportMode = true; this.setExportDimensions(); } break; default: // no-op, ignore unrecognized properties break; } }, performPropertyCleanup: function() { this.foregroundColorSoft = this.colorUtils.addAlphaToColor(this.foregroundColor, 0.25); this.foregroundColorSofter = this.colorUtils.addAlphaToColor(this.foregroundColor, 0.15); }, addEventListener: function(type, callback) { if(this.eventMap[type]) { this.eventMap[type].push(callback); } else { this.eventMap[type] = [callback]; } }, removeEventListener: function(type, callback) { if(this.eventMap[type] == undefined) { return; } var index = $.inArray(callback, this.eventMap[type]); if(this.eventMap[type][index]) { this.eventMap[type].splice(index, 1); } }, dispatchEvent: function(type, event) { event = event || {}; if(this.eventMap[type]) { for(var i in this.eventMap[type]) { this.eventMap[type][i](event); } } }, // TODO: this should be migrated to another object, formatting helper maybe? addClassToElement: function(elem, className) { // the className can potentially come from the search results, so make sure it is valid before // attempting to insert it... // if the className doesn't start with a letter or a '-' followed by a letter, don't insert if(!/^[-]?[A-Za-z]/.test(className)) { return; } // now filter out anything that is not a letter, number, '-', or '_' className = className.replace(/[^A-Za-z0-9_-]/g, ""); if(this.hasSVG) { if(elem.className.baseVal) { elem.className.baseVal += " " + className; } else { elem.className.baseVal = className; } } else { $(elem).addClass(className); } } }); //////////////////////////////////////////////////////////////////////////////// // Splunk.JSCharting.AbstractChart Splunk.JSCharting.AbstractChart = $.klass(Splunk.JSCharting.AbstractVisualization, { axesAreInverted: false, HOVER_TIMER: 25, focusedElementOpacity: 1, fadedElementOpacity: 0.3, fadedElementColor: "rgba(150, 150, 150, 0.3)", // override initialize: function($super, container) { $super(container); this.needsLegendMapping = true; this.hcChart = false; this.chartIsDrawing = false; this.chartIsStale = false; this.processedData = false; this.pendingData = false; this.pendingColors = false; this.pendingCallback = false; this.customConfig = false; this.chartIsEmpty = false; this.logYAxis = false; this.legendMaxWidth = 300; this.legendEllipsizeMode = 'ellipsisMiddle'; this.tooMuchData = false; this.fieldListMode = "hide_show"; this.fieldHideList = []; this.fieldShowList = []; this.legendLabels = []; this.colorPalette = new Splunk.JSCharting.ListColorPalette(); }, prepare: function(data, fieldInfo, properties) { this.properties = properties; this.generateDefaultConfig(); this.addRenderHooks(); this.applyProperties(properties); this.processData(data, fieldInfo, properties); if(this.chartIsEmpty) { this.configureEmptyChart(); } else { this.applyFormatting(properties, this.processedData); this.addEventHandlers(properties); if(this.customConfig) { $.extend(true, this.hcConfig, this.customConfig); } } }, getFieldList: function() { if(this.chartIsEmpty) { return []; } // response needs to be adjusted if the user has explicitly defined legend label list if(this.legendLabels.length > 0) { var adjustedList = $.extend(true, [], this.legendLabels); for(var i = 0; i < this.processedData.fieldNames.length; i++) { var name = this.processedData.fieldNames[i]; if($.inArray(name, adjustedList) === -1) { adjustedList.push(name); } } return adjustedList; } return this.processedData.fieldNames; }, setColorMapping: function(list, map, legendSize) { var i, color, newColors = []; for(i = 0; i < list.length; i++) { color = this.colorPalette.getColor(list[i], map[list[i]], legendSize); newColors.push(this.colorUtils.addAlphaToColor(color, this.focusedElementOpacity)); } this.hcConfig.colors = newColors; }, setColorList: function(list) { var i, newColors = []; for(i = 0; i < list.length; i++) { newColors.push(this.colorUtils.addAlphaToColor(list[i], this.focusedElementOpacity)); } this.hcConfig.colors = newColors; }, draw: function(callback) { if(this.chartIsDrawing) { this.chartIsStale = true; this.pendingCallback = callback; return; } this.chartIsDrawing = true; if(this.hcChart) { this.destroy(); } // SPL-49962: have to make sure there are as many colors as series, or HighCharts will add random colors if(this.hcConfig.series.length > this.hcConfig.colors.length) { var numInitialColors = this.hcConfig.colors.length; for(var i = numInitialColors; i < this.hcConfig.series.length; i++) { this.hcConfig.colors.push(this.hcConfig.colors[i % numInitialColors]); } } this.hcChart = new Highcharts.Chart(this.hcConfig, function(chart) { if(this.chartIsStale) { // if new data came in while the chart was rendering, re-draw immediately this.chartIsStale = false; this.draw(this.pendingCallback); } else { if(!this.chartIsEmpty) { this.onDrawFinished(chart, callback); } // SPL-53261 revealed that the chartIsDrawing flag was not being unset in the case of an empty chart // SPL-48515 and SPL-56383 revealed that the callback was not firing in the case of an empty chart else { this.chartIsDrawing = false; callback(chart); } } }.bind(this)); }, setData: function(data, fieldInfo) { clearTimeout(this.drawTimeout); this.prepare(data, fieldInfo, this.properties); }, resize: function(width, height) { this.chartWidth = width; this.chartHeight = height; if(this.hcChart) { this.hcChart.setSize(width, height, false); // need to update the chart options or the stale value will be used this.hcChart.options.chart.height = height; } }, destroy: function() { if(this.hcChart) { clearTimeout(this.drawTimeout); this.removeLegendHoverEffects(); this.hcChart.destroy(); this.hcChart = false; } }, // a way to set custom config options on an instance specific basis, // will be applied after all other configurations setCustomConfig: function(config) { this.customConfig = config; }, highlightIndexInLegend: function(index) { this.highlightSeriesInLegend(this.hcChart.series[index]); }, unHighlightIndexInLegend: function(index) { this.unHighlightSeriesInLegend(this.hcChart.series[index]); }, getChartObject: function() { return this.hcChart; }, /////////////////////////////////////////////////////////////////////////// // end of "public" interface generateDefaultConfig: function() { this.hcConfig = $.extend(true, {}, Splunk.JSCharting.DEFAULT_HC_CONFIG, { chart: { renderTo: this.renderTo, height: this.chartHeight, className: this.typeName } }); this.mapper = new Splunk.JSCharting.PropertyMapper(this.hcConfig); this.setColorList(Splunk.JSCharting.ListColorPalette.DEFAULT_COLORS); }, addRenderHooks: function() { $.extend(true, this.hcConfig, { legend: { hooks: { placementHook: this.legendPlacementHook.bind(this), labelRenderHook: this.legendLabelRenderHook.bind(this) } } }); }, applyFormatting: function(properties, data) { this.formatXAxis(properties, data); this.formatYAxis(properties, data); this.formatTooltip(properties, data); this.formatLegend(); }, addEventHandlers: function(properties) { this.addClickHandlers(); this.addHoverHandlers(); this.addLegendHandlers(properties); this.addRedrawHandlers(); }, processData: function(rawData, fieldInfo, properties) { this.processedData = rawData; if(!this.processedData || this.processedData.fieldNames.length === 0) { this.chartIsEmpty = true; } else { this.chartIsEmpty = false; this.addDataToConfig(); } }, onDrawFinished: function(chart, callback) { // SPL-48560: in export mode we need to explicitly close all paths to ensure the fill attr is respected if(this.exportMode && chart.options.chart.type === 'area'){ $.each(chart.series, function(i,series){ var d = series.area.attr('d'); if(!(d.indexOf('Z') >-1)){ series.area.attr({ 'd': d + ' Z' }); } }); } if(this.hcConfig.legend.enabled) { this.addLegendHoverEffects(chart); // SPL-47508: in export mode we have to do a little magic to make the legend symbols align and not overlap if(this.exportMode && chart.options.chart.type !== 'scatter') { $(chart.series).each(function(i, loopSeries) { if(!loopSeries.legendSymbol) { return false; } loopSeries.legendSymbol.attr({ height: 8, translateY: 4 }); }); } } if(this.testMode) { this.addTestingMetadata(chart); } this.onDrawOrResize(chart); this.chartIsDrawing = false; this.hcObjectId = chart.container.id; if(callback) { callback(chart); } }, configureEmptyChart: function() { $.extend(true, this.hcConfig, { yAxis: { tickColor: this.foregroundColorSoft, lineWidth: 1, lineColor: this.foregroundColorSoft, gridLineColor: this.foregroundColorSofter, tickWidth: 1, tickLength: 25, showFirstLabel: false, min: 0, max: (this.logYAxis) ? 2 : 100, tickInterval: (this.logYAxis) ? 1 : 10, labels: { style: { color: this.fontColor }, y: 15, formatter: (this.logYAxis) ? function() { return Math.pow(10, this.value); } : function() { return this.value; } }, title: { text: null } }, xAxis: { lineColor: this.foregroundColorSoft }, legend: { enabled: false }, series: [ { data: [], visible: false, showInLegend: false } ] }); }, //////////////////////////////////////////////////////////////////////////// // helper methods for managing chart properties // override applyPropertyByName: function($super, key, value, properties) { $super(key, value, properties); switch(key) { case 'chart.stackMode': this.mapStackMode(value, properties); break; case 'legend.placement': this.mapLegendPlacement(value); break; case 'chart.nullValueMode': if(value === 'connect') { this.mapper.mapValue(true, ["plotOptions", "series", "connectNulls"]); } // the distinction between omit and zero is handled by the // extractProcessedData method break; case 'secondaryAxis.scale': if(!properties['axisY.scale']) { this.logYAxis = (value === 'log'); } break; case 'axisY.scale': this.logYAxis = (value === 'log'); break; case "enableChartClick": this.enableChartClick = value; break; case "enableLegendClick": this.enableLegendClick = value; break; case 'legend.labelStyle.overflowMode': this.legendEllipsizeMode = value; break; case 'legend.masterLegend': // at this point in the partial implementation, the fact that legend.masterLegend is set means // that it has been explicitly disabled this.needsLegendMapping = false; break; case 'legend.labels': this.legendLabels = this.parseUtils.stringToArray(value) || []; break; case 'seriesColors': var hexArray = this.parseUtils.stringToHexArray(value); if(hexArray && hexArray.length > 0) { this.colorPalette = new Splunk.JSCharting.ListColorPalette(hexArray); this.setColorList(hexArray); } break; case 'data.fieldListMode': this.fieldListMode = value; break; case 'data.fieldHideList': this.fieldHideList = Splunk.util.stringToFieldList(value) || []; break; case 'data.fieldShowList': this.fieldShowList = Splunk.util.stringToFieldList(value) || []; break; default: // no-op, ignore unsupported properties break; } }, // override // this method's purpose is to post-process the properties and resolve any that are interdependent performPropertyCleanup: function($super) { $super(); $.extend(true, this.hcConfig, { chart: { backgroundColor: this.backgroundColor, borderColor: this.backgroundColor }, legend: { itemStyle: { color: this.fontColor }, itemHoverStyle: { color: this.fontColor } }, tooltip: { borderColor: this.foregroundColorSoft } }); if(this.exportMode) { $.extend(true, this.hcConfig, { plotOptions: { series: { enableMouseTracking: false, shadow: false } } }); } }, mapStackMode: function(name, properties) { if(properties['layout.splitSeries'] == 'true') { name = 'default'; } var translation = { "default": null, "stacked": "normal", "stacked100": "percent" }; this.mapper.mapValue(translation[name], ["plotOptions", "series", "stacking"]); }, mapLegendPlacement: function(name) { if(name in {left: 1, right: 1}) { this.mapper.mapObject({ legend: { enabled: true, verticalAlign: 'middle', align: name, layout: 'vertical', x: 0 } }); } else if(name in {bottom: 1, top: 1}) { this.mapper.mapObject({ legend: { enabled: true, verticalAlign: name, align: 'center', layout: 'horizontal', margin: 15, y: (name == 'bottom') ? -5 : 0 } }); } else { this.mapper.mapObject({ legend: { enabled: false } }); } }, setExportDimensions: function() { this.chartWidth = 600; this.chartHeight = 400; this.mapper.mapObject({ chart: { width: 600, height: 400 } }); }, //////////////////////////////////////////////////////////////////////////// // helper methods for handling label and axis formatting formatXAxis: function(properties, data) { var axisType = data.xAxisType, axisProperties = this.parseUtils.getXAxisProperties(properties), orientation = (this.axesAreInverted) ? 'vertical' : 'horizontal', colorScheme = this.getAxisColorScheme(); // add some extra info to the axisProperties as needed axisProperties.chartType = properties.chart; axisProperties.axisLength = $(this.renderTo).width(); if(axisProperties['axisTitle.text']){ axisProperties['axisTitle.text'] = Splunk.JSCharting.ParsingUtils.escapeHtml(axisProperties['axisTitle.text']); } switch(axisType) { case 'category': this.xAxis = new Splunk.JSCharting.CategoryAxis(axisProperties, data, orientation, colorScheme); break; case 'time': this.xAxis = new Splunk.JSCharting.TimeAxis(axisProperties, data, orientation, colorScheme, this.exportMode); break; default: // assumes a numeric axis this.xAxis = new Splunk.JSCharting.NumericAxis(axisProperties, data, orientation, colorScheme); break; } this.hcConfig.xAxis = this.xAxis.getConfig(); if(this.exportMode && (axisType === 'time')) { var xAxisMargin, spanSeries = data._spanSeries, span = (spanSeries && spanSeries.length > 0) ? parseInt(spanSeries[0], 10) : 1, secsPerDay = 60 * 60 * 24, secsPerYear = secsPerDay * 365; if(span >= secsPerYear) { xAxisMargin = 15; } else if(span >= secsPerDay) { xAxisMargin = 25; } else { xAxisMargin = 35; } this.hcConfig.xAxis.title.margin = xAxisMargin; } if(typeof this.hcConfig.xAxis.title.text === 'undefined') { this.hcConfig.xAxis.title.text = this.processedData.xAxisKey; } }, formatYAxis: function(properties, data) { var axisProperties = this.parseUtils.getYAxisProperties(properties), orientation = (this.axesAreInverted) ? 'horizontal' : 'vertical', colorScheme = this.getAxisColorScheme(); // add some extra info to the axisProperties as needed if(axisProperties['axisTitle.text']){ axisProperties['axisTitle.text'] = Splunk.JSCharting.ParsingUtils.escapeHtml(axisProperties['axisTitle.text']); } axisProperties.chartType = properties.chart; axisProperties.axisLength = $(this.renderTo).height(); axisProperties.percentMode = (this.properties['chart.stackMode'] === 'stacked100'); this.yAxis = new Splunk.JSCharting.NumericAxis(axisProperties, data, orientation, colorScheme); this.hcConfig.yAxis = this.yAxis.getConfig(); if((typeof this.hcConfig.yAxis.title.text === 'undefined') && this.processedData.fieldNames.length === 1) { this.hcConfig.yAxis.title.text = this.processedData.fieldNames[0]; } }, getAxisColorScheme: function() { return { foregroundColorSoft: this.foregroundColorSoft, foregroundColorSofter: this.foregroundColorSofter, fontColor: this.fontColor }; }, formatTooltip: function(properties, data) { var xAxisKey = this.xAxis.getKey(), resolveX = this.xAxis.formatTooltipValue.bind(this.xAxis), resolveY = this.yAxis.formatTooltipValue.bind(this.yAxis); this.mapper.mapObject({ tooltip: { formatter: function() { var seriesColorRgb = Splunk.JSCharting.ColorUtils.removeAlphaFromColor(this.point.series.color); return [ '<span style="color:#cccccc">', ((data.xAxisType == 'time') ? 'time: ' : xAxisKey + ': '), '</span>', '<span style="color:#ffffff">', resolveX(this, "x"), '</span>', '<br/>', '<span style="color:', seriesColorRgb, '">', Splunk.JSCharting.ParsingUtils.escapeHtml(this.series.name), ': </span>', '<span style="color:#ffffff">', resolveY(this, "y"), '</span>' ].join(''); } } }); }, formatLegend: function() { $.extend(true, this.hcConfig, { legend: { labelFormatter: function() { return Splunk.JSCharting.ParsingUtils.escapeHtml(this.name); } } }); }, legendPlacementHook: function(options, width, height, spacingBox) { if(this.hcConfig.legend.layout === 'vertical') { if(height >= spacingBox.height) { // if the legend is taller than the chart height, clip it to the top of the chart options.verticalAlign = 'top'; options.y = 0; } else if(this.properties['layout.splitSeries'] !== "true") { // a bit of a hack here... // at this point in the HighCharts rendering process we don't know the height of the x-axis // and can't factor it into the vertical alignment of the legend // so we make an educated guess based on what we know about the charting configuration var bottomSpacing, timeSpan; if(this.processedData.xAxisType === "time" && !this.axesAreInverted) { timeSpan = (this.processedData._spanSeries) ? parseInt(this.processedData._spanSeries[0], 10) : 1; bottomSpacing = (timeSpan >= (24 * 60 * 60)) ? 28 : 42; } else { bottomSpacing = 13; } options.y = -bottomSpacing / 2; } } }, legendLabelRenderHook: function(items, options, itemStyle, spacingBox, renderer) { var i, adjusted, fixedWidth, maxWidth, horizontalLayout = (options.layout === 'horizontal'), defaultFontSize = 12, minFontSize = 10, symbolWidth = options.symbolWidth, symbolPadding = options.symbolPadding, itemHorizSpacing = 10, labels = [], formatter = new Splunk.JSCharting.FormattingHelper(renderer), ellipsisModeMap = { 'default': 'start', 'ellipsisStart': 'start', 'ellipsisMiddle': 'middle', 'ellipsisEnd': 'end', 'ellipsisNone': 'none' }; if(horizontalLayout) { maxWidth = (items.length > 5) ? Math.floor(spacingBox.width / 6) : Math.floor(spacingBox.width / items.length) - (symbolWidth + symbolPadding + itemHorizSpacing); } else { maxWidth = Math.floor(spacingBox.width / 6); } // make a copy of the original formatting function, since we're going to clobber it if(!options.originalFormatter) { options.originalFormatter = options.labelFormatter; } // get all of the legend labels for(i = 0; i < items.length; i++) { labels.push(options.originalFormatter.call(items[i])); } adjusted = formatter.adjustLabels(labels, maxWidth, minFontSize, defaultFontSize, ellipsisModeMap[this.legendEllipsizeMode] || 'middle'); // in case of horizontal layout with ellipsized labels, set a fixed width for nice alignment if(adjusted.areEllipsized && horizontalLayout && items.length > 5) { fixedWidth = maxWidth + symbolWidth + symbolPadding + itemHorizSpacing; options.itemWidth = fixedWidth; } else { options.itemWidth = undefined; } // set the new labels to the name field of each item for(i = 0; i < items.length; i++) { items[i].ellipsizedName = adjusted.labels[i]; // if the legendItem is already set this is a resize event, so we need to explicitly reformat the item if(items[i].legendItem) { formatter.setElementText(items[i].legendItem, adjusted.labels[i]); items[i].legendItem.css({'font-size': adjusted.fontSize + 'px'}); } } // now that the ellipsizedName field has the pre-formatted labels, update the label formatter options.labelFormatter = function() { return this.ellipsizedName; }; // adjust the font size itemStyle['font-size'] = adjusted.fontSize + 'px'; formatter.destroy(); }, //////////////////////////////////////////////////////////////////////////// // helper methods for attaching event handlers addClickHandlers: function() { if(this.enableChartClick) { var self = this; $.extend(true, this.hcConfig, { plotOptions: { series: { point: { events: { click: function(event) { self.onPointClick.call(self, this, event); } } } } } }); } }, addHoverHandlers: function() { var that = this, properties = { highlightDelay: 125, unhighlightDelay: 50, onMouseOver: function(point) { that.onPointMouseOver(point); }, onMouseOut: function(point) { that.onPointMouseOut(point); } }, throttle = new this.Throttler(properties); $.extend(true, this.hcConfig, { plotOptions: { series: { point: { events: { mouseOver: function() { var point = this; throttle.mouseOverHappened(point); }, mouseOut: function() { var point = this; throttle.mouseOutHappened(point); } } } } } }); }, onPointClick: function(point, domEvent) { var xAxisKey = this.processedData.xAxisKey, xAxisType = this.processedData.xAxisType, event = { fields: [xAxisKey, point.series.name], data: {}, domEvent: domEvent }; event.data[point.series.name] = point.y; if(xAxisType == "time") { event.data._span = point._span; event.data[xAxisKey] = Splunk.util.getEpochTimeFromISO(point.name); } else { event.data[xAxisKey] = (xAxisType == 'category') ? point.name : point.x; } // determine the point's index in its series, // this allows upstream handlers to add row context to drilldown events var i, series = point.series; if(series && series.data && series.data.length > 0) { for(i = 0; i < series.data.length; i++) { if(series.data[i] === point) { event.pointIndex = i; break; } } } this.dispatchEvent('chartClicked', event); }, onPointMouseOver: function(point) { var series = point.series; this.highlightThisSeries(series); this.highlightSeriesInLegend(series); }, onPointMouseOut: function(point) { var series = point.series; this.unHighlightThisSeries(series); this.unHighlightSeriesInLegend(series); }, addLegendHandlers: function(properties) { var self = this; if(this.enableLegendClick) { $.extend(true, this.hcConfig, { plotOptions: { series: { events: { legendItemClick: function(event) { return self.onLegendClick.call(self, this, event); } } } }, legend: { itemStyle: { cursor: 'pointer' }, itemHoverStyle: { cursor: 'pointer' } } }); } }, onLegendClick: function(series, domEvent) { var event = { text: series.name, domEvent: domEvent }; this.dispatchEvent('legendClicked', event); return false; }, addLegendHoverEffects: function(chart) { var that = this, properties = { highlightDelay: 125, unhighlightDelay: 50, onMouseOver: function(series) { that.onLegendMouseOver(series); }, onMouseOut: function(series) { that.onLegendMouseOut(series); } }, throttle = new this.Throttler(properties); $(chart.series).each(function(i, loopSeries) { $(that.getSeriesLegendElements(loopSeries)).each(function(j, element) { $(element).bind('mouseover.splunk_jscharting', function() { throttle.mouseOverHappened(loopSeries); }); $(element).bind('mouseout.splunk_jscharting', function() { throttle.mouseOutHappened(loopSeries); }); }); }); }, removeLegendHoverEffects: function() { if(this.hcChart) { var self = this; $(this.hcChart.series).each(function(i, loopSeries) { $(self.getSeriesLegendElements(loopSeries)).each(function(j, element) { $(element).unbind('.splunk_jscharting'); }); }); } }, onLegendMouseOver: function(series) { this.highlightThisSeries(series); this.highlightSeriesInLegend(series); }, onLegendMouseOut: function(series) { this.unHighlightThisSeries(series); this.unHighlightSeriesInLegend(series); }, addRedrawHandlers: function(chart) { var self = this; $.extend(true, this.hcConfig, { chart: { events: { redraw: function() { self.onDrawOrResize.call(self, this); } } } }); }, onDrawOrResize: function(chart) { var formatter = new Splunk.JSCharting.FormattingHelper(chart.renderer); if(this.xAxis) { this.xAxis.onDrawOrResize(chart, formatter); } if(this.yAxis) { this.yAxis.onDrawOrResize(chart, formatter); } formatter.destroy(); }, highlightThisSeries: function(series) { if(!series || !series.chart) { return; } var chart = series.chart, index = series.index; $(chart.series).each(function(i, loopSeries) { if(i !== index) { this.fadeSeries(loopSeries); } else { this.focusSeries(loopSeries); } }.bind(this)); }, fadeSeries: function(series) { if(!series || !series.data) { return; } for(var i = 0; i < series.data.length; i++) { this.fadePoint(series.data[i], series); } }, fadePoint: function(point, series) { if(!point || !point.graphic) { return; } point.graphic.attr('fill', this.fadedElementColor); }, unHighlightThisSeries: function(series) { if(!series || !series.chart) { return; } var chart = series.chart, index = series.index; $(chart.series).each(function(i, loopSeries) { if(i !== index) { this.focusSeries(loopSeries); } }.bind(this)); }, focusSeries: function(series) { if(!series || !series.data) { return; } for(var i = 0; i < series.data.length; i++) { this.focusPoint(series.data[i], series); } }, focusPoint: function(point, series) { if(!point || !point.graphic) { return; } series = series || point.series; point.graphic.attr({'fill': series.color}); }, highlightSeriesInLegend: function(series) { if(!series || !series.chart) { return; } var i, loopSeries, chart = series.chart, index = series.index; for(i = 0; i < chart.series.length; i++) { loopSeries = chart.series[i]; if(i !== index) { if(!loopSeries) { break; } if(loopSeries.legendItem) { loopSeries.legendItem.attr('fill-opacity', this.fadedElementOpacity); } if(loopSeries.legendLine) { loopSeries.legendLine.attr('stroke', this.fadedElementColor); } if(loopSeries.legen