Commit 18a90667 by Alexander Zobnin

heatmap: refactor

parent a791a92d
......@@ -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;
}
}
......@@ -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));
......
......@@ -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;
}
/**
......
......@@ -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 = `<div class="graph-tooltip-time">${time}</div>
......@@ -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;
}
};
}
}
......@@ -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;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment