mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
heatmap: refactor
This commit is contained in:
parent
a791a92d79
commit
18a90667ba
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user