splunk-sdk
Version:
SDK for usage with the Splunk REST API
1,291 lines (1,162 loc) • 324 kB
JavaScript
// 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