Skip to content

Commit c8849c5

Browse files
mkfreemanFil
andauthored
Add normalize reducers min, max, deviation (#603)
* Add normalize reducers min, ax, deviation, variance * Remove variance option, fix deviation calculation * Preserve trailing newline, update readme * Remove extra new line * for the normalization basis=deviation, if there is only one point, or all the points are equal, normalize to 0 instead of NaN. * Plot.normalize unit tests Co-authored-by: Philippe Rivière <[email protected]>
1 parent 5d11571 commit c8849c5

File tree

3 files changed

+67
-1
lines changed

3 files changed

+67
-1
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,10 +1431,13 @@ The Plot.normalizeX and Plot.normalizeY transforms normalize series values relat
14311431

14321432
* *first* - the first value, as in an index chart; the default
14331433
* *last* - the last value
1434+
* *max* - the maximum value
14341435
* *mean* - the mean value (average)
14351436
* *median* - the median value
1437+
* *min* - the minimum value
14361438
* *sum* - the sum of values
14371439
* *extent* - the minimum is mapped to zero, and the maximum to one
1440+
* *deviation* - each value is transformed by subtracting the mean and then dividing by the standard deviation
14381441
* a function to be passed an array of values, returning the desired basis
14391442

14401443
The Plot.windowX and Plot.windowY transforms compute a moving window around each data point and then derive a summary statistic from values in the current window, say to compute rolling averages, rolling minimums, or rolling maximums. These transforms also take additional options:

src/transforms/normalize.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {extent, mean, median, sum} from "d3";
1+
import {extent, deviation, max, mean, median, min, sum} from "d3";
22
import {defined} from "../defined.js";
33
import {take} from "../mark.js";
44
import {mapX, mapY} from "./map.js";
@@ -17,10 +17,13 @@ export function normalize(basis) {
1717
if (basis === undefined) return normalizeFirst;
1818
if (typeof basis === "function") return normalizeBasis((I, S) => basis(take(S, I)));
1919
switch (`${basis}`.toLowerCase()) {
20+
case "deviation": return normalizeDeviation;
2021
case "first": return normalizeFirst;
2122
case "last": return normalizeLast;
23+
case "max": return normalizeMax;
2224
case "mean": return normalizeMean;
2325
case "median": return normalizeMedian;
26+
case "min": return normalizeMin;
2427
case "sum": return normalizeSum;
2528
case "extent": return normalizeExtent;
2629
}
@@ -61,6 +64,18 @@ const normalizeLast = normalizeBasis((I, S) => {
6164
}
6265
});
6366

67+
const normalizeDeviation = {
68+
map(I, S, T) {
69+
const m = mean(I, i => S[i]);
70+
const d = deviation(I, i => S[i]);
71+
for (const i of I) {
72+
T[i] = S[i] === null ? NaN : d ? (S[i] - m) / d : 0;
73+
}
74+
}
75+
};
76+
77+
const normalizeMax = normalizeBasis((I, S) => max(I, i => S[i]));
6478
const normalizeMean = normalizeBasis((I, S) => mean(I, i => S[i]));
6579
const normalizeMedian = normalizeBasis((I, S) => median(I, i => S[i]));
80+
const normalizeMin = normalizeBasis((I, S) => min(I, i => S[i]));
6681
const normalizeSum = normalizeBasis((I, S) => sum(I, i => S[i]));

test/transforms/normalize-test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as Plot from "@observablehq/plot";
2+
import assert from "assert";
3+
4+
it("Plot.normalize first", () => {
5+
testNormalize([1, 2, 4, 5], "first", [1, 2, 4, 5]);
6+
});
7+
8+
it("Plot.normalize last", () => {
9+
testNormalize([1, 2, 4, 5], "last", [0.2, 0.4, 0.8, 1]);
10+
});
11+
12+
it("Plot.normalize mean", () => {
13+
testNormalize([0, 4, 8, 4], "mean", [0, 1, 2, 1]);
14+
});
15+
16+
it("Plot.normalize median", () => {
17+
testNormalize([1, 2, 6, 6], "median", [0.25, 0.5, 1.5, 1.5]);
18+
});
19+
20+
it("Plot.normalize min", () => {
21+
testNormalize([10, 6, 2], "min", [5, 3, 1]);
22+
});
23+
24+
it("Plot.normalize max", () => {
25+
testNormalize([10, 6, 2], "max", [1, 0.6, 0.2]);
26+
});
27+
28+
it("Plot.normalize sum", () => {
29+
testNormalize([1, 1, 6, 2], "sum", [0.1, 0.1, 0.6, 0.2]);
30+
});
31+
32+
it("Plot.normalize extent", () => {
33+
testNormalize([1, 2, 3], "extent", [0, .5, 1]);
34+
});
35+
36+
it("Plot.normalize deviation", () => {
37+
testNormalize([1, 2, 3], "deviation", [-1, 0, 1]);
38+
});
39+
40+
it("Plot.normalize deviation doesn’t crash on equal values", () => {
41+
testNormalize([1, 1], "deviation", [0, 0]);
42+
});
43+
44+
function testNormalize(data, basis, r) {
45+
const mark = Plot.dot(data, Plot.normalizeY(basis, {y: data}));
46+
const c = new Map(mark.initialize().channels);
47+
assert.deepStrictEqual(c.get("y").value, r);
48+
}

0 commit comments

Comments
 (0)