11

When drawing graphs using SI codes is pretty much what we want. Our y-axis values tend to be large currency values. eg: $10,411,504,201.20

Abbreviating this, at least in a US locale, this should translate to $10.4B.

But using d3.format's 's' type for SI codes this would display as $10.4G. This might be great for some locales and good when dealing with computer-based values (eg: processor speed, memory...), but not so with currency or other non-computer types of values.

Is there a way to get locale-specific functionality similar to SI-codes that would convert billions to B instead of G, etc...?

(I realize this is mostly an SI-codes thing and not specific to D3, but since I'm using D3 this seems the most appropriate tag.)

lostdorje
  • 6,150
  • 9
  • 44
  • 86
  • 6
    I too once ran into this issue and couldn't find anything idiomatic to handle it, so I ended up doing this: `si = d3.format('s');` `siMod = function(val) { return si(val).replace(/G/, 'B') };` – meetamit Jun 11 '13 at 13:38
  • right, looks like i'll be doing the same thing. hope this type of functionality gets built into d3 sometime. – lostdorje Jun 12 '13 at 07:00
  • This is still not part of D3's framework yet right? Couldn't find a way to change it to US locale based on their documentation. – Arnaldo Capo Sep 09 '14 at 14:05
  • @meetamit - I have a question here (http://stackoverflow.com/q/41385010/1735836) you might be able to help me with. I like your comment but I have no idea how to implement it. – Patricia Dec 29 '16 at 18:27

2 Answers2

13

I prefer overriding d3.formatPrefix. Then you can just forget about replacing strings within your viz code. Simply execute the following code immediately after loading D3.js.

// Change D3's SI prefix to more business friendly units
//      K = thousands
//      M = millions
//      B = billions
//      T = trillion
//      P = quadrillion
//      E = quintillion
// small decimals are handled with e-n formatting.
var d3_formatPrefixes = ["e-24","e-21","e-18","e-15","e-12","e-9","e-6","e-3","","K","M","B","T","P","E","Z","Y"].map(d3_formatPrefix);

// Override d3's formatPrefix function
d3.formatPrefix = function(value, precision) {
    var i = 0;
    if (value) {
        if (value < 0) {
            value *= -1;
        }
        if (precision) {
            value = d3.round(value, d3_format_precision(value, precision));
        }
        i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10);
        i = Math.max(-24, Math.min(24, Math.floor((i - 1) / 3) * 3));
    }
    return d3_formatPrefixes[8 + i / 3];
};

function d3_formatPrefix(d, i) {
    var k = Math.pow(10, Math.abs(8 - i) * 3);
    return {
        scale: i > 8 ? function(d) { return d / k; } : function(d) { return d * k; },
        symbol: d
    };
}

function d3_format_precision(x, p) {
    return p - (x ? Math.ceil(Math.log(x) / Math.LN10) : 1);
}

After running this code, try formatting a number with SI prefix:

d3.format(".3s")(1234567890) // 1.23B

You could augment this code pretty simply to support different locales by including locale-specific d3_formatPrefixes values in an object and then select the proper one that matches a locale you need.

nross83
  • 532
  • 4
  • 10
  • 2
    'e-3' is so much better than 'm' when plotting financial information. Thanks! – ninjaPixel Jun 05 '15 at 16:12
  • Thanks! This does work with formatting plotly axis label too. – kimsk Aug 23 '16 at 17:13
  • I have a question here (http://stackoverflow.com/q/41385010/1735836) you might be able to help me with. It looks like your answer might work, but I'm not sure and I don't really know how to implement it. – Patricia Dec 29 '16 at 18:08
  • 1
    @Patricia, This should work provided you first load D3 and then immediately after D3 is loaded and executed, you execute this script. At that point you should be able to render any chart you'd like and you should see your desired formatting. If you execute this script before D3 is loaded or after your chart render code it won't work for you. – nross83 Jan 04 '17 at 21:27
  • Is there a way I can execute this code before I run my report, so that it is overloaded only in my part of the larger application? If not, then I need a little help in determining when d3 is loaded and then executing your script. It's a big application and I'm a newb. – Patricia Jan 12 '17 at 20:48
  • 1
    @Patricia, Yes, you could save the previous d3.formatPrefix to a temporary variable, then run my script followed by your report. When that's done, set d3.formatPrefix back to what it was. `var d3FormatPrefixFunc = d3.formatPrefix; // Run my script and change d3.formatPrefix // Run your report code d3.formatPrefix = d3FormatPrefixFunc; // Set it back.` – nross83 Jan 12 '17 at 20:55
  • I'm sorry. You're going to have to treat me like a newb. I put your script into an anonymous function, so I could run it. But the methods are not getting overridden, How do I run your script? I still have the question listed in above comment. I'd love to give you some points for helping me. – Patricia Jan 13 '17 at 02:32
  • 1
    @Patricia, It's really hard to answer questions with code in comments unfortunately :). Essentially, you'd want to put all my code into a function called `overrideSIPrefix`. Then, after D3 is loaded do `var d3FormatPrefixFunc = d3.formatPrefix` (see my previous comment). Then call `overrideSIPrefix()` which will set `d3.formatPrefix` to my custom logic. Then call the function which executes your report. Then do `d3.formatPrefix = d3FormatPrefixFunc` to set D3 back as it was before you tampered with it so that subsequent code will be unaffected. – nross83 Jan 13 '17 at 20:58
  • Making progress!! I wrapped your script with `function formatter () {` and `};` I added this code to save: `var d3FormatPrefixFunc = d3.formatPrefix;` then I run `formatter();` and run the report. I can see in the Global variables the new d3_formatPrefixes from your script. But when execution gets into d3.v3.js `function d3_locale_numberFormat(locale)` I get an `Uncaught TypeError: Cannot read property 'scale' of undefined` It looks like `unit` might be undefined. – Patricia Jan 13 '17 at 22:59
2

I like the answer by @nross83

Just going to paste a variation that I think might be more robust.

Example:


import { formatLocale, formatSpecifier } from "d3";

const baseLocale = {
    decimal: ".",
    thousands: ",",
    grouping: [3],
    currency: ["$", ""],
};

// You can define your own si prefix abbr. here
const d3SiPrefixMap = {
    y: "e-24",
    z: "e-21",
    a: "e-18",
    f: "e-15",
    p: "e-12",
    n: "e-9",
    µ: "e-6",
    m: "e-3",
    "": "",
    k: "K",
    M: "M",
    G: "B",
    T: "T",
    P: "P",
    E: "E",
    Z: "Z",
    Y: "Y",
};

const d3Format = (specifier: string) => {
  const locale = formatLocale({ ...baseLocale });
  const formattedSpecifier = formatSpecifier(specifier);
  const valueFormatter = locale.format(specifier);
  
  return (value: number) => {
      const result = valueFormatter(value);
      if (formattedSpecifier.type === "s") { 
        // modify the return value when using si-prefix. 
        const lastChar = result[result.length - 1];
        if (Object.keys(d3SiPrefixMap).includes(lastChar)) {
          return result.slice(0, -1) + d3SiPrefixMap[lastChar];
        }
      }
      // return the default result from d3 format in case the format type is not set to `s` (si suffix)
      return result;
  };
}

And use it like the following:


const value = 1000000000;
const formattedValue = d3Format("~s")(value);
console.log({formattedValue}); // Outputs: {formattedValue: "1B"} 

We used the formatSpecifier function from d3-format to check if the format type is s, i.e. si suffix, and only modify the return value in this case.

In the example above, I have not modified the actual d3 function. You can change the code accordingly if you want to do that for the viz stuff.


I hope this answer is helpful. Thank you :)

Aditya
  • 699
  • 6
  • 9