From d50549d6fd9f2b8e9ac3427efb3385e5a19b2ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 31 Mar 2021 16:42:26 +0200 Subject: [PATCH 1/3] In band scales, avoid collapsing with round=true when the range is too narrow to accommodate the domain. fixes https://github.com/d3/d3-scale/issues/242 --- src/band.js | 6 ++++-- test/band-test.js | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/band.js b/src/band.js index 88e1fbd..c6f3212 100644 --- a/src/band.js +++ b/src/band.js @@ -23,10 +23,12 @@ export default function band() { start = reverse ? r1 : r0, stop = reverse ? r0 : r1; step = (stop - start) / Math.max(1, n - paddingInner + paddingOuter * 2); - if (round) step = Math.floor(step); + var r = round && step >= 1; + if (r) step = Math.floor(step); start += (stop - start - step * (n - paddingInner)) * align; bandwidth = step * (1 - paddingInner); - if (round) start = Math.round(start), bandwidth = Math.round(bandwidth); + if (round) start = Math.round(start); + if (r) bandwidth = Math.round(bandwidth); var values = sequence(n).map(function(i) { return start + step * i; }); return ordinalRange(reverse ? values.reverse() : values); } diff --git a/test/band-test.js b/test/band-test.js index 2fef80e..99a0dcb 100644 --- a/test/band-test.js +++ b/test/band-test.js @@ -181,6 +181,23 @@ tape("band.rangeRound(values) coerces values to numbers", function(test) { test.end(); }); +tape("band.round is ignored when the range is too small and the scale would collapse", function(test) { + var domain = ["A", "B", "C", "D", "E", "F"]; + var s1 = scale.scaleBand().domain(domain).rangeRound([0, 3]); + test.deepEqual(domain.map(s1), [ 0, 0.5, 1, 1.5, 2, 2.5 ]); + test.equal(s1.bandwidth(), 0.5); + var s2 = scale.scaleBand().domain(domain).rangeRound([0, 30]); + test.deepEqual(domain.map(s2), [ 0, 5, 10, 15, 20, 25 ]); + var s3 = scale.scaleBand().domain(domain).rangeRound([0, 10]); + test.deepEqual(domain.map(s3), [ 2, 3, 4, 5, 6, 7 ]); + var s4 = scale.scaleBand().domain(domain).range([0, 3]).round(true); + test.deepEqual(domain.map(s4), [ 0, 0.5, 1, 1.5, 2, 2.5 ]); + var s5 = scale.scaleBand().domain(domain).range([0, 1.5]).round(true); + test.deepEqual(domain.map(s5), [ 0, 0.25, 0.5, 0.75, 1, 1.25 ]); + test.equal(s5.bandwidth(), 0.25); + test.end(); +}); + tape("band.paddingInner(p) specifies the inner padding p", function(test) { var s = scale.scaleBand().domain(["a", "b", "c"]).range([120, 0]).paddingInner(0.1).round(true); test.deepEqual(s.domain().map(s), [83, 42, 1]); From 55ff397bb7e66dc0ce395f80b60a727146643e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 31 Mar 2021 16:49:46 +0200 Subject: [PATCH 2/3] band.roundingPrecision --- src/band.js | 13 +++++++++---- test/band-test.js | 8 ++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/band.js b/src/band.js index c6f3212..4703259 100644 --- a/src/band.js +++ b/src/band.js @@ -11,6 +11,7 @@ export default function band() { step, bandwidth, round = false, + roundingPrecision = 1, paddingInner = 0, paddingOuter = 0, align = 0.5; @@ -23,12 +24,12 @@ export default function band() { start = reverse ? r1 : r0, stop = reverse ? r0 : r1; step = (stop - start) / Math.max(1, n - paddingInner + paddingOuter * 2); - var r = round && step >= 1; - if (r) step = Math.floor(step); + var r = round && step >= roundingPrecision; + if (r) step = roundingPrecision * Math.floor(step / roundingPrecision); start += (stop - start - step * (n - paddingInner)) * align; bandwidth = step * (1 - paddingInner); - if (round) start = Math.round(start); - if (r) bandwidth = Math.round(bandwidth); + if (round) start = roundingPrecision * Math.round(start / roundingPrecision); + if (r) bandwidth = roundingPrecision * Math.round(bandwidth / roundingPrecision); var values = sequence(n).map(function(i) { return start + step * i; }); return ordinalRange(reverse ? values.reverse() : values); } @@ -57,6 +58,10 @@ export default function band() { return arguments.length ? (round = !!_, rescale()) : round; }; + scale.roundingPrecision = function(_) { + return arguments.length ? (roundingPrecision = +_, rescale()) : roundingPrecision; + }; + scale.padding = function(_) { return arguments.length ? (paddingInner = Math.min(1, paddingOuter = +_), rescale()) : paddingInner; }; diff --git a/test/band-test.js b/test/band-test.js index 99a0dcb..b555ab6 100644 --- a/test/band-test.js +++ b/test/band-test.js @@ -198,6 +198,14 @@ tape("band.round is ignored when the range is too small and the scale would coll test.end(); }); +tape.only("band.roundingPrecision adjusts the rounding precision", function(test) { + var domain = ["A", "B", "C", "D", "E", "F"]; + var s1 = scale.scaleBand().domain(domain).rangeRound([0, 5]).roundingPrecision(0.5); + test.deepEqual(domain.map(s1), [ 1, 1.5, 2, 2.5, 3, 3.5 ]); + test.equal(s1.bandwidth(), 0.5); + test.end(); +}); + tape("band.paddingInner(p) specifies the inner padding p", function(test) { var s = scale.scaleBand().domain(["a", "b", "c"]).range([120, 0]).paddingInner(0.1).round(true); test.deepEqual(s.domain().map(s), [83, 42, 1]); From cdb0b0618243e98f1a899899ce17d48cfc8fcd2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 31 Mar 2021 16:59:20 +0200 Subject: [PATCH 3/3] document roundingPrecision --- README.md | 16 ++++++++++++++-- test/band-test.js | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 45eb099..bc95a6f 100644 --- a/README.md +++ b/README.md @@ -902,7 +902,13 @@ Rounding is sometimes useful for avoiding antialiasing artifacts, though also co # band.round([round]) · [Source](https://github.com/d3/d3-scale/blob/master/src/band.js), [Examples](https://observablehq.com/@d3/d3-scaleband) -If *round* is specified, enables or disables rounding accordingly. If rounding is enabled, the start and stop of each band will be integers. Rounding is sometimes useful for avoiding antialiasing artifacts, though also consider the [shape-rendering](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/shape-rendering) “crispEdges” styles. Note that if the width of the domain is not a multiple of the cardinality of the range, there may be leftover unused space, even without padding! Use [*band*.align](#band_align) to specify how the leftover space is distributed. +If *round* is specified, enables or disables rounding accordingly. If rounding is enabled, the start and stop of each band will be multiples of the rounding precision. Rounding is sometimes useful for avoiding antialiasing artifacts, though also consider the [shape-rendering](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/shape-rendering) “crispEdges” styles. Note that if the width of the domain is not a multiple of the cardinality of the range, there may be leftover unused space, even without padding! Use [*band*.align](#band_align) to specify how the leftover space is distributed. + +To avoid collapsing the scale, rounding is not applied when the range is smaller than the length of the domain times the rounding precision. + +# band.roundingPrecision([precision]) · [Source](https://github.com/d3/d3-scale/blob/master/src/band.js) + +Sets or reads the rounding precision. A precision of 0.5 is convenient to avoid aliasing artifacts on screens with a pixel density of 2. # band.paddingInner([padding]) · [Source](https://github.com/d3/d3-scale/blob/master/src/band.js), [Examples](https://observablehq.com/@d3/d3-scaleband) @@ -968,7 +974,13 @@ Rounding is sometimes useful for avoiding antialiasing artifacts, though also co # point.round([round]) · [Source](https://github.com/d3/d3-scale/blob/master/src/band.js), [Examples](https://observablehq.com/@d3/d3-scalepoint) -If *round* is specified, enables or disables rounding accordingly. If rounding is enabled, the position of each point will be integers. Rounding is sometimes useful for avoiding antialiasing artifacts, though also consider the [shape-rendering](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/shape-rendering) “crispEdges” styles. Note that if the width of the domain is not a multiple of the cardinality of the range, there may be leftover unused space, even without padding! Use [*point*.align](#point_align) to specify how the leftover space is distributed. +If *round* is specified, enables or disables rounding accordingly. If rounding is enabled, the position of each point will be multiples of the rounding precision. Rounding is sometimes useful for avoiding antialiasing artifacts, though also consider the [shape-rendering](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/shape-rendering) “crispEdges” styles. Note that if the width of the domain is not a multiple of the cardinality of the range, there may be leftover unused space, even without padding! Use [*point*.align](#point_align) to specify how the leftover space is distributed. + +To avoid collapsing the scale, rounding is not applied when the range is smaller than the length of the domain times the rounding precision. + +# point.roundingPrecision([precision]) · [Source](https://github.com/d3/d3-scale/blob/master/src/band.js) + +Sets or reads the rounding precision. A precision of 0.5 is convenient to avoid aliasing artifacts on screens with a pixel density of 2. # point.padding([padding]) · [Source](https://github.com/d3/d3-scale/blob/master/src/band.js), [Examples](https://observablehq.com/@d3/d3-scalepoint) diff --git a/test/band-test.js b/test/band-test.js index b555ab6..bea11de 100644 --- a/test/band-test.js +++ b/test/band-test.js @@ -198,7 +198,7 @@ tape("band.round is ignored when the range is too small and the scale would coll test.end(); }); -tape.only("band.roundingPrecision adjusts the rounding precision", function(test) { +tape("band.roundingPrecision adjusts the rounding precision", function(test) { var domain = ["A", "B", "C", "D", "E", "F"]; var s1 = scale.scaleBand().domain(domain).rangeRound([0, 5]).roundingPrecision(0.5); test.deepEqual(domain.map(s1), [ 1, 1.5, 2, 2.5, 3, 3.5 ]);