diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/utils/D3Formatting.ts b/superset-frontend/packages/superset-ui-chart-controls/src/utils/D3Formatting.ts index a8fd6312cbd1f..5541c4a4b4574 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/D3Formatting.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/D3Formatting.ts @@ -57,6 +57,8 @@ export const D3_FORMAT_OPTIONS: [string, string][] = [ ...d3Formatted, ['DURATION', t('Duration in ms (66000 => 1m 6s)')], ['DURATION_SUB', t('Duration in ms (1.40008 => 1ms 400µs 80ns)')], + ['MEMORY_DECIMAL', t('Memory in bytes - decimal (1024B => 1.024kB)')], + ['MEMORY_BINARY', t('Memory in bytes - binary (1024B => 1KiB)')], ]; export const D3_TIME_FORMAT_DOCS = t( diff --git a/superset-frontend/packages/superset-ui-core/src/number-format/factories/createMemoryFormatter.ts b/superset-frontend/packages/superset-ui-core/src/number-format/factories/createMemoryFormatter.ts new file mode 100644 index 0000000000000..193527b759dc4 --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/number-format/factories/createMemoryFormatter.ts @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import NumberFormatter from '../NumberFormatter'; + +export default function createMemoryFormatter( + config: { + description?: string; + id?: string; + label?: string; + binary?: boolean; + decimals?: number; + } = {}, +) { + const { description, id, label, binary, decimals = 2 } = config; + + return new NumberFormatter({ + description, + formatFunc: value => { + if (value === 0) return '0B'; + + const sign = value > 0 ? '' : '-'; + value = Math.abs(value); + + const suffixes = binary + ? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] + : ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'RB', 'QB']; + const base = binary ? 1024 : 1000; + + const i = Math.min( + suffixes.length - 1, + Math.floor(Math.log(value) / Math.log(base)), + ); + return `${sign}${parseFloat((value / Math.pow(base, i)).toFixed(decimals))}${suffixes[i]}`; + }, + id: id ?? 'memory_format', + label: label ?? `Memory formatter`, + }); +} diff --git a/superset-frontend/packages/superset-ui-core/src/number-format/index.ts b/superset-frontend/packages/superset-ui-core/src/number-format/index.ts index c65537552ee41..b9835d332d0e0 100644 --- a/superset-frontend/packages/superset-ui-core/src/number-format/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/number-format/index.ts @@ -31,5 +31,6 @@ export { export { default as NumberFormatterRegistry } from './NumberFormatterRegistry'; export { default as createD3NumberFormatter } from './factories/createD3NumberFormatter'; export { default as createDurationFormatter } from './factories/createDurationFormatter'; +export { default as createMemoryFormatter } from './factories/createMemoryFormatter'; export { default as createSiAtMostNDigitFormatter } from './factories/createSiAtMostNDigitFormatter'; export { default as createSmartNumberFormatter } from './factories/createSmartNumberFormatter'; diff --git a/superset-frontend/packages/superset-ui-core/test/number-format/factories/createMemoryFormatter.test.ts b/superset-frontend/packages/superset-ui-core/test/number-format/factories/createMemoryFormatter.test.ts new file mode 100644 index 0000000000000..90a44845135fd --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/test/number-format/factories/createMemoryFormatter.test.ts @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { NumberFormatter, createMemoryFormatter } from '@superset-ui/core'; + +describe('createMemoryFormatter()', () => { + it('creates an instance of MemoryFormatter', () => { + const formatter = createMemoryFormatter(); + expect(formatter).toBeInstanceOf(NumberFormatter); + }); + it('formats bytes in human readable format with default options', () => { + const formatter = createMemoryFormatter(); + expect(formatter(0)).toBe('0B'); + expect(formatter(50)).toBe('50B'); + expect(formatter(555)).toBe('555B'); + expect(formatter(1000)).toBe('1kB'); + expect(formatter(1111)).toBe('1.11kB'); + expect(formatter(1024)).toBe('1.02kB'); + expect(formatter(1337)).toBe('1.34kB'); + expect(formatter(1999)).toBe('2kB'); + expect(formatter(10 * 1000)).toBe('10kB'); + expect(formatter(100 * 1000)).toBe('100kB'); + expect(formatter(Math.pow(1000, 2))).toBe('1MB'); + expect(formatter(Math.pow(1000, 3))).toBe('1GB'); + expect(formatter(Math.pow(1000, 4))).toBe('1TB'); + expect(formatter(Math.pow(1000, 5))).toBe('1PB'); + expect(formatter(Math.pow(1000, 6))).toBe('1EB'); + expect(formatter(Math.pow(1000, 7))).toBe('1ZB'); + expect(formatter(Math.pow(1000, 8))).toBe('1YB'); + expect(formatter(Math.pow(1000, 9))).toBe('1RB'); + expect(formatter(Math.pow(1000, 10))).toBe('1QB'); + expect(formatter(Math.pow(1000, 11))).toBe('1000QB'); + expect(formatter(Math.pow(1000, 12))).toBe('1000000QB'); + }); + it('formats negative bytes in human readable format with default options', () => { + const formatter = createMemoryFormatter(); + expect(formatter(-50)).toBe('-50B'); + }); + it('formats float bytes in human readable format with default options', () => { + const formatter = createMemoryFormatter(); + expect(formatter(10.666)).toBe('10.67B'); + expect(formatter(1200.666)).toBe('1.2kB'); + }); + it('formats bytes in human readable format with additional binary option', () => { + const formatter = createMemoryFormatter({ binary: true }); + expect(formatter(0)).toBe('0B'); + expect(formatter(50)).toBe('50B'); + expect(formatter(555)).toBe('555B'); + expect(formatter(1000)).toBe('1000B'); + expect(formatter(1111)).toBe('1.08KiB'); + expect(formatter(1024)).toBe('1KiB'); + expect(formatter(1337)).toBe('1.31KiB'); + expect(formatter(2047)).toBe('2KiB'); + expect(formatter(10 * 1024)).toBe('10KiB'); + expect(formatter(100 * 1024)).toBe('100KiB'); + expect(formatter(Math.pow(1024, 2))).toBe('1MiB'); + expect(formatter(Math.pow(1024, 3))).toBe('1GiB'); + expect(formatter(Math.pow(1024, 4))).toBe('1TiB'); + expect(formatter(Math.pow(1024, 5))).toBe('1PiB'); + expect(formatter(Math.pow(1024, 6))).toBe('1EiB'); + expect(formatter(Math.pow(1024, 7))).toBe('1ZiB'); + expect(formatter(Math.pow(1024, 8))).toBe('1YiB'); + expect(formatter(Math.pow(1024, 9))).toBe('1024YiB'); + expect(formatter(Math.pow(1024, 10))).toBe('1048576YiB'); + }); + it('formats bytes in human readable format with additional decimals option', () => { + const formatter0decimals = createMemoryFormatter({ decimals: 0 }); + expect(formatter0decimals(0)).toBe('0B'); + expect(formatter0decimals(1111)).toBe('1kB'); + + const formatter3decimals = createMemoryFormatter({ decimals: 3 }); + expect(formatter3decimals(0)).toBe('0B'); + expect(formatter3decimals(1111)).toBe('1.111kB'); + }); +}); diff --git a/superset-frontend/packages/superset-ui-core/test/number-format/index.test.ts b/superset-frontend/packages/superset-ui-core/test/number-format/index.test.ts index 09395e722e6e2..103f5e44a9b7d 100644 --- a/superset-frontend/packages/superset-ui-core/test/number-format/index.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/number-format/index.test.ts @@ -21,6 +21,7 @@ import { createD3NumberFormatter, createDurationFormatter, createSiAtMostNDigitFormatter, + createMemoryFormatter, formatNumber, getNumberFormatter, getNumberFormatterRegistry, @@ -35,6 +36,7 @@ describe('index', () => { createD3NumberFormatter, createDurationFormatter, createSiAtMostNDigitFormatter, + createMemoryFormatter, formatNumber, getNumberFormatter, getNumberFormatterRegistry, diff --git a/superset-frontend/src/setup/setupFormatters.ts b/superset-frontend/src/setup/setupFormatters.ts index e18aeba9dcb3e..384b1be9e30ae 100644 --- a/superset-frontend/src/setup/setupFormatters.ts +++ b/superset-frontend/src/setup/setupFormatters.ts @@ -28,6 +28,7 @@ import { createSmartDateFormatter, createSmartDateVerboseFormatter, createSmartDateDetailedFormatter, + createMemoryFormatter, } from '@superset-ui/core'; import { FormatLocaleDefinition } from 'd3-format'; import { TimeLocaleDefinition } from 'd3-time-format'; @@ -76,7 +77,9 @@ export default function setupFormatters( .registerValue( 'DURATION_SUB', createDurationFormatter({ formatSubMilliseconds: true }), - ); + ) + .registerValue('MEMORY_DECIMAL', createMemoryFormatter({ binary: false })) + .registerValue('MEMORY_BINARY', createMemoryFormatter({ binary: true })); const timeFormatterRegistry = getTimeFormatterRegistry();