Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Variable precision in d3.format

I'm using d3.format("s") with d3.svg.axis.tickFormat to nicely format the tick labels with SI units (from the International System of Units). It works beautifully for the most part, except for certain values that run into a rounding error and return a ton of decimal places (1400, for example, as 1400 * 0.001 = 1.4000000000000001).

To get around this I can specify a precision, such as d3.format(".2s") but this forces 2 significant digits in cases where 1 would be better. For example, if I have ticks [5000, 10000, 15000] I would now see [5.0k, 10k, 15k]. Originally when I didn't specify precision at all I got [5k, 10k, 15k].

I'm wondering if it's possible to specify a maximum precision such that it will only get applied when the number of significant digits exceeds the number specified? So 5000 would be 5k and not 5.0k.

like image 912
Scott Cameron Avatar asked Apr 25 '12 06:04

Scott Cameron


2 Answers

While 1400 * .001 is 1.4000000000000001, 1400 / 1000 is 1.4; multiplying an integer by a negative power of ten (such as .001) may introduce rounding error, but it appears that dividing an integer by a power of ten (such as 1000) does not. Or at least, it's less likely? I've implemented a tentative fix in the fix-si-format-rounding branch. It's strictly backwards-incompatible, but since the d3.formatPrefix function is undocumented, I think it'd be safe to include in the next patch release.

Edit: This fix was released in version 2.9.2.

like image 199
mbostock Avatar answered Nov 15 '22 05:11

mbostock


You can do this by placing an if/else statement in an anonymous function where you would output the text string. Within that if/else you can satisfy any condition that you want and create a function that parses your value. Here is an example:

var w = 30, h = 300;
var data = [
  {'point':1, 'value':100.005},
  {'point':2, 'value':20.010},
  {'point':3, 'value':1000},
  {'point':4, 'value':5000.099},
  {'point':5, 'value':6000.934},
  {'point':6, 'value':9000.888},
  {'point':7, 'value':500.22},
  {'point':8, 'value':5.123},
  {'point':9, 'value':50.022},
  {'point':10, 'value':7000.999}
];
var chart = d3.select("body").append("svg")
  .attr('class', 'chart')
  .attr("width", w * data.length - 1)
  .attr("height", h);

chart.selectAll('text')
  .data(data)
.enter().append('text')
  .attr('x', function(d, i) { return x(i); })
  .attr('y', function(d) { return h - y(d.value); })
  .attr('dx', '0.75em')
  .attr('dy', '-0.3em')
  .attr('text-anchor', 'start')
  .text(function(d) { 
    if(d.value > 5000) {
      // Define your formatting function to return SI units
      var si = d3.format('s');
      return String(si(d.value));
    } else {
      // Otherwise round to the nearest integer
      return String(d3.round(d.value, 0));
    }
  });

Hope that helps.

like image 23
tatlar Avatar answered Nov 15 '22 06:11

tatlar