UNPKG

@amcharts/amcharts4

Version:
812 lines 31.4 kB
/** * Number formatting-related functionality. */ import { __extends } from "tslib"; import { Language } from "../utils/Language"; import { BaseObject } from "../Base"; import { getTextFormatter } from "../formatters/TextFormatter"; import { registry } from "../Registry"; import * as $strings from "../utils/Strings"; import * as $object from "../utils/Object"; import * as $utils from "../utils/Utils"; import * as $type from "../utils/Type"; import * as $math from "../utils/Math"; /** * NumberFormatter class. Formats numbers according to specified formats. * * @todo Apply translations to suffixes/prefixes */ var NumberFormatter = /** @class */ (function (_super) { __extends(NumberFormatter, _super); /** * Constructor */ function NumberFormatter() { var _this = _super.call(this) || this; /** * A base value for negative numbers. Will treat all numbers below this value * as negative numbers. */ _this._negativeBase = 0; /** * Holds number format. * * @default #,###.##### */ _this._numberFormat = "#,###.#####"; /** * Output format to produce. If the format calls for applying color to the * formatted value, this setting will determine what markup to use: SVG or * HTML. * * Available options: svg, html. * * @default "svg" */ _this._outputFormat = "svg"; /** * Any number smaller than this will be considered "small" number, which will * trigger special formatting if "a" format modifier is used. */ _this._smallNumberThreshold = 1.00; _this._forceLTR = false; _this.className = "NumberFormatter"; _this.applyTheme(); return _this; } NumberFormatter.prototype.dispose = function () { _super.prototype.dispose.call(this); if (this.language) { this.language.dispose(); } }; Object.defineProperty(NumberFormatter.prototype, "language", { /** * @return Language */ get: function () { if (!this._language) { if (this.sprite) { this._language = this.sprite.language; } else { this._language = new Language; } } return this._language; }, /** * A reference to [[Language]] instance. * * Formatter will use language to translate various items, like number * suffixes, etc. * * @param value Language */ set: function (value) { this._language = value; }, enumerable: true, configurable: true }); /** * Formats the number according to specific format. * * @param value Value to format * @param format Format to apply * @return Formatted number */ NumberFormatter.prototype.format = function (value, format, precision) { // no format passed in or "Number" if (typeof format === "undefined" || ($type.isString(format) && format.toLowerCase() === "number")) { format = this._numberFormat; } // Init return value var formatted; // Cast to number just in case // TODO: maybe use better casting var source = Number(value); // Is it a built-in format or Intl.NumberFormatOptions if ($type.isObject(format)) { try { if (this.intlLocales) { return new Intl.NumberFormat(this.intlLocales, format).format(source); } else { return new Intl.NumberFormat(undefined, format).format(source); } } catch (e) { return "Invalid"; } } else { // Clean format format = $utils.cleanFormat(format); // Get format info (it will also deal with parser caching) var info = this.parseFormat(format, this.language); // format and replace the number var details = void 0; if (source > this._negativeBase) { details = info.positive; } else if (source < this._negativeBase) { details = info.negative; } else { details = info.zero; } // Adjust precision if ($type.hasValue(precision) && !details.mod) { details = $object.clone(details); details.decimals.active = source == 0 ? 0 : precision; } // Format formatted = details.template.split($strings.PLACEHOLDER).join(this.applyFormat(source, details)); } if (this.forceLTR) { formatted = "‎" + formatted; } return formatted; }; /** * Parses supplied format into structured object which can be used to format * the number. * * @param format Format string, i.e. "#,###.00" * @param language Language */ NumberFormatter.prototype.parseFormat = function (format, language) { var _this = this; // Check cache var cached = this.getCache(format); if ($type.hasValue(cached)) { return cached; } // init format parse info holder var info = { "positive": { "thousands": { "active": -1, "passive": -1, "interval": -1, "separator": language.translateEmpty("_thousandSeparator") }, "decimals": { "active": -1, "passive": -1, "separator": language.translateEmpty("_decimalSeparator") }, "template": "", "source": "", "parsed": false }, "negative": { "thousands": { "active": -1, "passive": -1, "interval": -1, "separator": language.translateEmpty("_thousandSeparator") }, "decimals": { "active": -1, "passive": -1, "separator": language.translateEmpty("_decimalSeparator") }, "template": "", "source": "", "parsed": false }, "zero": { "thousands": { "active": -1, "passive": -1, "interval": -1, "separator": language.translateEmpty("_thousandSeparator") }, "decimals": { "active": -1, "passive": -1, "separator": language.translateEmpty("_decimalSeparator") }, "template": "", "source": "", "parsed": false } }; // Escape double vertical bars (that mean display one vertical bar) format = format.replace("||", $strings.PLACEHOLDER2); // Split it up and deal with different formats var parts = format.split("|"); info.positive.source = parts[0]; if (typeof parts[2] === "undefined") { info.zero = info.positive; } else { info.zero.source = parts[2]; } if (typeof parts[1] === "undefined") { info.negative = info.positive; } else { info.negative.source = parts[1]; } // Parse each $object.each(info, function (part, item) { // Already parsed if (item.parsed) { return; } // Check cached if (typeof _this.getCache(item.source) !== "undefined") { info[part] = _this.getCache(item.source); return; } // Begin parsing var partFormat = item.source; // Just "Number"? if (partFormat.toLowerCase() === "number") { partFormat = $type.isString(_this._numberFormat) ? _this._numberFormat : "#,###.#####"; } // Let TextFormatter split into chunks var chunks = getTextFormatter().chunk(partFormat, true); for (var i = 0; i < chunks.length; i++) { var chunk = chunks[i]; // replace back double vertical bar chunk.text = chunk.text.replace($strings.PLACEHOLDER2, "|"); if (chunk.type === "value") { // Parse format // Look for codes var matches = chunk.text.match(/[#0.,]+[ ]?[abespABESP%!]?[abespABESP‰!]?/); if (matches) { if (matches === null || matches[0] === "") { // no codes here - assume string // nothing to do here item.template += chunk.text; } else { // look for the format modifiers at the end var mods = matches[0].match(/[abespABESP%‰!]{2}|[abespABESP%‰]{1}$/); if (mods) { item.mod = mods[0].toLowerCase(); item.modSpacing = matches[0].match(/[ ]{1}[abespABESP%‰!]{1}$/) ? true : false; } // break the format up var a = matches[0].split("."); // Deal with thousands if (a[0] === "") { // No directives for thousands // Leave default settings (no formatting) } else { // Counts item.thousands.active = (a[0].match(/0/g) || []).length; item.thousands.passive = (a[0].match(/\#/g) || []).length + item.thousands.active; // Separator interval var b = a[0].split(","); if (b.length === 1) { // No thousands separators // Do nothing } else { // Use length fo the last chunk as thousands length item.thousands.interval = $type.getValue(b.pop()).length; if (item.thousands.interval === 0) { item.thousands.interval = -1; } } } // Deal with decimals if (typeof (a[1]) === "undefined") { // No directives for decimals // Leave at defaults (no formatting) } else { // Counts item.decimals.active = (a[1].match(/0/g) || []).length; item.decimals.passive = (a[1].match(/\#/g) || []).length + item.decimals.active; } // Add special code to template item.template += chunk.text.split(matches[0]).join($strings.PLACEHOLDER); } } } else { // Quoted string - take it as it is item.template += chunk.text; } } // Apply style formatting //item.template = getTextFormatter().format(item.template, this.outputFormat); // Save cache _this.setCache(item.source, item); // Mark this as parsed item.parsed = true; }); // Save cache (the whole thing) this.setCache(format, info); return info; }; /** * Applies parsed format to a numeric value. * * @param value Value * @param details Parsed format as returned by parseFormat() * @return Formatted number */ NumberFormatter.prototype.applyFormat = function (value, details) { // Use absolute values var negative = value < 0; value = Math.abs(value); // Recalculate according to modifier var prefix = "", suffix = ""; var mods = details.mod ? details.mod.split("") : []; if (mods.indexOf("b") !== -1) { var a_1 = this.applyPrefix(value, this.bytePrefixes, mods.indexOf("!") !== -1); value = a_1[0]; prefix = a_1[1]; suffix = a_1[2]; if (details.modSpacing) { suffix = " " + suffix; } } else if (mods.indexOf("a") !== -1) { var a_2 = this.applyPrefix(value, value < this.smallNumberThreshold ? this.smallNumberPrefixes : this.bigNumberPrefixes, mods.indexOf("!") !== -1); value = a_2[0]; prefix = a_2[1]; suffix = a_2[2]; if (details.modSpacing) { suffix = " " + suffix; } } else if (mods.indexOf("p") !== -1) { var ol = Math.min(value.toString().length + 2, 21); //value *= 100; value = parseFloat(value.toPrecision(ol)); prefix = this.language.translate("_percentPrefix") || ""; suffix = this.language.translate("_percentSuffix") || ""; if (prefix == "" && suffix == "") { suffix = "%"; } } else if (mods.indexOf("%") !== -1) { var ol = $math.min(value.toString().length + 2, 21); value *= 100; value = parseFloat(value.toPrecision(ol)); suffix = "%"; } else if (mods.indexOf("‰") !== -1) { var ol = $math.min(value.toString().length + 3, 21); value *= 1000; value = parseFloat(value.toPrecision(ol)); suffix = "‰"; } // Round to passive if (mods.indexOf("e") !== -1) { // convert the value to exponential var exp = void 0; if (details.decimals.passive >= 0) { exp = value.toExponential(details.decimals.passive).split("e"); } else { exp = value.toExponential().split("e"); } value = Number(exp[0]); suffix = "e" + exp[1]; if (details.modSpacing) { suffix = " " + suffix; } } else if (details.decimals.passive === 0) { value = Math.round(value); } else if (details.decimals.passive > 0) { var d = Math.pow(10, details.decimals.passive); value = Math.round(value * d) / d; } // Init return value var res = ""; // Calc integer and decimal parts var a = $utils.numberToString(value).split("."); // Format integers var ints = a[0]; // Pad integers to active length if (ints.length < details.thousands.active) { ints = Array(details.thousands.active - ints.length + 1).join("0") + ints; } // Insert thousands separators if (details.thousands.interval > 0) { var ip = []; var intsr = ints.split("").reverse().join(""); for (var i = 0, len = ints.length; i <= len; i += details.thousands.interval) { var c = intsr.substr(i, details.thousands.interval).split("").reverse().join(""); if (c !== "") { ip.unshift(c); } } ints = ip.join(details.thousands.separator); } // Add integers res += ints; // Add decimals if (a.length === 1) { a.push(""); } var decs = a[1]; // Fill zeros? if (decs.length < details.decimals.active) { decs += Array(details.decimals.active - decs.length + 1).join("0"); } if (decs !== "") { res += details.decimals.separator + decs; } // Can't have empty return value if (res === "") { res = "0"; } // Add minus sign back if (value !== 0 && negative && (mods.indexOf("s") === -1)) { res = "-" + res; } // Add suffixes/prefixes if (prefix) { res = prefix + res; } if (suffix) { res += suffix; } return res; }; /** * Chooses appropriate prefix and suffix based on the passed in rules. * * @param value Value * @param prefixes Prefix array * @param force Force application of a first prefix (@sice 4.5.4) * @return Result */ NumberFormatter.prototype.applyPrefix = function (value, prefixes, force) { if (force === void 0) { force = false; } var newvalue = value; var prefix = ""; var suffix = ""; var applied = false; var k = 1; for (var i = 0, len = prefixes.length; i < len; i++) { if (prefixes[i].number <= value) { if (prefixes[i].number === 0) { newvalue = 0; } else { newvalue = value / prefixes[i].number; k = prefixes[i].number; } prefix = prefixes[i].prefix; suffix = prefixes[i].suffix; applied = true; } } if (!applied && force && prefixes.length && value != 0) { // Prefix was not applied. Use the first prefix. newvalue = value / prefixes[0].number; prefix = prefixes[0].prefix; suffix = prefixes[0].suffix; applied = true; } if (applied) { newvalue = parseFloat(newvalue.toPrecision($math.min(k.toString().length + Math.floor(newvalue).toString().replace(/[^0-9]*/g, "").length, 21))); } return [newvalue, prefix, suffix]; }; /** * Invalidates the parent [[Sprite]] object. */ NumberFormatter.prototype.invalidateSprite = function () { if (this.sprite) { this.sprite.invalidate(); } }; Object.defineProperty(NumberFormatter.prototype, "numberFormat", { /** * @return A format to use for number formatting */ get: function () { return this._numberFormat; }, /** * Number format. * * @default "#,###.#####" * @see {@link https://www.amcharts.com/docs/v4/concepts/formatters/formatting-numbers/} Tutorial on number formatting * @param format A format to use for number formatting */ set: function (format) { this._numberFormat = format; }, enumerable: true, configurable: true }); Object.defineProperty(NumberFormatter.prototype, "intlLocales", { /** * @return Date format */ get: function () { return this._intlLocales; }, /** * Locales if you are using date formats in `Intl.NumberFormatOptions` syntax. * * @see (@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat) about using Intl for number formatting * @param value Locales */ set: function (value) { this._intlLocales = value; this.invalidateSprite(); }, enumerable: true, configurable: true }); Object.defineProperty(NumberFormatter.prototype, "negativeBase", { /** * @return A format to use for number formatting */ get: function () { return this._negativeBase; }, /** * Negative base for negative numbers. * * @default 0 * @see {@link https://www.amcharts.com/docs/v4/concepts/formatters/formatting-numbers/} Tutorial on number formatting * @param format A format to use for number formatting */ set: function (value) { this._negativeBase = value; }, enumerable: true, configurable: true }); Object.defineProperty(NumberFormatter.prototype, "bigNumberPrefixes", { /** * @return Prefixes for big numbers */ get: function () { if (!$type.hasValue(this._bigNumberPrefixes)) { this._bigNumberPrefixes = [ { "number": 1e+3, "suffix": this.language.translate("_big_number_suffix_3") }, { "number": 1e+6, "suffix": this.language.translate("_big_number_suffix_6") }, { "number": 1e+9, "suffix": this.language.translate("_big_number_suffix_9") }, { "number": 1e+12, "suffix": this.language.translate("_big_number_suffix_12") }, { "number": 1e+15, "suffix": this.language.translate("_big_number_suffix_15") }, { "number": 1e+18, "suffix": this.language.translate("_big_number_suffix_18") }, { "number": 1e+21, "suffix": this.language.translate("_big_number_suffix_21") }, { "number": 1e+24, "suffix": this.language.translate("_big_number_suffix_24") } ]; } return this._bigNumberPrefixes; }, /** * Prefixes for big numbers. * * It's an array of objects of number/prefix pairs. * * ```JSON * [ * { "number": 1e+3, "suffix": "K" }, * { "number": 1e+6, "suffix": "M" }, * { "number": 1e+9, "suffix": "G" }, * { "number": 1e+12, "suffix": "T" }, * { "number": 1e+15, "suffix": "P" }, * { "number": 1e+18, "suffix": "E" }, * { "number": 1e+21, "suffix": "Z" }, * { "number": 1e+24, "suffix": "Y" } * ] * ``` * * If the number is bigger than the `number` ir will be converted to the * appropriate bigger number with prefix. * * E.g. as per above `1500` will be converted to `1.5K`. * * Please note that for this transformation to be enabled, you need to * enable it specific modifier in your format setting. * * The modifier for big/small number modification is "a": * * ```Text * {myfield.formatNumber("#,###.00a")} * ``` * * @see {@link https://www.amcharts.com/docs/v4/concepts/formatters/formatting-numbers/} Tutorial on number formatting * @param prefixes Prefixes for big numbers */ set: function (prefixes) { this._bigNumberPrefixes = prefixes; }, enumerable: true, configurable: true }); Object.defineProperty(NumberFormatter.prototype, "smallNumberPrefixes", { /** * @return Prefixes for small numbers */ get: function () { if (!$type.hasValue(this._smallNumberPrefixes)) { this._smallNumberPrefixes = [ { "number": 1e-24, "suffix": this.language.translate("_small_number_suffix_24") }, { "number": 1e-21, "suffix": this.language.translate("_small_number_suffix_21") }, { "number": 1e-18, "suffix": this.language.translate("_small_number_suffix_18") }, { "number": 1e-15, "suffix": this.language.translate("_small_number_suffix_15") }, { "number": 1e-12, "suffix": this.language.translate("_small_number_suffix_12") }, { "number": 1e-9, "suffix": this.language.translate("_small_number_suffix_9") }, { "number": 1e-6, "suffix": this.language.translate("_small_number_suffix_6") }, { "number": 1e-3, "suffix": this.language.translate("_small_number_suffix_3") } ]; } return this._smallNumberPrefixes; }, /** * Prefixes for big numbers. * * It's an array of objects of number/prefix pairs. * * ```JSON * [ * { "number": 1e-24, "suffix": "y" }, * { "number": 1e-21, "suffix": "z" }, * { "number": 1e-18, "suffix": "a" }, * { "number": 1e-15, "suffix": "f" }, * { "number": 1e-12, "suffix": "p" }, * { "number": 1e-9, "suffix": "n" }, * { "number": 1e-6, "suffix": "μ" }, * { "number": 1e-3, "suffix": "m" } * ] * ``` * * If the number is smaller than the `number` ir will be converted to the * appropriate smaller number with prefix. * * E.g. as per above `0.0015` will be converted to `1.5m`. * * Please note that for this transformation to be enabled, you need to * enable it specific modifier in your format setting. * * The modifier for big/small number modification is "a": * * ```Text * {myfield.formatNumber("#,###.00a")} * ``` * * IMPORTANT: The order of the suffixes is important. The list must start * from the smallest number and work towards bigger ones. * * @see {@link https://www.amcharts.com/docs/v4/concepts/formatters/formatting-numbers/} Tutorial on number formatting * @param prefixes Prefixes for small numbers */ set: function (prefixes) { this._smallNumberPrefixes = prefixes; }, enumerable: true, configurable: true }); Object.defineProperty(NumberFormatter.prototype, "smallNumberThreshold", { /** * @return Small number threshold */ get: function () { return this._smallNumberThreshold; }, /** * Any number smaller than this will be considered "small" number, which will * trigger special formatting if "a" format modifier is used. * * @since 4.6.8 * @param value Small number threshold */ set: function (value) { this._smallNumberThreshold = value; }, enumerable: true, configurable: true }); Object.defineProperty(NumberFormatter.prototype, "bytePrefixes", { /** * @return Prefixes for byte-size formatting */ get: function () { if (!$type.hasValue(this._bytePrefixes)) { this._bytePrefixes = [ { "number": 1, suffix: this.language.translate("_byte_suffix_B") }, { "number": 1024, suffix: this.language.translate("_byte_suffix_KB") }, { "number": 1048576, suffix: this.language.translate("_byte_suffix_MB") }, { "number": 1073741824, suffix: this.language.translate("_byte_suffix_GB") }, { "number": 1099511627776, suffix: this.language.translate("_byte_suffix_TB") }, { "number": 1125899906842624, suffix: this.language.translate("_byte_suffix_PB") } ]; } return this._bytePrefixes; }, /** * Basically the same as `bigNumberPrefixes`, except base for calculation * is not thousand but byte (1024). * * The modifier is "b". * * ```Text * {myfield.formatNumber("#,###.00b")} * ``` * * The above `2048` will change to `2K`. * * @see {@link https://www.amcharts.com/docs/v4/concepts/formatters/formatting-numbers/} Tutorial on number formatting * @param prefixes Prefixes for byte-size formatting */ set: function (prefixes) { this._bytePrefixes = prefixes; }, enumerable: true, configurable: true }); Object.defineProperty(NumberFormatter.prototype, "outputFormat", { /** * @ignore Exclude from docs * @return Output format */ get: function () { return this._outputFormat; }, /** * Ooutput format: "svg" or "html". * * @ignore Exclude from docs * @param value Output format */ set: function (outputFormat) { this._outputFormat = outputFormat.toLowerCase(); this.invalidateSprite(); }, enumerable: true, configurable: true }); Object.defineProperty(NumberFormatter.prototype, "forceLTR", { get: function () { return this._forceLTR; }, set: function (value) { this._forceLTR = value; this.invalidateSprite(); }, enumerable: true, configurable: true }); /** * Replaces brackets with temporary placeholders. * * @ignore Exclude from docs * @param text Input text * @return Escaped text */ NumberFormatter.prototype.escape = function (text) { return text.replace("||", $strings.PLACEHOLDER2); }; /** * Replaces placeholders back to brackets. * * @ignore Exclude from docs * @param text Escaped text * @return Unescaped text */ NumberFormatter.prototype.unescape = function (text) { return text.replace($strings.PLACEHOLDER2, "|"); }; return NumberFormatter; }(BaseObject)); export { NumberFormatter }; /** * Register class in system, so that it can be instantiated using its name from * anywhere. * * @ignore */ registry.registeredClasses["NumberFormatter"] = NumberFormatter; //# sourceMappingURL=NumberFormatter.js.map