Skip to content

Commit

Permalink
d3.blur
Browse files Browse the repository at this point in the history
fixes #56
  • Loading branch information
Fil committed Jun 30, 2022
1 parent 1d1e460 commit 9e4fe06
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 0 deletions.
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,7 @@ Returns an array of arrays, where the *i*th array contains the *i*th element fro
d3.zip([1, 2], [3, 4]); // returns [[1, 3], [2, 4]]
```

<<<<<<< HEAD
### Iterables

These are equivalent to built-in array methods, but work with any iterable including Map, Set, and Generator.
Expand Down Expand Up @@ -821,6 +822,44 @@ Returns true if *a* and *b* are disjoint: if *a* and *b* contain no shared value
```js
d3.disjoint([1, 3], [2, 4]) // true
```
=======
### Blur

<a name="blur" href="#blur">#</a> d3.<b>blur</b>() · [Source](https://github.com/d3/d3-array/blob/master/src/blur.js), [Examples](https://observablehq.com/@d3/d3-blur)

Creates a blur transformer, which can blur (or smooth) an *array* of values by three iterations of a moving average transform.

<a name="blur_radius" href="#blur_radius">#</a> *blur*.<b>radius</b>([*radius*])

If *radius* is specified, sets the radius of the transformation: on each iteration, the value of a point is transformed into the mean value of itself and the *radius* points of data surrounding it on the left and on the right (taking into account the edges). If *radius* is not specified, return the current radius (if horizontal and vertical radii have been set separately, returns their average value). If *radius* is not an integer value, the blurring is applied partially. Defaults to 5.

<a name="blur_value" href="#blur_value">#</a> *blur*.<b>value</b>([*value*])

If *value* is specified, sets the *value* accessor, which will read the *array*. If not specified, return the current number. Defaults to the special *null* accessor, which copies the values directly (faster than an identity function).

Example:
```js
const blurred = d3.blur().value(d => d.temperature)(data);
```

<a name="blur_width" href="#blur_width">#</a> *blur*.<b>width</b>([*width*])

If *width* is specified, sets the width of the transformation, otherwise returns the current width. When 0 < width < length, *blur* considers that the *array* describes values in two dimensions—as a rectangle of a certain width (height inferred as length divided by width). In that case each iteration involves an horizontal (x) blurring, followed by a vertical (y) blurring. Defaults to undefined (horizontal dimension).

<a name="blur_radiusX" href="#blur_radiusX">#</a> *blur*.<b>radiusX</b>([*radius*])

If *radius* is specified, sets the horizontal radius of the transformation, otherwise returns it. (Use 0 for vertical blurring.)

<a name="blur_radiusY" href="#blur_radiusY">#</a> *blur*.<b>radiusY</b>([*radius*])

If *radius* is specified, sets the vertical radius of the transformation, otherwise returns it. (Use 0 for horizontal blurring.)


<<<<<<< HEAD
If *direction* is specified, sets the direction of the transformation, otherwise returns the current direction. When the direction is "y" and [width](#blur_width) indicates that the data is rectangular, only vertical blurring is applied. Similarly direction "x" applies only horizontal blurring. Default: undefined (both directions are applied on 2D data).
>>>>>>> ef4a970 (d3.blur)
=======
>>>>>>> e0a2099 (- add radiusX, radiusY to the API (set direction "y" with radiusX(0) and vice versa))
### Bins

Expand Down
94 changes: 94 additions & 0 deletions src/blur.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {floor, max, min} from "./math.js";
import {arrayify} from "./utils.js";

function blurTransfer(V, r, n, vertical) {
if (!r) return; // radius 0 is a noop

const iterations = 3, r0 = Math.floor(r);
// for a non-integer radius, interpolate between floor(r) and ceil(r)
if (r === r0) {
for (let i = 0; i < iterations; i++) {
blurTransferInt(V, r, n, vertical);
}
} else {
const frac = r - r0, frac_1 = 1 - frac;
const data = V[0].slice();
for (let i = 0; i < iterations; i++) {
blurTransferInt(V, r0 + 1, n, vertical);
}
const data_ceil = V[0];
V[0] = data;
if (r0 > 0) {
for (let i = 0; i < iterations; i++) {
blurTransferInt(V, r0, n, vertical);
}
}
for (let i = 0; i < data.length; i++) {
V[0][i] = V[0][i] * frac_1 + data_ceil[i] * frac;
}
}
}

function blurTransferInt(V, r, n, vertical) {
const [source, target] = V,
m = floor(source.length / n),
w = 2 * r + 1,
w1 = 1 / w,
ki = vertical ? m : 1,
kj = vertical ? 1 : n,
W = w * ki,
R = r * ki;

for (let j = 0; j < m; ++j) {
const k0 = kj * j,
kn = k0 + ki * (n - 1);
for (let i = 0, sr = w * source[k0]; i < n + r; ++i) {
const k = ki * i + kj * j;
sr += source[min(k, kn)] - source[max(k - W, k0)];
target[max(k - R, k0)] = sr * w1;
}
}
V.reverse(); // target becomes V[0] and will be used as source in the next iteration
}

export default function blur() {
let rx = 5,
ry = rx,
value,
width;
const V = [];

function blur(data) {
// reuse the V arrays if possible
if (value || !V[0] || V[0].length !== data.length) {
V[0] = value ? Float32Array.from(data, value) : Float32Array.from(data);
V[1] = new Float32Array(V[0].length);
} else {
V[0].set(arrayify(data));
}

const n = width || V[0].length;
const m = Math.round(V[0].length / n);

blurTransfer(V, rx, n, false);
blurTransfer(V, ry, m, true);

V[0].width = n;
V[0].height = m;
return V[0];
}

blur.radius = _ => _ === undefined
? (rx + ry) / 2
: (rx = ry = +_, blur);
blur.radiusX = _ => _ === undefined
? rx : (rx = +_, blur);
blur.radiusY = _ => _ === undefined
? ry : (ry = +_, blur);
blur.width = _ =>
_ === undefined ? width : (width = Math.round(+_), blur);
blur.value = _ =>
typeof _ === "function" ? (value = _, blur) : value;

return blur;
}
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export {default as bisect, bisectRight, bisectLeft, bisectCenter} from "./bisect.js";
export {default as ascending} from "./ascending.js";
export {default as bisector} from "./bisector.js";
export {default as blur} from "./blur.js";
export {default as count} from "./count.js";
export {default as cross} from "./cross.js";
export {default as cumsum} from "./cumsum.js";
Expand Down
5 changes: 5 additions & 0 deletions src/math.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const floor = Math.floor;

export const max = Math.max;

export const min = Math.min;
3 changes: 3 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function arrayify(values) {
return typeof values !== "object" || "length" in values ? values : Array.from(values);
}
72 changes: 72 additions & 0 deletions test/blur-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import assert from "assert";
import {blur} from "../src/index.js";

it("blur() returns a default blur generator", () => {
const h = blur();
assert.equal(h.radius(), 5);
assert.equal(h.radiusX(), 5);
assert.equal(h.radiusY(), 5);
assert.equal(h.width(), undefined);
assert.equal(h.value(), undefined);
});

it("blur() blurs in 1D", () => {
const h = blur();
assert.deepEqual(
h.radius(1).width(0)([0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0]),
Object.assign(Float32Array.from([0, 0, 0, 1, 3, 6, 7, 6, 3, 1, 0, 0, 0, 0]), { width: 14, height: 1 })
);
});

it("blur() blurs in 2D", () => {
const h = blur();
assert.deepEqual(
h.width(4).radiusX(1).radiusY(1)([0, 0, 0, 0, 729, 0, 0, 0, 0, 0, 0, 0]),
Object.assign(Float32Array.from([117, 81, 36, 9, 117, 81, 36, 9, 117, 81, 36, 9]), { width: 4, height: 3 })
);
});

it("blur().radiusY(0) blurs horizontally", () => {
const h = blur();
assert.deepEqual(
h.width(4).radiusX(1).radiusY(0)([27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
Object.assign(Float32Array.from([13, 9, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0]), { width: 4, height: 3 })
);
});

it("blur().radiusX(0) blurs vertically", () => {
const h = blur();
assert.deepEqual(
h.width(4).radiusX(0).radiusY(1)([0, 0, 0, 27, 3, -9, 0, 0, 0, 0, 0, 0]),
Object.assign(Float32Array.from([1, -3, 0, 13, 1, -3, 0, 9, 1, -3, 0, 5]), { width: 4, height: 3 })
);
});

it("blur().radius(0.5) does a fraction of blur", () => {
const h = blur().width(5), V = [0,0,0,0,0, 0,0,5184,0,0, 0,0,0,0,0];
assert.deepEqual(
h.radius(0.5)(V),
Object.assign(Float32Array.from([64, 96, 544, 96, 64, 256, 384, 2176, 384, 256, 64, 96, 544, 96, 64]), { width: 5, height: 3 })
);
});

it("blur().radius(1.2) does a fraction of blur", () => {
const h = blur(), V = [0,0,0,0,0, 0,0,1,0,0, 0,0,0,0,0];
const V1 = h.radius(1)(V);
const V2 = h.radius(2)(V);
for (let i = 0; i < V1.length; i++) V1[i] = 0.8 * V1[i] + 0.2 * V2[i];
assert.deepEqual(Array.from(h.radius(1.2)(V)), Array.from(V1));
});

it("blur().radius() returns the (average) radius", () => {
const h = blur();
assert.equal(h.width(2).radiusX(1).radiusY(1).radius(), 1);
assert.equal(h.width(2).radius(2).radius(), 2);
assert.equal(h.width(2).radiusX(1).radiusY(5).radius(), 3);
});

it("blur() accepts an iterable", () => {
const h = blur().radius(1);
assert.deepEqual(h(new Set([27, 0, -27])), Object.assign(Float32Array.from([8, 0, -8]), { width: 3, height: 1 }));
assert.deepEqual(h.value(d => d.a)(new Set([{a: 27}, {a: 0}, {a: -27}])), Object.assign(Float32Array.from([8, 0, -8]), { width: 3, height: 1 }));
});

0 comments on commit 9e4fe06

Please sign in to comment.