{"action":"created","issue":{"url":"https://api.github.com/repos/mbostock/d3/issues/2513","labels_url":"https://api.github.com/repos/mbostock/d3/issues/2513/labels{/name}","comments_url":"https://api.github.com/repos/mbostock/d3/issues/2513/comments","events_url":"https://api.github.com/repos/mbostock/d3/issues/2513/events","html_url":"https://github.com/mbostock/d3/issues/2513","id":99271266,"number":2513,"title":"Quantize scale produces incorrect result on qunatized range thresholds","user":{"login":"barrybecker4","id":3020163,"avatar_url":"https://avatars.githubusercontent.com/u/3020163?v=3","gravatar_id":"","url":"https://api.github.com/users/barrybecker4","html_url":"https://github.com/barrybecker4","followers_url":"https://api.github.com/users/barrybecker4/followers","following_url":"https://api.github.com/users/barrybecker4/following{/other_user}","gists_url":"https://api.github.com/users/barrybecker4/gists{/gist_id}","starred_url":"https://api.github.com/users/barrybecker4/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/barrybecker4/subscriptions","organizations_url":"https://api.github.com/users/barrybecker4/orgs","repos_url":"https://api.github.com/users/barrybecker4/repos","events_url":"https://api.github.com/users/barrybecker4/events{/privacy}","received_events_url":"https://api.github.com/users/barrybecker4/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/mbostock/d3/labels/bug","name":"bug","color":"d62728"}],"state":"closed","locked":false,"assignee":null,"milestone":{"url":"https://api.github.com/repos/mbostock/d3/milestones/32","html_url":"https://github.com/mbostock/d3/milestones/3.5.x","labels_url":"https://api.github.com/repos/mbostock/d3/milestones/32/labels","id":860338,"number":32,"title":"3.5.x","description":"","creator":{"login":"mbostock","id":230541,"avatar_url":"https://avatars.githubusercontent.com/u/230541?v=3","gravatar_id":"","url":"https://api.github.com/users/mbostock","html_url":"https://github.com/mbostock","followers_url":"https://api.github.com/users/mbostock/followers","following_url":"https://api.github.com/users/mbostock/following{/other_user}","gists_url":"https://api.github.com/users/mbostock/gists{/gist_id}","starred_url":"https://api.github.com/users/mbostock/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mbostock/subscriptions","organizations_url":"https://api.github.com/users/mbostock/orgs","repos_url":"https://api.github.com/users/mbostock/repos","events_url":"https://api.github.com/users/mbostock/events{/privacy}","received_events_url":"https://api.github.com/users/mbostock/received_events","type":"User","site_admin":false},"open_issues":48,"closed_issues":37,"state":"open","created_at":"2014-11-10T16:45:16Z","updated_at":"2015-10-22T06:15:00Z","due_on":null,"closed_at":null},"comments":3,"created_at":"2015-08-05T18:45:19Z","updated_at":"2015-10-22T06:15:00Z","closed_at":"2015-10-22T06:15:00Z","body":"Suppose I create a quantize scale like this:\r\nvar quantizeScale = d3.scale.quantize().domain([4.2, 6.2]).range(d3.range(10));\r\n\r\nThen the qunatized bins should be [4.2, 4.4], [4.4, 4.6], [4.6, 4.8], [4.8, 5], [5, 5.2], ... [6, 6.2]\r\n\r\nThere is a problem getting the correct range value when scaling some of the threshold values.\r\nMy expectation is that the low value of each bin (shown above) should be included in the bin, and the high value should be excluded.\r\nThis is the case most of the time, but not always. Because of floating point precision issues (like the fact that .3 - .1 is not .2), we sometimes get unexpected results.\r\n\r\nFor example,\r\n quantizeScale(4.9999) should be 3 (and is)\r\n quantizeScale(5) should be 4 (but is 3)\r\n quantizeScale(5.001) should be 4 (and is)\r\n \r\n The inconsistent behavior causes lots of subtle problems.\r\n I have a proposed fix, but I'm not sure if it is the best fix, or if it works in all cases.\r\n The fix is in d3's quantize.js\r\n \r\n I altered the scale function to be\r\n  function scale(x) {\r\n\t return range[Math.max(0, Math.min(i, Math.floor(Number(kx * (x - x0)).toFixed(12))))];\r\n  }\r\n  \r\n and the invertExtent function to be\r\n \r\n scale.invertExtent = function(y) {\r\n\t    y = range.indexOf(y);\r\n\t    y = y < 0 ? NaN : y / kx + x0;\r\n\t    return [+Number(y).toFixed(12), +Number(y + 1 / kx).toFixed(12)];\r\n };\r\n\r\nThis makes sure that numbers like 5.999999999999 or 4.0000000000001 get rounded to something reasonable so that the correct bin is selected when we are on the boundary.\r\n"},"comment":{"url":"https://api.github.com/repos/mbostock/d3/issues/comments/150118912","html_url":"https://github.com/mbostock/d3/issues/2513#issuecomment-150118912","issue_url":"https://api.github.com/repos/mbostock/d3/issues/2513","id":150118912,"user":{"login":"mbostock","id":230541,"avatar_url":"https://avatars.githubusercontent.com/u/230541?v=3","gravatar_id":"","url":"https://api.github.com/users/mbostock","html_url":"https://github.com/mbostock","followers_url":"https://api.github.com/users/mbostock/followers","following_url":"https://api.github.com/users/mbostock/following{/other_user}","gists_url":"https://api.github.com/users/mbostock/gists{/gist_id}","starred_url":"https://api.github.com/users/mbostock/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mbostock/subscriptions","organizations_url":"https://api.github.com/users/mbostock/orgs","repos_url":"https://api.github.com/users/mbostock/repos","events_url":"https://api.github.com/users/mbostock/events{/privacy}","received_events_url":"https://api.github.com/users/mbostock/received_events","type":"User","site_admin":false},"created_at":"2015-10-22T06:15:00Z","updated_at":"2015-10-22T06:15:00Z","body":"Thanks for filing this interesting bug, and sorry it took me so long to get back to you. (I’ve been on paternity leave.)\r\n\r\nThere is technically no such thing as 4.2 in IEEE 754. The actual value is closer to 4.20000000000000017763568394002504646778106689453125. So, that quantization threshold is ever so slightly greater than 5; equivalently, when the scale computes the output range index for the input value 5, it computes 5 - 4.20000000000000017763568394002504646778106689453125 = 0.79999999999999982236431605997495353221893310546875. This divided by 2 / 10 = 0.200000000000000011102230246251565404236316680908203125, which gives 3.99999999999999911182158029987476766109466552734375, which is then floored to 3 because it is less than 4.\r\n\r\nYou could arbitrarily truncate input values and the domain to 12 decimal places, but what if the scale’s domain is intentionally very small? What if your domain represents, say, atom sizes? That doesn’t seem like a general solution and could introduce other unexpected behavior (albeit in less common cases).\r\n\r\nMy take is that the problem is *not* that quantize is returning the wrong value, but that quantize.invertExtent and quantize are inconsistent. As you say:\r\n\r\n> My expectation is that the low value of each bin (shown above) should be included in the bin, and the high value should be excluded.\r\n\r\nSo, if quantize.invertExtent(3) returns [4.8, 5], then quantize(4.8) should return 3 and quantize(5) should return 4. Instead, quantize(4.8) returns 2 and quantize(5) returns 3. Similarly, quantize.invertExtent(2) returns [4.6000000000000005, 4.800000000000001], so the ranges aren’t aligned due to precision error: quantize.invertExtent(i)[0] should equal quantize.invertExtent(i-1)[1].\r\n\r\nI’ve filed d3/d3-scale#17 to track this in 4.0. I’m not sure yet exactly what the fix is, but it should be doable. I don’t plan on fixing this for 3.x—instead for 4.0—so I will be closing this bug. Please followup in the d3-scale repo, if you like.\r\n\r\n(Tangentially related d3/d3-arrays#5.)"}}