Public
Edited
Jun 19, 2024
Insert cell
Insert cell
// change me
numSignficantFigures = 3
Insert cell
// change me
values = [
123456789012, 19.985, 0.9999, 0.000006, 0.000056, 0.000456, 0.003456,
0.023456, 0.123445, 0.1, 0.12, 0.123, 1.23456, 12.34567, 123, 1234, 12345,
493049, 999999, 1000000, 389040247492
]
Insert cell
printFormattedValues(values, {
roundingMode: SIGNIFICANCE,
numSignificantFigures: numSignficantFigures
})
Insert cell
printFormattedValues(values, {
roundingMode: SIGNIFICANCE,
numSignificantFigures: numSignficantFigures,
numberAbbreviation: "short"
})
Insert cell
Insert cell
printFormattedValues([999999.99, 999999.999, 1000000], {
roundingMode: DECIMAL,
numDecimalPlaces: 2
})
Insert cell
Insert cell
printFormattedValues = (values, options) => {
const rounded = values.map((value) => {
const formatted = formatValue(value, options);
return { value, formatted };
});
return (
"\n" +
(rounded
.map((item) => `${item.value} -> ${item.formatted.join(" / ")}`)
.join("\n") +
"\n")
);
}
Insert cell
DECIMAL = "decimal"
Insert cell
SIGNIFICANCE = "significance"
Insert cell
function formatValue(
value,
{
roundingMode = DECIMAL,
trailingZeroes = false, // only applies to fixed-point notation
unit = "",
spaceBeforeUnit = !checkIsUnitPercent(unit),
useNoBreakSpace = false,
showPlus = false,
numDecimalPlaces = 2,
numSignificantFigures = 3,
numberAbbreviation = "long"
} = {}
) {
const formatter = createFormatter(unit);

// Explore how specifiers work here
// https://observablehq.com/@ikesau/d3-format-interactive-demo
const specifier = new d3.FormatSpecifier({
zero: "0",
trim: getTrim({
roundingMode,
trailingZeroes
}),
sign: getSign({ showPlus }),
symbol: getSymbol({ unit }),
comma: ",",
precision: getPrecision({
roundingMode,
value,
numDecimalPlaces,
numSignificantFigures,
type: getType({ roundingMode, numberAbbreviation, value, unit })
}),
type: getType({ roundingMode, numberAbbreviation, value, unit })
}).toString();

const formattedString = formatter(specifier)(value);

const postprocessedString = postprocessString({
string: formattedString,
roundingMode,
numberAbbreviation,
spaceBeforeUnit,
useNoBreakSpace,
unit,
value,
numDecimalPlaces
});

return [postprocessedString];
}
Insert cell
function postprocessString({
string,
roundingMode,
numberAbbreviation,
spaceBeforeUnit,
useNoBreakSpace,
unit,
value,
numDecimalPlaces
}) {
let output = string;

// handling infinitesimal values
if (roundingMode !== SIGNIFICANCE) {
const tooSmallThreshold = Math.pow(10, -numDecimalPlaces).toPrecision(1);
if (numberAbbreviation && 0 < value && value < +tooSmallThreshold) {
output = "<" + output.replace(/0\.?(\d+)?/, tooSmallThreshold);
}
}

if (numberAbbreviation) {
output = replaceSIPrefixes({
string: output,
numberAbbreviation
});
}

if (unit && !checkIsUnitCurrency(unit)) {
const spaceCharacter = useNoBreakSpace ? "\u00a0" : " ";
const appendage = spaceBeforeUnit ? spaceCharacter + unit : unit;
output += appendage;
}

return output;
}
Insert cell
function replaceSIPrefixes({ string, numberAbbreviation }) {
const prefix = string[string.length - 1];

const prefixMap = {
short: {
k: "k",
M: "M",
G: "B",
T: "T",
P: "quad",
E: "quint",
Z: "sext",
Y: "sept"
},
long: {
k: "k",
M: " million",
G: " billion",
T: " trillion",
P: " quadrillion",
E: " quintillion",
Z: " sextillion",
Y: " septillion"
}
};

if (prefixMap[numberAbbreviation][prefix]) {
return string.replace(prefix, prefixMap[numberAbbreviation][prefix]);
}
return string;
}
Insert cell
function getPrecision({
value,
roundingMode,
numDecimalPlaces,
numSignificantFigures,
type
}) {
if (roundingMode === SIGNIFICANCE) {
return `${numSignificantFigures}`;
}

if (type === "f") {
return `${numDecimalPlaces}`;
}

// when dealing with abbreviated numbers, adjust precision so we get 12.84 million instead of 13 million
// the modulo one-liner counts the "place columns" of the number, resetting every 3
// 1 -> 1, 48 -> 2, 981 -> 3, 7222 -> 1
const numberOfDigits = String(Math.floor(Math.abs(value))).length;
const precisionPadding = ((numberOfDigits - 1) % 3) + 1;

// hard-coded 2 decimal places for abbreviated numbers
return `${precisionPadding + 2}`;
}
Insert cell
function getType({ roundingMode, numberAbbreviation, value, unit }) {
// f: fixed-point notation (i.e. fixed number of decimal points)
// r: decimal notation, rounded to significant digits
// s: decimal notation with an SI prefix, rounded to significant digits

const typeMap = {
[DECIMAL]: "f",
[SIGNIFICANCE]: "r"
};
const type = typeMap[roundingMode];

if (checkIsUnitPercent(unit)) {
return type;
}
if (numberAbbreviation === "long") {
// do not abbreviate until 1 million
return Math.abs(value) < 1e6 ? type : "s";
}
if (numberAbbreviation === "short") {
// do not abbreviate until 1 thousand
return Math.abs(value) < 1e3 ? type : "s";
}

return type;
}
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
function getSymbol({ unit }) {
return checkIsUnitCurrency(unit) ? "$" : "";
}
Insert cell
function getSign({ showPlus }) {
return showPlus ? "+" : "";
}
Insert cell
function getTrim({ roundingMode, trailingZeroes }) {
// always show trailing zeroes when rounding to significant figures
return roundingMode === SIGNIFICANCE ? "" : trailingZeroes ? "" : "~";
}
Insert cell
function checkIsUnitPercent(unit) {
return unit[0] === "%";
}
Insert cell
function checkIsUnitCurrency(unit) {
return ["$", "£"].includes(unit);
}
Insert cell
// Used outside this module to figure out if the unit will be joined with the number.
function checkIsVeryShortUnit(unit) {
return ["%", "$", "£"].includes(unit);
}
Insert cell
createFormatter = (currency) =>
d3.formatLocale({
decimal: ".",
thousands: ",",
grouping: [3],
minus: "-",
currency: [currency, ""]
}).format
Insert cell
d3 = require("d3@6.1.1")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more