From a05f266e2727118f6b80cd110b1488d2086a713e Mon Sep 17 00:00:00 2001 From: Ihor Yeromin Date: Tue, 2 Jan 2024 19:40:57 +0200 Subject: [PATCH] Units: Add scalable unit option (#79411) * fix(units): add scalable unit option --------- Co-authored-by: nmarrs --- .betterer.results | 3 +- .../configure-standard-options/index.md | 4 + .../src/field/displayProcessor.ts | 2 +- packages/grafana-data/src/types/dataFrame.ts | 1 + .../src/valueFormats/categories.ts | 216 +++++++++--------- .../src/valueFormats/symbolFormatters.test.ts | 27 +++ .../src/valueFormats/symbolFormatters.ts | 7 +- .../src/valueFormats/valueFormats.test.ts | 12 + .../src/valueFormats/valueFormats.ts | 27 ++- .../core/components/OptionsUI/registry.tsx | 33 ++- 10 files changed, 211 insertions(+), 121 deletions(-) diff --git a/.betterer.results b/.betterer.results index 8cb345bb383..cb5461faa35 100644 --- a/.betterer.results +++ b/.betterer.results @@ -990,8 +990,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "6"], [0, 0, 0, "Unexpected any. Specify a different type.", "7"], [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"] + [0, 0, 0, "Unexpected any. Specify a different type.", "9"] ], "public/app/core/components/PageHeader/PageHeader.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], diff --git a/docs/sources/panels-visualizations/configure-standard-options/index.md b/docs/sources/panels-visualizations/configure-standard-options/index.md index ea99c1062a9..54bd1e133cc 100644 --- a/docs/sources/panels-visualizations/configure-standard-options/index.md +++ b/docs/sources/panels-visualizations/configure-standard-options/index.md @@ -81,6 +81,10 @@ You can also paste a native emoji in the unit picker and pick it as a custom uni Grafana can sometimes be too aggressive in parsing strings and displaying them as numbers. To configure Grafana to show the original string value, create a field override and add a unit property with the `String` unit. +### Scale units + +By default, Grafana automatically scales the unit based on the magnitude of the value. For example, if you have a value of 0.14 kW, Grafana will display it as 140 W. Another example is that 3000 kW will be displayed at 3 MW. If you want to disable this behavior, you can toggle off the **Scale units** switch. + ### Min Lets you set the minimum value used in percentage threshold calculations. Leave blank to automatically calculate the minimum. diff --git a/packages/grafana-data/src/field/displayProcessor.ts b/packages/grafana-data/src/field/displayProcessor.ts index 9cdef2760a6..97bdd23a99f 100644 --- a/packages/grafana-data/src/field/displayProcessor.ts +++ b/packages/grafana-data/src/field/displayProcessor.ts @@ -80,7 +80,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP const canTrimTrailingDecimalZeros = !hasDateUnit && !hasCurrencyUnit && !hasBoolUnit && !isLocaleFormat && isNumType && config.decimals == null; - const formatFunc = getValueFormat(unit || 'none'); + const formatFunc = getValueFormat(unit || 'none', config.unitScale); const scaleFunc = getScaleCalculator(field, options.theme); return (value: unknown, adjacentDecimals?: DecimalCount) => { diff --git a/packages/grafana-data/src/types/dataFrame.ts b/packages/grafana-data/src/types/dataFrame.ts index c4712e91ac4..3b9d1a4326b 100644 --- a/packages/grafana-data/src/types/dataFrame.ts +++ b/packages/grafana-data/src/types/dataFrame.ts @@ -68,6 +68,7 @@ export interface FieldConfig { // Numeric Options unit?: string; + unitScale?: boolean; decimals?: DecimalCount; // Significant digits (for display) min?: number | null; max?: number | null; diff --git a/packages/grafana-data/src/valueFormats/categories.ts b/packages/grafana-data/src/valueFormats/categories.ts index 800dec30e07..d1793c68ff0 100644 --- a/packages/grafana-data/src/valueFormats/categories.ts +++ b/packages/grafana-data/src/valueFormats/categories.ts @@ -34,7 +34,7 @@ import { booleanValueFormatter, } from './valueFormats'; -export const getCategories = (): ValueFormatCategory[] => [ +export const getCategories = (scalable = true): ValueFormatCategory[] => [ { name: 'Misc', formats: [ @@ -88,14 +88,14 @@ export const getCategories = (): ValueFormatCategory[] => [ { name: 'Computation', formats: [ - { name: 'FLOP/s', id: 'flops', fn: SIPrefix('FLOPS') }, - { name: 'MFLOP/s', id: 'mflops', fn: SIPrefix('FLOPS', 2) }, - { name: 'GFLOP/s', id: 'gflops', fn: SIPrefix('FLOPS', 3) }, - { name: 'TFLOP/s', id: 'tflops', fn: SIPrefix('FLOPS', 4) }, - { name: 'PFLOP/s', id: 'pflops', fn: SIPrefix('FLOPS', 5) }, - { name: 'EFLOP/s', id: 'eflops', fn: SIPrefix('FLOPS', 6) }, - { name: 'ZFLOP/s', id: 'zflops', fn: SIPrefix('FLOPS', 7) }, - { name: 'YFLOP/s', id: 'yflops', fn: SIPrefix('FLOPS', 8) }, + { name: 'FLOP/s', id: 'flops', fn: SIPrefix('FLOPS', 0, scalable) }, + { name: 'MFLOP/s', id: 'mflops', fn: SIPrefix('FLOPS', 2, scalable) }, + { name: 'GFLOP/s', id: 'gflops', fn: SIPrefix('FLOPS', 3, scalable) }, + { name: 'TFLOP/s', id: 'tflops', fn: SIPrefix('FLOPS', 4, scalable) }, + { name: 'PFLOP/s', id: 'pflops', fn: SIPrefix('FLOPS', 5, scalable) }, + { name: 'EFLOP/s', id: 'eflops', fn: SIPrefix('FLOPS', 6, scalable) }, + { name: 'ZFLOP/s', id: 'zflops', fn: SIPrefix('FLOPS', 7, scalable) }, + { name: 'YFLOP/s', id: 'yflops', fn: SIPrefix('FLOPS', 8, scalable) }, ], }, { @@ -151,49 +151,49 @@ export const getCategories = (): ValueFormatCategory[] => [ name: 'Data', formats: [ { name: 'bytes(IEC)', id: 'bytes', fn: binaryPrefix('B') }, - { name: 'bytes(SI)', id: 'decbytes', fn: SIPrefix('B') }, + { name: 'bytes(SI)', id: 'decbytes', fn: SIPrefix('B', 0, scalable) }, { name: 'bits(IEC)', id: 'bits', fn: binaryPrefix('b') }, - { name: 'bits(SI)', id: 'decbits', fn: SIPrefix('b') }, + { name: 'bits(SI)', id: 'decbits', fn: SIPrefix('b', 0, scalable) }, { name: 'kibibytes', id: 'kbytes', fn: binaryPrefix('B', 1) }, - { name: 'kilobytes', id: 'deckbytes', fn: SIPrefix('B', 1) }, + { name: 'kilobytes', id: 'deckbytes', fn: SIPrefix('B', 1, scalable) }, { name: 'mebibytes', id: 'mbytes', fn: binaryPrefix('B', 2) }, - { name: 'megabytes', id: 'decmbytes', fn: SIPrefix('B', 2) }, + { name: 'megabytes', id: 'decmbytes', fn: SIPrefix('B', 2, scalable) }, { name: 'gibibytes', id: 'gbytes', fn: binaryPrefix('B', 3) }, - { name: 'gigabytes', id: 'decgbytes', fn: SIPrefix('B', 3) }, + { name: 'gigabytes', id: 'decgbytes', fn: SIPrefix('B', 3, scalable) }, { name: 'tebibytes', id: 'tbytes', fn: binaryPrefix('B', 4) }, - { name: 'terabytes', id: 'dectbytes', fn: SIPrefix('B', 4) }, + { name: 'terabytes', id: 'dectbytes', fn: SIPrefix('B', 4, scalable) }, { name: 'pebibytes', id: 'pbytes', fn: binaryPrefix('B', 5) }, - { name: 'petabytes', id: 'decpbytes', fn: SIPrefix('B', 5) }, + { name: 'petabytes', id: 'decpbytes', fn: SIPrefix('B', 5, scalable) }, ], }, { name: 'Data rate', formats: [ - { name: 'packets/sec', id: 'pps', fn: SIPrefix('p/s') }, + { name: 'packets/sec', id: 'pps', fn: SIPrefix('p/s', 0, scalable) }, { name: 'bytes/sec(IEC)', id: 'binBps', fn: binaryPrefix('B/s') }, - { name: 'bytes/sec(SI)', id: 'Bps', fn: SIPrefix('B/s') }, + { name: 'bytes/sec(SI)', id: 'Bps', fn: SIPrefix('B/s', 0, scalable) }, { name: 'bits/sec(IEC)', id: 'binbps', fn: binaryPrefix('b/s') }, - { name: 'bits/sec(SI)', id: 'bps', fn: SIPrefix('b/s') }, + { name: 'bits/sec(SI)', id: 'bps', fn: SIPrefix('b/s', 0, scalable) }, { name: 'kibibytes/sec', id: 'KiBs', fn: binaryPrefix('B/s', 1) }, { name: 'kibibits/sec', id: 'Kibits', fn: binaryPrefix('b/s', 1) }, - { name: 'kilobytes/sec', id: 'KBs', fn: SIPrefix('B/s', 1) }, - { name: 'kilobits/sec', id: 'Kbits', fn: SIPrefix('b/s', 1) }, + { name: 'kilobytes/sec', id: 'KBs', fn: SIPrefix('B/s', 1, scalable) }, + { name: 'kilobits/sec', id: 'Kbits', fn: SIPrefix('b/s', 1, scalable) }, { name: 'mebibytes/sec', id: 'MiBs', fn: binaryPrefix('B/s', 2) }, { name: 'mebibits/sec', id: 'Mibits', fn: binaryPrefix('b/s', 2) }, - { name: 'megabytes/sec', id: 'MBs', fn: SIPrefix('B/s', 2) }, - { name: 'megabits/sec', id: 'Mbits', fn: SIPrefix('b/s', 2) }, + { name: 'megabytes/sec', id: 'MBs', fn: SIPrefix('B/s', 2, scalable) }, + { name: 'megabits/sec', id: 'Mbits', fn: SIPrefix('b/s', 2, scalable) }, { name: 'gibibytes/sec', id: 'GiBs', fn: binaryPrefix('B/s', 3) }, { name: 'gibibits/sec', id: 'Gibits', fn: binaryPrefix('b/s', 3) }, - { name: 'gigabytes/sec', id: 'GBs', fn: SIPrefix('B/s', 3) }, - { name: 'gigabits/sec', id: 'Gbits', fn: SIPrefix('b/s', 3) }, + { name: 'gigabytes/sec', id: 'GBs', fn: SIPrefix('B/s', 3, scalable) }, + { name: 'gigabits/sec', id: 'Gbits', fn: SIPrefix('b/s', 3, scalable) }, { name: 'tebibytes/sec', id: 'TiBs', fn: binaryPrefix('B/s', 4) }, { name: 'tebibits/sec', id: 'Tibits', fn: binaryPrefix('b/s', 4) }, - { name: 'terabytes/sec', id: 'TBs', fn: SIPrefix('B/s', 4) }, - { name: 'terabits/sec', id: 'Tbits', fn: SIPrefix('b/s', 4) }, + { name: 'terabytes/sec', id: 'TBs', fn: SIPrefix('B/s', 4, scalable) }, + { name: 'terabits/sec', id: 'Tbits', fn: SIPrefix('b/s', 4, scalable) }, { name: 'pebibytes/sec', id: 'PiBs', fn: binaryPrefix('B/s', 5) }, { name: 'pebibits/sec', id: 'Pibits', fn: binaryPrefix('b/s', 5) }, - { name: 'petabytes/sec', id: 'PBs', fn: SIPrefix('B/s', 5) }, - { name: 'petabits/sec', id: 'Pbits', fn: SIPrefix('b/s', 5) }, + { name: 'petabytes/sec', id: 'PBs', fn: SIPrefix('B/s', 5, scalable) }, + { name: 'petabits/sec', id: 'Pbits', fn: SIPrefix('b/s', 5, scalable) }, ], }, { @@ -216,46 +216,46 @@ export const getCategories = (): ValueFormatCategory[] => [ { name: 'Energy', formats: [ - { name: 'Watt (W)', id: 'watt', fn: SIPrefix('W') }, - { name: 'Kilowatt (kW)', id: 'kwatt', fn: SIPrefix('W', 1) }, - { name: 'Megawatt (MW)', id: 'megwatt', fn: SIPrefix('W', 2) }, - { name: 'Gigawatt (GW)', id: 'gwatt', fn: SIPrefix('W', 3) }, - { name: 'Milliwatt (mW)', id: 'mwatt', fn: SIPrefix('W', -1) }, + { name: 'Watt (W)', id: 'watt', fn: SIPrefix('W', 0, scalable) }, + { name: 'Kilowatt (kW)', id: 'kwatt', fn: SIPrefix('W', 1, scalable) }, + { name: 'Megawatt (MW)', id: 'megwatt', fn: SIPrefix('W', 2, scalable) }, + { name: 'Gigawatt (GW)', id: 'gwatt', fn: SIPrefix('W', 3, scalable) }, + { name: 'Milliwatt (mW)', id: 'mwatt', fn: SIPrefix('W', -1, scalable) }, { name: 'Watt per square meter (W/m²)', id: 'Wm2', fn: toFixedUnit('W/m²') }, - { name: 'Volt-Ampere (VA)', id: 'voltamp', fn: SIPrefix('VA') }, - { name: 'Kilovolt-Ampere (kVA)', id: 'kvoltamp', fn: SIPrefix('VA', 1) }, - { name: 'Volt-Ampere reactive (VAr)', id: 'voltampreact', fn: SIPrefix('VAr') }, - { name: 'Kilovolt-Ampere reactive (kVAr)', id: 'kvoltampreact', fn: SIPrefix('VAr', 1) }, + { name: 'Volt-Ampere (VA)', id: 'voltamp', fn: SIPrefix('VA', 0, scalable) }, + { name: 'Kilovolt-Ampere (kVA)', id: 'kvoltamp', fn: SIPrefix('VA', 1, scalable) }, + { name: 'Volt-Ampere reactive (VAr)', id: 'voltampreact', fn: SIPrefix('VAr', 0, scalable) }, + { name: 'Kilovolt-Ampere reactive (kVAr)', id: 'kvoltampreact', fn: SIPrefix('VAr', 1, scalable) }, { name: 'Watt-hour (Wh)', id: 'watth', fn: SIPrefix('Wh') }, - { name: 'Watt-hour per Kilogram (Wh/kg)', id: 'watthperkg', fn: SIPrefix('Wh/kg') }, - { name: 'Kilowatt-hour (kWh)', id: 'kwatth', fn: SIPrefix('Wh', 1) }, - { name: 'Kilowatt-min (kWm)', id: 'kwattm', fn: SIPrefix('W-Min', 1) }, - { name: 'Megawatt-hour (MWh)', id: 'mwatth', fn: SIPrefix('Wh', 2) }, - { name: 'Ampere-hour (Ah)', id: 'amph', fn: SIPrefix('Ah') }, - { name: 'Kiloampere-hour (kAh)', id: 'kamph', fn: SIPrefix('Ah', 1) }, - { name: 'Milliampere-hour (mAh)', id: 'mamph', fn: SIPrefix('Ah', -1) }, - { name: 'Joule (J)', id: 'joule', fn: SIPrefix('J') }, - { name: 'Electron volt (eV)', id: 'ev', fn: SIPrefix('eV') }, - { name: 'Ampere (A)', id: 'amp', fn: SIPrefix('A') }, - { name: 'Kiloampere (kA)', id: 'kamp', fn: SIPrefix('A', 1) }, - { name: 'Milliampere (mA)', id: 'mamp', fn: SIPrefix('A', -1) }, - { name: 'Volt (V)', id: 'volt', fn: SIPrefix('V') }, - { name: 'Kilovolt (kV)', id: 'kvolt', fn: SIPrefix('V', 1) }, - { name: 'Millivolt (mV)', id: 'mvolt', fn: SIPrefix('V', -1) }, - { name: 'Decibel-milliwatt (dBm)', id: 'dBm', fn: SIPrefix('dBm') }, - { name: 'Milliohm (mΩ)', id: 'mohm', fn: SIPrefix('Ω', -1) }, - { name: 'Ohm (Ω)', id: 'ohm', fn: SIPrefix('Ω') }, - { name: 'Kiloohm (kΩ)', id: 'kohm', fn: SIPrefix('Ω', 1) }, - { name: 'Megaohm (MΩ)', id: 'Mohm', fn: SIPrefix('Ω', 2) }, - { name: 'Farad (F)', id: 'farad', fn: SIPrefix('F') }, - { name: 'Microfarad (µF)', id: 'µfarad', fn: SIPrefix('F', -2) }, - { name: 'Nanofarad (nF)', id: 'nfarad', fn: SIPrefix('F', -3) }, - { name: 'Picofarad (pF)', id: 'pfarad', fn: SIPrefix('F', -4) }, - { name: 'Femtofarad (fF)', id: 'ffarad', fn: SIPrefix('F', -5) }, - { name: 'Henry (H)', id: 'henry', fn: SIPrefix('H') }, - { name: 'Millihenry (mH)', id: 'mhenry', fn: SIPrefix('H', -1) }, - { name: 'Microhenry (µH)', id: 'µhenry', fn: SIPrefix('H', -2) }, - { name: 'Lumens (Lm)', id: 'lumens', fn: SIPrefix('Lm') }, + { name: 'Watt-hour per Kilogram (Wh/kg)', id: 'watthperkg', fn: SIPrefix('Wh/kg', 0, scalable) }, + { name: 'Kilowatt-hour (kWh)', id: 'kwatth', fn: SIPrefix('Wh', 1, scalable) }, + { name: 'Kilowatt-min (kWm)', id: 'kwattm', fn: SIPrefix('W-Min', 1, scalable) }, + { name: 'Megawatt-hour (MWh)', id: 'mwatth', fn: SIPrefix('Wh', 2, scalable) }, + { name: 'Ampere-hour (Ah)', id: 'amph', fn: SIPrefix('Ah', 0, scalable) }, + { name: 'Kiloampere-hour (kAh)', id: 'kamph', fn: SIPrefix('Ah', 1, scalable) }, + { name: 'Milliampere-hour (mAh)', id: 'mamph', fn: SIPrefix('Ah', -1, scalable) }, + { name: 'Joule (J)', id: 'joule', fn: SIPrefix('J', 0, scalable) }, + { name: 'Electron volt (eV)', id: 'ev', fn: SIPrefix('eV', 0, scalable) }, + { name: 'Ampere (A)', id: 'amp', fn: SIPrefix('A', 0, scalable) }, + { name: 'Kiloampere (kA)', id: 'kamp', fn: SIPrefix('A', 1, scalable) }, + { name: 'Milliampere (mA)', id: 'mamp', fn: SIPrefix('A', -1, scalable) }, + { name: 'Volt (V)', id: 'volt', fn: SIPrefix('V', 0, scalable) }, + { name: 'Kilovolt (kV)', id: 'kvolt', fn: SIPrefix('V', 1, scalable) }, + { name: 'Millivolt (mV)', id: 'mvolt', fn: SIPrefix('V', -1, scalable) }, + { name: 'Decibel-milliwatt (dBm)', id: 'dBm', fn: SIPrefix('dBm', 0, scalable) }, + { name: 'Milliohm (mΩ)', id: 'mohm', fn: SIPrefix('Ω', -1, scalable) }, + { name: 'Ohm (Ω)', id: 'ohm', fn: SIPrefix('Ω', 0, scalable) }, + { name: 'Kiloohm (kΩ)', id: 'kohm', fn: SIPrefix('Ω', 1, scalable) }, + { name: 'Megaohm (MΩ)', id: 'Mohm', fn: SIPrefix('Ω', 2, scalable) }, + { name: 'Farad (F)', id: 'farad', fn: SIPrefix('F', 0, scalable) }, + { name: 'Microfarad (µF)', id: 'µfarad', fn: SIPrefix('F', -2, scalable) }, + { name: 'Nanofarad (nF)', id: 'nfarad', fn: SIPrefix('F', -3, scalable) }, + { name: 'Picofarad (pF)', id: 'pfarad', fn: SIPrefix('F', -4, scalable) }, + { name: 'Femtofarad (fF)', id: 'ffarad', fn: SIPrefix('F', -5, scalable) }, + { name: 'Henry (H)', id: 'henry', fn: SIPrefix('H', 0, scalable) }, + { name: 'Millihenry (mH)', id: 'mhenry', fn: SIPrefix('H', -1, scalable) }, + { name: 'Microhenry (µH)', id: 'µhenry', fn: SIPrefix('H', -2, scalable) }, + { name: 'Lumens (Lm)', id: 'lumens', fn: SIPrefix('Lm', 0, scalable) }, ], }, { @@ -274,52 +274,52 @@ export const getCategories = (): ValueFormatCategory[] => [ { name: 'Force', formats: [ - { name: 'Newton-meters (Nm)', id: 'forceNm', fn: SIPrefix('Nm') }, - { name: 'Kilonewton-meters (kNm)', id: 'forcekNm', fn: SIPrefix('Nm', 1) }, - { name: 'Newtons (N)', id: 'forceN', fn: SIPrefix('N') }, - { name: 'Kilonewtons (kN)', id: 'forcekN', fn: SIPrefix('N', 1) }, + { name: 'Newton-meters (Nm)', id: 'forceNm', fn: SIPrefix('Nm', 0, scalable) }, + { name: 'Kilonewton-meters (kNm)', id: 'forcekNm', fn: SIPrefix('Nm', 1, scalable) }, + { name: 'Newtons (N)', id: 'forceN', fn: SIPrefix('N', 0, scalable) }, + { name: 'Kilonewtons (kN)', id: 'forcekN', fn: SIPrefix('N', 1, scalable) }, ], }, { name: 'Hash rate', formats: [ - { name: 'hashes/sec', id: 'Hs', fn: SIPrefix('H/s') }, - { name: 'kilohashes/sec', id: 'KHs', fn: SIPrefix('H/s', 1) }, - { name: 'megahashes/sec', id: 'MHs', fn: SIPrefix('H/s', 2) }, - { name: 'gigahashes/sec', id: 'GHs', fn: SIPrefix('H/s', 3) }, - { name: 'terahashes/sec', id: 'THs', fn: SIPrefix('H/s', 4) }, - { name: 'petahashes/sec', id: 'PHs', fn: SIPrefix('H/s', 5) }, - { name: 'exahashes/sec', id: 'EHs', fn: SIPrefix('H/s', 6) }, + { name: 'hashes/sec', id: 'Hs', fn: SIPrefix('H/s', 0, scalable) }, + { name: 'kilohashes/sec', id: 'KHs', fn: SIPrefix('H/s', 1, scalable) }, + { name: 'megahashes/sec', id: 'MHs', fn: SIPrefix('H/s', 2, scalable) }, + { name: 'gigahashes/sec', id: 'GHs', fn: SIPrefix('H/s', 3, scalable) }, + { name: 'terahashes/sec', id: 'THs', fn: SIPrefix('H/s', 4, scalable) }, + { name: 'petahashes/sec', id: 'PHs', fn: SIPrefix('H/s', 5, scalable) }, + { name: 'exahashes/sec', id: 'EHs', fn: SIPrefix('H/s', 6, scalable) }, ], }, { name: 'Mass', formats: [ - { name: 'milligram (mg)', id: 'massmg', fn: SIPrefix('g', -1) }, - { name: 'gram (g)', id: 'massg', fn: SIPrefix('g') }, + { name: 'milligram (mg)', id: 'massmg', fn: SIPrefix('g', -1, scalable) }, + { name: 'gram (g)', id: 'massg', fn: SIPrefix('g', 0, scalable) }, { name: 'pound (lb)', id: 'masslb', fn: toFixedUnit('lb') }, - { name: 'kilogram (kg)', id: 'masskg', fn: SIPrefix('g', 1) }, + { name: 'kilogram (kg)', id: 'masskg', fn: SIPrefix('g', 1, scalable) }, { name: 'metric ton (t)', id: 'masst', fn: toFixedUnit('t') }, ], }, { name: 'Length', formats: [ - { name: 'millimeter (mm)', id: 'lengthmm', fn: SIPrefix('m', -1) }, + { name: 'millimeter (mm)', id: 'lengthmm', fn: SIPrefix('m', -1, scalable) }, { name: 'inch (in)', id: 'lengthin', fn: toFixedUnit('in') }, { name: 'feet (ft)', id: 'lengthft', fn: toFixedUnit('ft') }, - { name: 'meter (m)', id: 'lengthm', fn: SIPrefix('m') }, - { name: 'kilometer (km)', id: 'lengthkm', fn: SIPrefix('m', 1) }, + { name: 'meter (m)', id: 'lengthm', fn: SIPrefix('m', 0, scalable) }, + { name: 'kilometer (km)', id: 'lengthkm', fn: SIPrefix('m', 1, scalable) }, { name: 'mile (mi)', id: 'lengthmi', fn: toFixedUnit('mi') }, ], }, { name: 'Pressure', formats: [ - { name: 'Millibars', id: 'pressurembar', fn: SIPrefix('bar', -1) }, - { name: 'Bars', id: 'pressurebar', fn: SIPrefix('bar') }, - { name: 'Kilobars', id: 'pressurekbar', fn: SIPrefix('bar', 1) }, - { name: 'Pascals', id: 'pressurepa', fn: SIPrefix('Pa') }, + { name: 'Millibars', id: 'pressurembar', fn: SIPrefix('bar', -1, scalable) }, + { name: 'Bars', id: 'pressurebar', fn: SIPrefix('bar', 0, scalable) }, + { name: 'Kilobars', id: 'pressurekbar', fn: SIPrefix('bar', 1, scalable) }, + { name: 'Pascals', id: 'pressurepa', fn: SIPrefix('Pa', 0, scalable) }, { name: 'Hectopascals', id: 'pressurehpa', fn: toFixedUnit('hPa') }, { name: 'Kilopascals', id: 'pressurekpa', fn: toFixedUnit('kPa') }, { name: 'Inches of mercury', id: 'pressurehg', fn: toFixedUnit('"Hg') }, @@ -329,29 +329,29 @@ export const getCategories = (): ValueFormatCategory[] => [ { name: 'Radiation', formats: [ - { name: 'Becquerel (Bq)', id: 'radbq', fn: SIPrefix('Bq') }, - { name: 'curie (Ci)', id: 'radci', fn: SIPrefix('Ci') }, - { name: 'Gray (Gy)', id: 'radgy', fn: SIPrefix('Gy') }, - { name: 'rad', id: 'radrad', fn: SIPrefix('rad') }, - { name: 'Sievert (Sv)', id: 'radsv', fn: SIPrefix('Sv') }, - { name: 'milliSievert (mSv)', id: 'radmsv', fn: SIPrefix('Sv', -1) }, - { name: 'microSievert (µSv)', id: 'radusv', fn: SIPrefix('Sv', -2) }, - { name: 'rem', id: 'radrem', fn: SIPrefix('rem') }, - { name: 'Exposure (C/kg)', id: 'radexpckg', fn: SIPrefix('C/kg') }, - { name: 'roentgen (R)', id: 'radr', fn: SIPrefix('R') }, - { name: 'Sievert/hour (Sv/h)', id: 'radsvh', fn: SIPrefix('Sv/h') }, - { name: 'milliSievert/hour (mSv/h)', id: 'radmsvh', fn: SIPrefix('Sv/h', -1) }, - { name: 'microSievert/hour (µSv/h)', id: 'radusvh', fn: SIPrefix('Sv/h', -2) }, + { name: 'Becquerel (Bq)', id: 'radbq', fn: SIPrefix('Bq', 0, scalable) }, + { name: 'curie (Ci)', id: 'radci', fn: SIPrefix('Ci', 0, scalable) }, + { name: 'Gray (Gy)', id: 'radgy', fn: SIPrefix('Gy', 0, scalable) }, + { name: 'rad', id: 'radrad', fn: SIPrefix('rad', 0, scalable) }, + { name: 'Sievert (Sv)', id: 'radsv', fn: SIPrefix('Sv', 0, scalable) }, + { name: 'milliSievert (mSv)', id: 'radmsv', fn: SIPrefix('Sv', -1, scalable) }, + { name: 'microSievert (µSv)', id: 'radusv', fn: SIPrefix('Sv', -2, scalable) }, + { name: 'rem', id: 'radrem', fn: SIPrefix('rem', 0, scalable) }, + { name: 'Exposure (C/kg)', id: 'radexpckg', fn: SIPrefix('C/kg', 0, scalable) }, + { name: 'roentgen (R)', id: 'radr', fn: SIPrefix('R', 0, scalable) }, + { name: 'Sievert/hour (Sv/h)', id: 'radsvh', fn: SIPrefix('Sv/h', 0, scalable) }, + { name: 'milliSievert/hour (mSv/h)', id: 'radmsvh', fn: SIPrefix('Sv/h', -1, scalable) }, + { name: 'microSievert/hour (µSv/h)', id: 'radusvh', fn: SIPrefix('Sv/h', -2, scalable) }, ], }, { name: 'Rotational Speed', formats: [ { name: 'Revolutions per minute (rpm)', id: 'rotrpm', fn: toFixedUnit('rpm') }, - { name: 'Hertz (Hz)', id: 'rothz', fn: SIPrefix('Hz') }, - { name: 'Kilohertz (kHz)', id: 'rotkhz', fn: SIPrefix('Hz', 1) }, - { name: 'Megahertz (MHz)', id: 'rotmhz', fn: SIPrefix('Hz', 2) }, - { name: 'Gigahertz (GHz)', id: 'rotghz', fn: SIPrefix('Hz', 3) }, + { name: 'Hertz (Hz)', id: 'rothz', fn: SIPrefix('Hz', 0, scalable) }, + { name: 'Kilohertz (kHz)', id: 'rotkhz', fn: SIPrefix('Hz', 1, scalable) }, + { name: 'Megahertz (MHz)', id: 'rotmhz', fn: SIPrefix('Hz', 2, scalable) }, + { name: 'Gigahertz (GHz)', id: 'rotghz', fn: SIPrefix('Hz', 3, scalable) }, { name: 'Radians per second (rad/s)', id: 'rotrads', fn: toFixedUnit('rad/s') }, { name: 'Degrees per second (°/s)', id: 'rotdegs', fn: toFixedUnit('°/s') }, ], @@ -367,7 +367,7 @@ export const getCategories = (): ValueFormatCategory[] => [ { name: 'Time', formats: [ - { name: 'Hertz (1/s)', id: 'hertz', fn: SIPrefix('Hz') }, + { name: 'Hertz (1/s)', id: 'hertz', fn: SIPrefix('Hz', 0, scalable) }, { name: 'nanoseconds (ns)', id: 'ns', fn: toNanoSeconds }, { name: 'microseconds (µs)', id: 'µs', fn: toMicroSeconds }, { name: 'milliseconds (ms)', id: 'ms', fn: toMilliSeconds }, @@ -420,8 +420,8 @@ export const getCategories = (): ValueFormatCategory[] => [ { name: 'Volume', formats: [ - { name: 'millilitre (mL)', id: 'mlitre', fn: SIPrefix('L', -1) }, - { name: 'litre (L)', id: 'litre', fn: SIPrefix('L') }, + { name: 'millilitre (mL)', id: 'mlitre', fn: SIPrefix('L', -1, scalable) }, + { name: 'litre (L)', id: 'litre', fn: SIPrefix('L', 0, scalable) }, { name: 'cubic meter', id: 'm3', fn: toFixedUnit('m³') }, { name: 'Normal cubic meter', id: 'Nm3', fn: toFixedUnit('Nm³') }, { name: 'cubic decimeter', id: 'dm3', fn: toFixedUnit('dm³') }, diff --git a/packages/grafana-data/src/valueFormats/symbolFormatters.test.ts b/packages/grafana-data/src/valueFormats/symbolFormatters.test.ts index 05273473e84..0fe8684bc5f 100644 --- a/packages/grafana-data/src/valueFormats/symbolFormatters.test.ts +++ b/packages/grafana-data/src/valueFormats/symbolFormatters.test.ts @@ -82,3 +82,30 @@ describe('SIPrefix', () => { expect(text).toEqual(expectedText); }); }); + +describe('SIPrefix with scalable set to false', () => { + const symbol = 'V'; + const fmtFunc = SIPrefix(symbol, 0, false); + + it.each` + value | expectedSuffix | expectedText + ${0} | ${' V'} | ${'0'} + ${999} | ${' V'} | ${'999'} + ${1000} | ${' V'} | ${'1000'} + ${1000000} | ${' V'} | ${'1000000'} + ${1000000000} | ${' V'} | ${'1000000000'} + ${1000000000000} | ${' V'} | ${'1000000000000'} + ${1000000000000000} | ${' V'} | ${'1000000000000000'} + ${-1000000000000} | ${' V'} | ${'-1000000000000'} + ${-1000000000} | ${' V'} | ${'-1000000000'} + ${-1000000} | ${' V'} | ${'-1000000'} + ${-1000} | ${' V'} | ${'-1000'} + ${-999} | ${' V'} | ${'-999'} + `('when called with value:{$value}', ({ value, expectedSuffix, expectedText }) => { + const { prefix, suffix, text } = fmtFunc(value); + + expect(prefix).toEqual(undefined); + expect(suffix).toEqual(expectedSuffix); + expect(text).toEqual(expectedText); + }); +}); diff --git a/packages/grafana-data/src/valueFormats/symbolFormatters.ts b/packages/grafana-data/src/valueFormats/symbolFormatters.ts index d9cdc85616c..1c633bbb8f4 100644 --- a/packages/grafana-data/src/valueFormats/symbolFormatters.ts +++ b/packages/grafana-data/src/valueFormats/symbolFormatters.ts @@ -1,6 +1,6 @@ import { DecimalCount } from '../types/displayValue'; -import { scaledUnits, ValueFormatter } from './valueFormats'; +import { scaledUnits, ValueFormatter, toFixedUnit } from './valueFormats'; export function currency(symbol: string, asSuffix?: boolean): ValueFormatter { const units = ['', 'K', 'M', 'B', 'T']; @@ -41,7 +41,10 @@ export function binaryPrefix(unit: string, offset = 0): ValueFormatter { return scaledUnits(1024, units, offset); } -export function SIPrefix(unit: string, offset = 0): ValueFormatter { +export function SIPrefix(unit: string, offset = 0, scalable = true): ValueFormatter { const units = SI_PREFIXES.map((p) => ' ' + p + unit); + if (!scalable) { + return toFixedUnit(SI_PREFIXES[SI_BASE_INDEX + offset] + unit); + } return scaledUnits(1000, units, SI_BASE_INDEX + offset); } diff --git a/packages/grafana-data/src/valueFormats/valueFormats.test.ts b/packages/grafana-data/src/valueFormats/valueFormats.test.ts index b0776ae4b41..40adc6a7bf2 100644 --- a/packages/grafana-data/src/valueFormats/valueFormats.test.ts +++ b/packages/grafana-data/src/valueFormats/valueFormats.test.ts @@ -154,4 +154,16 @@ describe('valueFormats', () => { expect(fmt0).toEqual(fmt1); }); }); + + describe('getValueFormat with scalable set to false', () => { + it.each` + value | expected + ${1000000} | ${'1000000 W'} + ${0.001} | ${'0.00100 W'} + `('should return a fixed unit regardless of the input value', ({ value, expected }) => { + const result = getValueFormat('watt', false)(value); + const full = formattedValueToString(result); + expect(full).toBe(expected); + }); + }); }); diff --git a/packages/grafana-data/src/valueFormats/valueFormats.ts b/packages/grafana-data/src/valueFormats/valueFormats.ts index e4d99adcc91..4f7333681ac 100644 --- a/packages/grafana-data/src/valueFormats/valueFormats.ts +++ b/packages/grafana-data/src/valueFormats/valueFormats.ts @@ -43,6 +43,7 @@ export interface ValueFormatterIndex { // Globals & formats cache let categories: ValueFormatCategory[] = []; const index: ValueFormatterIndex = {}; +const indexScalable: ValueFormatterIndex = {}; let hasBuiltIndex = false; export function toFixed(value: number, decimals?: DecimalCount): string { @@ -199,6 +200,7 @@ export function stringFormater(value: number): FormattedValue { function buildFormats() { categories = getCategories(); + const nonScalableCategories = getCategories(false); for (const cat of categories) { for (const format of cat.formats) { @@ -206,18 +208,28 @@ function buildFormats() { } } + for (const cat of nonScalableCategories) { + for (const format of cat.formats) { + indexScalable[format.id] = format.fn; + } + } + // Resolve units pointing to old IDs [{ from: 'farenheit', to: 'fahrenheit' }].forEach((alias) => { - const f = index[alias.to]; - if (f) { - index[alias.from] = f; + const f0 = index[alias.to]; + if (f0) { + index[alias.from] = f0; + } + const f1 = indexScalable[alias.to]; + if (f1) { + indexScalable[alias.from] = f1; } }); hasBuiltIndex = true; } -export function getValueFormat(id?: string | null): ValueFormatter { +export function getValueFormat(id?: string | null, scalable = true): ValueFormatter { if (!id) { return toFixedUnit(''); } @@ -226,7 +238,7 @@ export function getValueFormat(id?: string | null): ValueFormatter { buildFormats(); } - const fmt = index[id]; + const fmt = scalable ? index[id] : indexScalable[id]; if (!fmt && id) { let idx = id.indexOf(':'); @@ -278,11 +290,14 @@ export function getValueFormat(id?: string | null): ValueFormatter { return fmt; } -export function getValueFormatterIndex(): ValueFormatterIndex { +export function getValueFormatterIndex(scalable = true): ValueFormatterIndex { if (!hasBuiltIndex) { buildFormats(); } + if (scalable) { + return indexScalable; + } return index; } diff --git a/public/app/core/components/OptionsUI/registry.tsx b/public/app/core/components/OptionsUI/registry.tsx index dfe87a79436..ec01fd133a5 100644 --- a/public/app/core/components/OptionsUI/registry.tsx +++ b/public/app/core/components/OptionsUI/registry.tsx @@ -247,7 +247,22 @@ export const getAllStandardFieldConfigs = () => { category, }; - const fieldMinMax: FieldConfigPropertyItem = { + const unitScale: FieldConfigPropertyItem = { + id: 'unitScale', + path: 'unitScale', + name: 'Scale units', + description: 'Automatically scale units relative to magnitude of the value', + + editor: standardEditorsRegistry.get('boolean').editor, + override: standardEditorsRegistry.get('boolean').editor, + process: booleanOverrideProcessor, + + defaultValue: true, + shouldApply: () => true, + category, + }; + + const fieldMinMax: FieldConfigPropertyItem<{ min: number; max: number }, boolean, BooleanFieldSettings> = { id: 'fieldMinMax', path: 'fieldMinMax', name: 'Field min/max', @@ -416,5 +431,19 @@ export const getAllStandardFieldConfigs = () => { category, }; - return [unit, min, max, fieldMinMax, decimals, displayName, color, noValue, links, mappings, thresholds, filterable]; + return [ + unit, + unitScale, + min, + max, + fieldMinMax, + decimals, + displayName, + color, + noValue, + links, + mappings, + thresholds, + filterable, + ]; };