diff --git a/public/app/core/utils/ticks.ts b/public/app/core/utils/ticks.ts index 834b7bd0cc4..db65104cfc0 100644 --- a/public/app/core/utils/ticks.ts +++ b/public/app/core/utils/ticks.ts @@ -156,3 +156,61 @@ export function getFlotTickDecimals(data, axis) { const scaledDecimals = tickDecimals - Math.floor(Math.log(size) / Math.LN10); return { tickDecimals, scaledDecimals }; } + +/** + * Format timestamp similar to Grafana graph panel. + * @param ticks Number of ticks + * @param min Time from (in milliseconds) + * @param max Time to (in milliseconds) + */ +export function grafanaTimeFormat(ticks, min, max) { + if (min && max && ticks) { + let range = max - min; + let secPerTick = range / ticks / 1000; + let oneDay = 86400000; + let oneYear = 31536000000; + + if (secPerTick <= 45) { + return '%H:%M:%S'; + } + if (secPerTick <= 7200 || range <= oneDay) { + return '%H:%M'; + } + if (secPerTick <= 80000) { + return '%m/%d %H:%M'; + } + if (secPerTick <= 2419200 || range <= oneYear) { + return '%m/%d'; + } + return '%Y-%m'; + } + + return '%H:%M'; +} + +/** + * Logarithm of value for arbitrary base. + */ +export function logp(value, base) { + return Math.log(value) / Math.log(base); +} + +/** + * Get decimal precision of number (3.14 => 2) + */ +export function getPrecision(num: number): number { + let str = num.toString(); + return getStringPrecision(str); +} + +/** + * Get decimal precision of number stored as a string ("3.14" => 2) + */ +export function getStringPrecision(num: string): number { + let dot_index = num.indexOf('.'); + if (dot_index === -1) { + return 0; + } else { + return num.length - dot_index - 1; + } +} diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 5a3b04905ef..47d3e6616b9 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -89,6 +89,8 @@ let colorSchemes = [ { name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'darm' }, ]; +const ds_support_histogram_sort = ['prometheus', 'elasticsearch']; + export class HeatmapCtrl extends MetricsPanelCtrl { static templateUrl = 'module.html'; @@ -207,15 +209,20 @@ export class HeatmapCtrl extends MetricsPanelCtrl { } convertHistogramToHeatmapData() { + const panelDatasource = this.getPanelDataSourceType(); let xBucketSize, yBucketSize, bucketsData, tsBuckets; + // Try to sort series by bucket bound, if datasource doesn't do it. + if (!_.includes(ds_support_histogram_sort, panelDatasource)) { + this.series.sort(sortSeriesByLabel); + } + // Convert histogram to heatmap. Each histogram bucket represented by the series which name is // a top (or bottom, depends of datasource) bucket bound. Further, these values will be used as X axis labels. - this.series.sort(sortSeriesByLabel); bucketsData = histogramToHeatmap(this.series); tsBuckets = _.map(this.series, 'label'); - if (this.datasource && this.datasource.type === 'prometheus') { + if (panelDatasource === 'prometheus') { // Prometheus labels are upper inclusive bounds, so add empty bottom bucket label. tsBuckets = [''].concat(tsBuckets); } else { @@ -241,6 +248,14 @@ export class HeatmapCtrl extends MetricsPanelCtrl { }; } + getPanelDataSourceType() { + if (this.datasource.meta && this.datasource.meta.id) { + return this.datasource.meta.id; + } else { + return 'unknown'; + } + } + onDataReceived(dataList) { this.series = dataList.map(this.seriesHandler.bind(this)); diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index 133193e1369..048b19de911 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -67,7 +67,7 @@ function sortSeriesByLabel(s1, s2) { label1 = parseHistogramLabel(s1.label); label2 = parseHistogramLabel(s2.label); } catch (err) { - console.log(err); + console.log(err.message || err); return 0; } @@ -83,10 +83,14 @@ function sortSeriesByLabel(s1, s2) { } function parseHistogramLabel(label: string): number { - if (label === '+Inf') { + if (label === '+Inf' || label === 'inf') { return +Infinity; } - return Number(label); + const value = Number(label); + if (isNaN(value)) { + throw new Error(`Error parsing histogram label: ${label} is not a number`); + } + return value; } /** diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index 0ef1832e7e6..1caa9ae9c69 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -100,14 +100,14 @@ export class HeatmapTooltip { let countValueFormatter, bucketBoundFormatter; if (_.isNumber(this.panel.tooltipDecimals)) { countValueFormatter = this.countValueFormatter(this.panel.tooltipDecimals, null); - bucketBoundFormatter = this.bucketBoundFormatter(this.panel.tooltipDecimals, null); + bucketBoundFormatter = this.panelCtrl.tickValueFormatter(this.panelCtrl.decimals, null); } else { // auto decimals // legend and tooltip gets one more decimal precision // than graph legend ticks let decimals = (this.panelCtrl.decimals || -1) + 1; countValueFormatter = this.countValueFormatter(decimals, this.panelCtrl.scaledDecimals + 2); - bucketBoundFormatter = this.bucketBoundFormatter(decimals, this.panelCtrl.scaledDecimals + 2); + bucketBoundFormatter = this.panelCtrl.tickValueFormatter(decimals, this.panelCtrl.scaledDecimals + 2); } let tooltipHtml = `
${time}
@@ -116,19 +116,13 @@ export class HeatmapTooltip { if (yData) { if (yData.bounds) { if (data.tsBuckets) { - const decimals = this.panelCtrl.decimals || 0; + // Use Y-axis labels const tickFormatter = valIndex => { - let valueFormatted = data.tsBuckets[valIndex]; - if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') { - // Try to format numeric tick labels - valueFormatted = this.bucketBoundFormatter(decimals)(_.toNumber(valueFormatted)); - } - return valueFormatted; + return data.tsBucketsFormatted ? data.tsBucketsFormatted[valIndex] : data.tsBuckets[valIndex]; }; - const tsBucketsTickFormatter = tickFormatter.bind(this); - boundBottom = tsBucketsTickFormatter(yBucketIndex); - boundTop = yBucketIndex < data.tsBuckets.length - 1 ? tsBucketsTickFormatter(yBucketIndex + 1) : ''; + boundBottom = tickFormatter(yBucketIndex); + boundTop = yBucketIndex < data.tsBuckets.length - 1 ? tickFormatter(yBucketIndex + 1) : ''; } else { // Display 0 if bucket is a special 'zero' bucket let bottom = yData.y ? yData.bounds.bottom : 0; @@ -282,21 +276,9 @@ export class HeatmapTooltip { } countValueFormatter(decimals, scaledDecimals = null) { - let format = 'none'; + let format = 'short'; return function(value) { return kbn.valueFormats[format](value, decimals, scaledDecimals); }; } - - bucketBoundFormatter(decimals, scaledDecimals = null) { - let format = this.panel.yAxis.format; - return function(value) { - try { - return format !== 'none' ? kbn.valueFormats[format](value, decimals, scaledDecimals) : value; - } catch (err) { - console.error(err.message || err); - return value; - } - }; - } } diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 0ccba2952cd..55dd91f4b63 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -4,7 +4,7 @@ import moment from 'moment'; import * as d3 from 'd3'; import kbn from 'app/core/utils/kbn'; import { appEvents, contextSrv } from 'app/core/core'; -import { tickStep, getScaledDecimals, getFlotTickSize } from 'app/core/utils/ticks'; +import * as ticksUtils from 'app/core/utils/ticks'; import { HeatmapTooltip } from './heatmap_tooltip'; import { mergeZeroBuckets } from './heatmap_data_converter'; import { getColorScale, getOpacityScale } from './color_scale'; @@ -108,7 +108,7 @@ export default function link(scope, elem, attrs, ctrl) { .range([0, chartWidth]); let ticks = chartWidth / DEFAULT_X_TICK_SIZE_PX; - let grafanaTimeFormatter = grafanaTimeFormat(ticks, timeRange.from, timeRange.to); + let grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, timeRange.from, timeRange.to); let timeFormat; let dashboardTimeZone = ctrl.dashboard.getTimezone(); if (dashboardTimeZone === 'utc') { @@ -141,7 +141,7 @@ export default function link(scope, elem, attrs, ctrl) { function addYAxis() { let ticks = Math.ceil(chartHeight / DEFAULT_Y_TICK_SIZE_PX); - let tick_interval = tickStep(data.heatmapStats.min, data.heatmapStats.max, ticks); + let tick_interval = ticksUtils.tickStep(data.heatmapStats.min, data.heatmapStats.max, ticks); let { y_min, y_max } = wideYAxisRange(data.heatmapStats.min, data.heatmapStats.max, tick_interval); // Rewrite min and max if it have been set explicitly @@ -149,14 +149,14 @@ export default function link(scope, elem, attrs, ctrl) { y_max = panel.yAxis.max !== null ? panel.yAxis.max : y_max; // Adjust ticks after Y range widening - tick_interval = tickStep(y_min, y_max, ticks); + tick_interval = ticksUtils.tickStep(y_min, y_max, ticks); ticks = Math.ceil((y_max - y_min) / tick_interval); - let decimalsAuto = getPrecision(tick_interval); + let decimalsAuto = ticksUtils.getPrecision(tick_interval); let decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals; // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js) - let flot_tick_size = getFlotTickSize(y_min, y_max, ticks, decimalsAuto); - let scaledDecimals = getScaledDecimals(decimals, flot_tick_size); + let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, ticks, decimalsAuto); + let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size); ctrl.decimals = decimals; ctrl.scaledDecimals = scaledDecimals; @@ -248,12 +248,12 @@ export default function link(scope, elem, attrs, ctrl) { let domain = yScale.domain(); let tick_values = logScaleTickValues(domain, log_base); - let decimalsAuto = getPrecision(y_min); + let decimalsAuto = ticksUtils.getPrecision(y_min); let decimals = panel.yAxis.decimals || decimalsAuto; // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js) - let flot_tick_size = getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto); - let scaledDecimals = getScaledDecimals(decimals, flot_tick_size); + let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto); + let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size); ctrl.decimals = decimals; ctrl.scaledDecimals = scaledDecimals; @@ -305,7 +305,7 @@ export default function link(scope, elem, attrs, ctrl) { .range([chartHeight, 0]); const tick_values = _.map(tsBuckets, (b, i) => i); - const decimalsAuto = _.max(_.map(tsBuckets, getStringPrecision)); + const decimalsAuto = _.max(_.map(tsBuckets, ticksUtils.getStringPrecision)); const decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals; ctrl.decimals = decimals; @@ -318,6 +318,9 @@ export default function link(scope, elem, attrs, ctrl) { return valueFormatted; } + const tsBucketsFormatted = _.map(tsBuckets, (v, i) => tickFormatter(i)); + data.tsBucketsFormatted = tsBucketsFormatted; + let yAxis = d3 .axisLeft(yScale) .tickValues(tick_values) @@ -361,11 +364,11 @@ export default function link(scope, elem, attrs, ctrl) { } function adjustLogMax(max, base) { - return Math.pow(base, Math.ceil(logp(max, base))); + return Math.pow(base, Math.ceil(ticksUtils.logp(max, base))); } function adjustLogMin(min, base) { - return Math.pow(base, Math.floor(logp(min, base))); + return Math.pow(base, Math.floor(ticksUtils.logp(min, base))); } function logScaleTickValues(domain, base) { @@ -374,14 +377,14 @@ export default function link(scope, elem, attrs, ctrl) { let tickValues = []; if (domainMin < 1) { - let under_one_ticks = Math.floor(logp(domainMin, base)); + let under_one_ticks = Math.floor(ticksUtils.logp(domainMin, base)); for (let i = under_one_ticks; i < 0; i++) { let tick_value = Math.pow(base, i); tickValues.push(tick_value); } } - let ticks = Math.ceil(logp(domainMax, base)); + let ticks = Math.ceil(ticksUtils.logp(domainMax, base)); for (let i = 0; i <= ticks; i++) { let tick_value = Math.pow(base, i); tickValues.push(tick_value); @@ -402,6 +405,8 @@ export default function link(scope, elem, attrs, ctrl) { }; } + ctrl.tickValueFormatter = tickValueFormatter; + function fixYAxisTickSize() { heatmap .select('.axis-y') @@ -827,46 +832,3 @@ export default function link(scope, elem, attrs, ctrl) { $heatmap.on('mousemove', onMouseMove); $heatmap.on('mouseleave', onMouseLeave); } - -function grafanaTimeFormat(ticks, min, max) { - if (min && max && ticks) { - let range = max - min; - let secPerTick = range / ticks / 1000; - let oneDay = 86400000; - let oneYear = 31536000000; - - if (secPerTick <= 45) { - return '%H:%M:%S'; - } - if (secPerTick <= 7200 || range <= oneDay) { - return '%H:%M'; - } - if (secPerTick <= 80000) { - return '%m/%d %H:%M'; - } - if (secPerTick <= 2419200 || range <= oneYear) { - return '%m/%d'; - } - return '%Y-%m'; - } - - return '%H:%M'; -} - -function logp(value, base) { - return Math.log(value) / Math.log(base); -} - -function getPrecision(num: number): number { - let str = num.toString(); - return getStringPrecision(str); -} - -function getStringPrecision(num: string): number { - let dot_index = num.indexOf('.'); - if (dot_index === -1) { - return 0; - } else { - return num.length - dot_index - 1; - } -}