diff --git a/tensorboard/webapp/widgets/histogram/BUILD b/tensorboard/webapp/widgets/histogram/BUILD index 807e834b82..aefd283d0c 100644 --- a/tensorboard/webapp/widgets/histogram/BUILD +++ b/tensorboard/webapp/widgets/histogram/BUILD @@ -21,6 +21,7 @@ tf_ng_module( ":histogram_styles", ], deps = [ + ":formatter", ":types", "//tensorboard/webapp/third_party:d3", "//tensorboard/webapp/widgets:resize_detector", @@ -37,11 +38,13 @@ tf_ts_library( name = "histogram_test", testonly = True, srcs = [ + "formatter_test.ts", "histogram_card_fob_test.ts", "histogram_test.ts", "histogram_util_test.ts", ], deps = [ + ":formatter", ":histogram", ":types", "//tensorboard/webapp/angular:expect_angular_core_testing", @@ -66,3 +69,14 @@ tf_ts_library( "//tensorboard/webapp:tb_polymer_interop_types", ], ) + +tf_ts_library( + name = "formatter", + srcs = [ + "formatter.ts", + ], + visibility = ["//tensorboard:internal"], + deps = [ + "//tensorboard/webapp/third_party:d3", + ], +) diff --git a/tensorboard/webapp/widgets/histogram/formatter.ts b/tensorboard/webapp/widgets/histogram/formatter.ts new file mode 100644 index 0000000000..b9072b231f --- /dev/null +++ b/tensorboard/webapp/widgets/histogram/formatter.ts @@ -0,0 +1,41 @@ +/* Copyright 2022 The TensorFlow Authors. All Rights Reserved. + +Licensed 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 * as d3 from '../../third_party/d3'; + +const LARGE_NUMBER = 10000; +const SMALL_NUMBER = 0.001; + +const d3LargeFormatter = d3.format('.2~s'); +const d3MiddleFormatter = d3.format('.4~r'); +const d3SmallFormatter = d3.format('.2~e'); + +export function formatTickNumber(x: number | {valueOf(): number}): string { + /** + * Formats very large nubmers using SI notation, very small numbers + * using exponential notation, and mid-sized numbers using their + * natural printout. + */ + if (x === 0) { + return '0'; + } + const absNum = Math.abs(x as number); + if (absNum >= LARGE_NUMBER) { + return d3LargeFormatter(x); + } + if (absNum < SMALL_NUMBER) { + return d3SmallFormatter(x); + } + return d3MiddleFormatter(x); +} diff --git a/tensorboard/webapp/widgets/histogram/formatter_test.ts b/tensorboard/webapp/widgets/histogram/formatter_test.ts new file mode 100644 index 0000000000..3fa4e85698 --- /dev/null +++ b/tensorboard/webapp/widgets/histogram/formatter_test.ts @@ -0,0 +1,52 @@ +/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. + +Licensed 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 {formatTickNumber} from './formatter'; + +describe('histogram/formatter test', () => { + describe('formatTickNumber', () => { + it('formats small numbers with 4 sig. digits', () => { + expect(formatTickNumber(0)).toBe('0'); + expect(formatTickNumber(1)).toBe('1'); + expect(formatTickNumber(1.000000000001)).toBe('1'); + expect(formatTickNumber(5)).toBe('5'); + expect(formatTickNumber(-100.4)).toBe('-100.4'); + expect(formatTickNumber(3.01)).toBe('3.01'); + expect(formatTickNumber(9999)).toBe('9999'); + expect(formatTickNumber(0.09)).toBe('0.09'); + }); + + it('formats small numbers in exponential format with 2 sig. digits', () => { + expect(formatTickNumber(-0.000050000001)).toBe('-5e-5'); + expect(formatTickNumber(0.00005)).toBe('5e-5'); + expect(formatTickNumber(2.3411e-14)).toBe('2.34e-14'); + expect(formatTickNumber(-2.3411e-14)).toBe('-2.34e-14'); + }); + + it('formats large numbers in SI format with 2 sig. digits', () => { + expect(formatTickNumber(1.004e6)).toBe('1M'); + expect(formatTickNumber(-1.004e6)).toBe('-1M'); + expect(formatTickNumber(1.004e13)).toBe('10T'); + expect(formatTickNumber(-1.004e13)).toBe('-10T'); + }); + + it('fails to format large number with many decimals nicely', () => { + // This causes TensorBoard to format axis in less than ideal when spread of a + // viewBox is miniscule compared to the number. e.g., you see axis that says, + // "1G", "1G", "1G" which is quite meaningless. It will be addressed in the future. + expect(formatTickNumber(1e9 + 0.00000001)).toBe('1G'); + expect(formatTickNumber(1e9 - 0.00000001)).toBe('1G'); + }); + }); +}); diff --git a/tensorboard/webapp/widgets/histogram/histogram_component.ts b/tensorboard/webapp/widgets/histogram/histogram_component.ts index 1c7a4a9455..5508083d3e 100644 --- a/tensorboard/webapp/widgets/histogram/histogram_component.ts +++ b/tensorboard/webapp/widgets/histogram/histogram_component.ts @@ -30,6 +30,7 @@ import {takeUntil} from 'rxjs/operators'; import * as d3 from '../../third_party/d3'; import {HCLColor} from '../../third_party/d3'; import {TimeSelection} from '../card_fob/card_fob_types'; +import {formatTickNumber} from './formatter'; import { Bin, HistogramData, @@ -113,8 +114,9 @@ export class HistogramComponent implements AfterViewInit, OnChanges, OnDestroy { contentClientRect: {height: 0, width: 0}, }; scales: Scales | null = null; + private formatters = { - binNumber: d3.format('.3~s'), + binNumber: formatTickNumber, count: d3.format('.3n'), // DefinitelyTyped is incorrect that the `timeFormat` only takes `Date` as // an input. Better type it for downstream types.