heatmap: refactor

This commit is contained in:
Alexander Zobnin 2018-03-07 16:33:33 +03:00
parent a791a92d79
commit 18a90667ba
5 changed files with 109 additions and 88 deletions

View File

@ -156,3 +156,61 @@ export function getFlotTickDecimals(data, axis) {
const scaledDecimals = tickDecimals - Math.floor(Math.log(size) / Math.LN10); const scaledDecimals = tickDecimals - Math.floor(Math.log(size) / Math.LN10);
return { tickDecimals, scaledDecimals }; 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;
}
}

View File

@ -89,6 +89,8 @@ let colorSchemes = [
{ name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'darm' }, { name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'darm' },
]; ];
const ds_support_histogram_sort = ['prometheus', 'elasticsearch'];
export class HeatmapCtrl extends MetricsPanelCtrl { export class HeatmapCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html'; static templateUrl = 'module.html';
@ -207,15 +209,20 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
} }
convertHistogramToHeatmapData() { convertHistogramToHeatmapData() {
const panelDatasource = this.getPanelDataSourceType();
let xBucketSize, yBucketSize, bucketsData, tsBuckets; 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 // 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. // 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); bucketsData = histogramToHeatmap(this.series);
tsBuckets = _.map(this.series, 'label'); 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. // Prometheus labels are upper inclusive bounds, so add empty bottom bucket label.
tsBuckets = [''].concat(tsBuckets); tsBuckets = [''].concat(tsBuckets);
} else { } 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) { onDataReceived(dataList) {
this.series = dataList.map(this.seriesHandler.bind(this)); this.series = dataList.map(this.seriesHandler.bind(this));

View File

@ -67,7 +67,7 @@ function sortSeriesByLabel(s1, s2) {
label1 = parseHistogramLabel(s1.label); label1 = parseHistogramLabel(s1.label);
label2 = parseHistogramLabel(s2.label); label2 = parseHistogramLabel(s2.label);
} catch (err) { } catch (err) {
console.log(err); console.log(err.message || err);
return 0; return 0;
} }
@ -83,10 +83,14 @@ function sortSeriesByLabel(s1, s2) {
} }
function parseHistogramLabel(label: string): number { function parseHistogramLabel(label: string): number {
if (label === '+Inf') { if (label === '+Inf' || label === 'inf') {
return +Infinity; 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;
} }
/** /**

View File

@ -100,14 +100,14 @@ export class HeatmapTooltip {
let countValueFormatter, bucketBoundFormatter; let countValueFormatter, bucketBoundFormatter;
if (_.isNumber(this.panel.tooltipDecimals)) { if (_.isNumber(this.panel.tooltipDecimals)) {
countValueFormatter = this.countValueFormatter(this.panel.tooltipDecimals, null); countValueFormatter = this.countValueFormatter(this.panel.tooltipDecimals, null);
bucketBoundFormatter = this.bucketBoundFormatter(this.panel.tooltipDecimals, null); bucketBoundFormatter = this.panelCtrl.tickValueFormatter(this.panelCtrl.decimals, null);
} else { } else {
// auto decimals // auto decimals
// legend and tooltip gets one more decimal precision // legend and tooltip gets one more decimal precision
// than graph legend ticks // than graph legend ticks
let decimals = (this.panelCtrl.decimals || -1) + 1; let decimals = (this.panelCtrl.decimals || -1) + 1;
countValueFormatter = this.countValueFormatter(decimals, this.panelCtrl.scaledDecimals + 2); 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> let tooltipHtml = `<div class="graph-tooltip-time">${time}</div>
@ -116,19 +116,13 @@ export class HeatmapTooltip {
if (yData) { if (yData) {
if (yData.bounds) { if (yData.bounds) {
if (data.tsBuckets) { if (data.tsBuckets) {
const decimals = this.panelCtrl.decimals || 0; // Use Y-axis labels
const tickFormatter = valIndex => { const tickFormatter = valIndex => {
let valueFormatted = data.tsBuckets[valIndex]; return data.tsBucketsFormatted ? data.tsBucketsFormatted[valIndex] : data.tsBuckets[valIndex];
if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') {
// Try to format numeric tick labels
valueFormatted = this.bucketBoundFormatter(decimals)(_.toNumber(valueFormatted));
}
return valueFormatted;
}; };
const tsBucketsTickFormatter = tickFormatter.bind(this);
boundBottom = tsBucketsTickFormatter(yBucketIndex); boundBottom = tickFormatter(yBucketIndex);
boundTop = yBucketIndex < data.tsBuckets.length - 1 ? tsBucketsTickFormatter(yBucketIndex + 1) : ''; boundTop = yBucketIndex < data.tsBuckets.length - 1 ? tickFormatter(yBucketIndex + 1) : '';
} else { } else {
// Display 0 if bucket is a special 'zero' bucket // Display 0 if bucket is a special 'zero' bucket
let bottom = yData.y ? yData.bounds.bottom : 0; let bottom = yData.y ? yData.bounds.bottom : 0;
@ -282,21 +276,9 @@ export class HeatmapTooltip {
} }
countValueFormatter(decimals, scaledDecimals = null) { countValueFormatter(decimals, scaledDecimals = null) {
let format = 'none'; let format = 'short';
return function(value) { return function(value) {
return kbn.valueFormats[format](value, decimals, scaledDecimals); 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;
}
};
}
} }

View File

@ -4,7 +4,7 @@ import moment from 'moment';
import * as d3 from 'd3'; import * as d3 from 'd3';
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import { appEvents, contextSrv } from 'app/core/core'; 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 { HeatmapTooltip } from './heatmap_tooltip';
import { mergeZeroBuckets } from './heatmap_data_converter'; import { mergeZeroBuckets } from './heatmap_data_converter';
import { getColorScale, getOpacityScale } from './color_scale'; import { getColorScale, getOpacityScale } from './color_scale';
@ -108,7 +108,7 @@ export default function link(scope, elem, attrs, ctrl) {
.range([0, chartWidth]); .range([0, chartWidth]);
let ticks = chartWidth / DEFAULT_X_TICK_SIZE_PX; 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 timeFormat;
let dashboardTimeZone = ctrl.dashboard.getTimezone(); let dashboardTimeZone = ctrl.dashboard.getTimezone();
if (dashboardTimeZone === 'utc') { if (dashboardTimeZone === 'utc') {
@ -141,7 +141,7 @@ export default function link(scope, elem, attrs, ctrl) {
function addYAxis() { function addYAxis() {
let ticks = Math.ceil(chartHeight / DEFAULT_Y_TICK_SIZE_PX); 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); let { y_min, y_max } = wideYAxisRange(data.heatmapStats.min, data.heatmapStats.max, tick_interval);
// Rewrite min and max if it have been set explicitly // 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; y_max = panel.yAxis.max !== null ? panel.yAxis.max : y_max;
// Adjust ticks after Y range widening // 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); 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; let decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals;
// Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js) // 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 flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, ticks, decimalsAuto);
let scaledDecimals = getScaledDecimals(decimals, flot_tick_size); let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size);
ctrl.decimals = decimals; ctrl.decimals = decimals;
ctrl.scaledDecimals = scaledDecimals; ctrl.scaledDecimals = scaledDecimals;
@ -248,12 +248,12 @@ export default function link(scope, elem, attrs, ctrl) {
let domain = yScale.domain(); let domain = yScale.domain();
let tick_values = logScaleTickValues(domain, log_base); let tick_values = logScaleTickValues(domain, log_base);
let decimalsAuto = getPrecision(y_min); let decimalsAuto = ticksUtils.getPrecision(y_min);
let decimals = panel.yAxis.decimals || decimalsAuto; let decimals = panel.yAxis.decimals || decimalsAuto;
// Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js) // 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 flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto);
let scaledDecimals = getScaledDecimals(decimals, flot_tick_size); let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size);
ctrl.decimals = decimals; ctrl.decimals = decimals;
ctrl.scaledDecimals = scaledDecimals; ctrl.scaledDecimals = scaledDecimals;
@ -305,7 +305,7 @@ export default function link(scope, elem, attrs, ctrl) {
.range([chartHeight, 0]); .range([chartHeight, 0]);
const tick_values = _.map(tsBuckets, (b, i) => i); 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; const decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals;
ctrl.decimals = decimals; ctrl.decimals = decimals;
@ -318,6 +318,9 @@ export default function link(scope, elem, attrs, ctrl) {
return valueFormatted; return valueFormatted;
} }
const tsBucketsFormatted = _.map(tsBuckets, (v, i) => tickFormatter(i));
data.tsBucketsFormatted = tsBucketsFormatted;
let yAxis = d3 let yAxis = d3
.axisLeft(yScale) .axisLeft(yScale)
.tickValues(tick_values) .tickValues(tick_values)
@ -361,11 +364,11 @@ export default function link(scope, elem, attrs, ctrl) {
} }
function adjustLogMax(max, base) { 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) { 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) { function logScaleTickValues(domain, base) {
@ -374,14 +377,14 @@ export default function link(scope, elem, attrs, ctrl) {
let tickValues = []; let tickValues = [];
if (domainMin < 1) { 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++) { for (let i = under_one_ticks; i < 0; i++) {
let tick_value = Math.pow(base, i); let tick_value = Math.pow(base, i);
tickValues.push(tick_value); 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++) { for (let i = 0; i <= ticks; i++) {
let tick_value = Math.pow(base, i); let tick_value = Math.pow(base, i);
tickValues.push(tick_value); tickValues.push(tick_value);
@ -402,6 +405,8 @@ export default function link(scope, elem, attrs, ctrl) {
}; };
} }
ctrl.tickValueFormatter = tickValueFormatter;
function fixYAxisTickSize() { function fixYAxisTickSize() {
heatmap heatmap
.select('.axis-y') .select('.axis-y')
@ -827,46 +832,3 @@ export default function link(scope, elem, attrs, ctrl) {
$heatmap.on('mousemove', onMouseMove); $heatmap.on('mousemove', onMouseMove);
$heatmap.on('mouseleave', onMouseLeave); $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;
}
}