1/**
2 * Calculate tick step.
3 * Implementation from d3-array (ticks.js)
4 * https://github.com/d3/d3-array/blob/master/src/ticks.js
5 * @param start Start value
6 * @param stop End value
7 * @param count Ticks count
8 */
9export function tickStep(start: number, stop: number, count: number): number {
10  const e10 = Math.sqrt(50),
11    e5 = Math.sqrt(10),
12    e2 = Math.sqrt(2);
13
14  const step0 = Math.abs(stop - start) / Math.max(0, count);
15  let step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10));
16  const error = step0 / step1;
17
18  if (error >= e10) {
19    step1 *= 10;
20  } else if (error >= e5) {
21    step1 *= 5;
22  } else if (error >= e2) {
23    step1 *= 2;
24  }
25
26  return stop < start ? -step1 : step1;
27}
28
29export function getScaledDecimals(decimals: number, tickSize: number) {
30  return decimals - Math.floor(Math.log(tickSize) / Math.LN10);
31}
32
33/**
34 * Calculate tick size based on min and max values, number of ticks and precision.
35 * Implementation from Flot.
36 * @param min           Axis minimum
37 * @param max           Axis maximum
38 * @param noTicks       Number of ticks
39 * @param tickDecimals  Tick decimal precision
40 */
41export function getFlotTickSize(min: number, max: number, noTicks: number, tickDecimals: number) {
42  const delta = (max - min) / noTicks;
43  let dec = -Math.floor(Math.log(delta) / Math.LN10);
44  const maxDec = tickDecimals;
45
46  const magn = Math.pow(10, -dec);
47  const norm = delta / magn; // norm is between 1.0 and 10.0
48  let size;
49
50  if (norm < 1.5) {
51    size = 1;
52  } else if (norm < 3) {
53    size = 2;
54    // special case for 2.5, requires an extra decimal
55    if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
56      size = 2.5;
57      ++dec;
58    }
59  } else if (norm < 7.5) {
60    size = 5;
61  } else {
62    size = 10;
63  }
64
65  size *= magn;
66
67  return size;
68}
69
70/**
71 * Calculate axis range (min and max).
72 * Implementation from Flot.
73 */
74export function getFlotRange(panelMin: any, panelMax: any, datamin: number, datamax: number) {
75  const autoscaleMargin = 0.02;
76
77  let min = +(panelMin != null ? panelMin : datamin);
78  let max = +(panelMax != null ? panelMax : datamax);
79  const delta = max - min;
80
81  if (delta === 0.0) {
82    // Grafana fix: wide Y min and max using increased wideFactor
83    // when all series values are the same
84    const wideFactor = 0.25;
85    const widen = Math.abs(max === 0 ? 1 : max * wideFactor);
86
87    if (panelMin === null) {
88      min -= widen;
89    }
90    // always widen max if we couldn't widen min to ensure we
91    // don't fall into min == max which doesn't work
92    if (panelMax == null || panelMin != null) {
93      max += widen;
94    }
95  } else {
96    // consider autoscaling
97    const margin = autoscaleMargin;
98    if (margin != null) {
99      if (panelMin == null) {
100        min -= delta * margin;
101        // make sure we don't go below zero if all values
102        // are positive
103        if (min < 0 && datamin != null && datamin >= 0) {
104          min = 0;
105        }
106      }
107      if (panelMax == null) {
108        max += delta * margin;
109        if (max > 0 && datamax != null && datamax <= 0) {
110          max = 0;
111        }
112      }
113    }
114  }
115  return { min, max };
116}
117
118/**
119 * Calculate tick decimals.
120 * Implementation from Flot.
121 */
122export function getFlotTickDecimals(datamin: number, datamax: number, axis: { min: any; max: any }, height: number) {
123  const { min, max } = getFlotRange(axis.min, axis.max, datamin, datamax);
124  const noTicks = 0.3 * Math.sqrt(height);
125  const delta = (max - min) / noTicks;
126  const dec = -Math.floor(Math.log(delta) / Math.LN10);
127
128  const magn = Math.pow(10, -dec);
129  // norm is between 1.0 and 10.0
130  const norm = delta / magn;
131  let size;
132
133  if (norm < 1.5) {
134    size = 1;
135  } else if (norm < 3) {
136    size = 2;
137    // special case for 2.5, requires an extra decimal
138    if (norm > 2.25) {
139      size = 2.5;
140    }
141  } else if (norm < 7.5) {
142    size = 5;
143  } else {
144    size = 10;
145  }
146  size *= magn;
147
148  const tickDecimals = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1);
149  // grafana addition
150  const scaledDecimals = tickDecimals - Math.floor(Math.log(size) / Math.LN10);
151  return { tickDecimals, scaledDecimals };
152}
153
154/**
155 * Format timestamp similar to Grafana graph panel.
156 * @param ticks Number of ticks
157 * @param min Time from (in milliseconds)
158 * @param max Time to (in milliseconds)
159 */
160export function grafanaTimeFormat(ticks: number, min: number, max: number) {
161  if (min && max && ticks) {
162    const range = max - min;
163    const secPerTick = range / ticks / 1000;
164    const oneDay = 86400000;
165    const oneYear = 31536000000;
166
167    if (secPerTick <= 45) {
168      return 'HH:mm:ss';
169    }
170    if (secPerTick <= 7200 || range <= oneDay) {
171      return 'HH:mm';
172    }
173    if (secPerTick <= 80000) {
174      return 'MM/DD HH:mm';
175    }
176    if (secPerTick <= 2419200 || range <= oneYear) {
177      return 'MM/DD';
178    }
179    return 'YYYY-MM';
180  }
181
182  return 'HH:mm';
183}
184
185/**
186 * Logarithm of value for arbitrary base.
187 */
188export function logp(value: number, base: number) {
189  return Math.log(value) / Math.log(base);
190}
191
192/**
193 * Get decimal precision of number (3.14 => 2)
194 */
195export function getPrecision(num: number): number {
196  const str = num.toString();
197  return getStringPrecision(str);
198}
199
200/**
201 * Get decimal precision of number stored as a string ("3.14" => 2)
202 */
203export function getStringPrecision(num: string): number {
204  if (isNaN((num as unknown) as number)) {
205    return 0;
206  }
207
208  const dotIndex = num.indexOf('.');
209  if (dotIndex === -1) {
210    return 0;
211  } else {
212    return num.length - dotIndex - 1;
213  }
214}
215