displayValue.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. // Libraries
  2. import _ from 'lodash';
  3. // Utils
  4. import { getValueFormat } from './valueFormats/valueFormats';
  5. import { getMappedValue } from './valueMappings';
  6. import { getColorFromHexRgbOrName } from './namedColorsPalette';
  7. // Types
  8. import {
  9. Threshold,
  10. ValueMapping,
  11. DecimalInfo,
  12. DisplayValue,
  13. GrafanaTheme,
  14. GrafanaThemeType,
  15. DecimalCount,
  16. } from '../types';
  17. import { DateTime, dateTime, Field } from '@grafana/data';
  18. export type DisplayProcessor = (value: any) => DisplayValue;
  19. export interface DisplayValueOptions {
  20. field?: Partial<Field>;
  21. mappings?: ValueMapping[];
  22. thresholds?: Threshold[];
  23. // Alternative to empty string
  24. noValue?: string;
  25. // Context
  26. isUtc?: boolean;
  27. theme?: GrafanaTheme; // Will pick 'dark' if not defined
  28. }
  29. export function getDisplayProcessor(options?: DisplayValueOptions): DisplayProcessor {
  30. if (options && !_.isEmpty(options)) {
  31. const field = options.field ? options.field : {};
  32. const formatFunc = getValueFormat(field.unit || 'none');
  33. return (value: any) => {
  34. const { mappings, thresholds, theme } = options;
  35. let color;
  36. let text = _.toString(value);
  37. let numeric = toNumber(value);
  38. let shouldFormat = true;
  39. if (mappings && mappings.length > 0) {
  40. const mappedValue = getMappedValue(mappings, value);
  41. if (mappedValue) {
  42. text = mappedValue.text;
  43. const v = toNumber(text);
  44. if (!isNaN(v)) {
  45. numeric = v;
  46. }
  47. shouldFormat = false;
  48. }
  49. }
  50. if (field.dateFormat) {
  51. const date = toMoment(value, numeric, field.dateFormat);
  52. if (date.isValid()) {
  53. text = date.format(field.dateFormat);
  54. shouldFormat = false;
  55. }
  56. }
  57. if (!isNaN(numeric)) {
  58. if (shouldFormat && !_.isBoolean(value)) {
  59. const { decimals, scaledDecimals } = getDecimalsForValue(value, field.decimals);
  60. text = formatFunc(numeric, decimals, scaledDecimals, options.isUtc);
  61. }
  62. if (thresholds && thresholds.length) {
  63. color = getColorFromThreshold(numeric, thresholds, theme);
  64. }
  65. }
  66. if (!text) {
  67. text = options.noValue ? options.noValue : '';
  68. }
  69. return { text, numeric, color };
  70. };
  71. }
  72. return toStringProcessor;
  73. }
  74. function toMoment(value: any, numeric: number, format: string): DateTime {
  75. if (!isNaN(numeric)) {
  76. const v = dateTime(numeric);
  77. if (v.isValid()) {
  78. return v;
  79. }
  80. }
  81. const v = dateTime(value, format);
  82. if (v.isValid) {
  83. return v;
  84. }
  85. return dateTime(value); // moment will try to parse the format
  86. }
  87. /** Will return any value as a number or NaN */
  88. function toNumber(value: any): number {
  89. if (typeof value === 'number') {
  90. return value;
  91. }
  92. if (value === null || value === undefined || Array.isArray(value)) {
  93. return NaN; // lodash calls them 0
  94. }
  95. if (typeof value === 'boolean') {
  96. return value ? 1 : 0;
  97. }
  98. return _.toNumber(value);
  99. }
  100. function toStringProcessor(value: any): DisplayValue {
  101. return { text: _.toString(value), numeric: toNumber(value) };
  102. }
  103. export function getColorFromThreshold(value: number, thresholds: Threshold[], theme?: GrafanaTheme): string {
  104. const themeType = theme ? theme.type : GrafanaThemeType.Dark;
  105. if (thresholds.length === 1) {
  106. return getColorFromHexRgbOrName(thresholds[0].color, themeType);
  107. }
  108. const atThreshold = thresholds.filter(threshold => value === threshold.value)[0];
  109. if (atThreshold) {
  110. return getColorFromHexRgbOrName(atThreshold.color, themeType);
  111. }
  112. const belowThreshold = thresholds.filter(threshold => value > threshold.value);
  113. if (belowThreshold.length > 0) {
  114. const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0];
  115. return getColorFromHexRgbOrName(nearestThreshold.color, themeType);
  116. }
  117. // Use the first threshold as the default color
  118. return getColorFromHexRgbOrName(thresholds[0].color, themeType);
  119. }
  120. export function getDecimalsForValue(value: number, decimalOverride?: DecimalCount): DecimalInfo {
  121. if (_.isNumber(decimalOverride)) {
  122. // It's important that scaledDecimals is null here
  123. return { decimals: decimalOverride, scaledDecimals: null };
  124. }
  125. const delta = value / 2;
  126. let dec = -Math.floor(Math.log(delta) / Math.LN10);
  127. const magn = Math.pow(10, -dec);
  128. const norm = delta / magn; // norm is between 1.0 and 10.0
  129. let size;
  130. if (norm < 1.5) {
  131. size = 1;
  132. } else if (norm < 3) {
  133. size = 2;
  134. // special case for 2.5, requires an extra decimal
  135. if (norm > 2.25) {
  136. size = 2.5;
  137. ++dec;
  138. }
  139. } else if (norm < 7.5) {
  140. size = 5;
  141. } else {
  142. size = 10;
  143. }
  144. size *= magn;
  145. // reduce starting decimals if not needed
  146. if (Math.floor(value) === value) {
  147. dec = 0;
  148. }
  149. const decimals = Math.max(0, dec);
  150. const scaledDecimals = decimals - Math.floor(Math.log(size) / Math.LN10) + 2;
  151. return { decimals, scaledDecimals };
  152. }