Skip to content

Commit 47f06e6

Browse files
authored
index-aware reducer functions (#1553)
* index-aware reducer functions * simplify
1 parent 0a38eb8 commit 47f06e6

File tree

9 files changed

+121
-27
lines changed

9 files changed

+121
-27
lines changed

docs/transforms/normalize.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ The **basis** option specifies how to normalize the series values; it is one of:
120120
* *extent* - the minimum is mapped to zero, and the maximum to one
121121
* *deviation* - subtract the mean, then divide by the standard deviation
122122
* a function to be passed an array of values, returning the desired basis
123+
* a function to be passed an index and channel value array, returning the desired basis
123124

124125
## normalize(*basis*)
125126

docs/transforms/window.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ The following named reducers are supported:
181181
* *first* - the first value
182182
* *last* - the last value
183183

184-
A reducer may also be specified as a function to be passed an array of **k** values.
184+
A reducer may also be specified as a function to be passed an index of size **k** and the corresponding input channel array; or if the function only takes one argument, an array of **k** values.
185185

186186
## window(*k*)
187187

src/options.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,11 @@ export function take(values, index) {
211211
return map(index, (i) => values[i]);
212212
}
213213

214+
// If f does not take exactly one argument, wraps it in a function that uses take.
215+
export function taker(f) {
216+
return f.length === 1 ? (index, values) => f(take(values, index)) : f;
217+
}
218+
214219
// Based on InternMap (d3.group).
215220
export function keyof(value) {
216221
return value !== null && typeof value === "object" ? value.valueOf() : value;

src/reducer.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export type ReducerName =
5555
* A shorthand functional reducer implementation: given an array of input
5656
* channel *values*, returns the corresponding reduced output value.
5757
*/
58-
export type ReducerFunction<S = any, T = S> = (values: S[]) => T;
58+
export type ReducerFunction<S = any, T = S> = ((index: number[], values: S[]) => T) | ((values: S[]) => T);
5959

6060
/** A reducer implementation. */
6161
export interface ReducerImplementation<S = any, T = S> {

src/transforms/normalize.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {ReducerPercentile} from "../reducer.js";
1+
import type {ReducerFunction, ReducerPercentile} from "../reducer.js";
22
import type {Transformed} from "./basic.js";
33
import type {Map} from "./map.js";
44

@@ -32,7 +32,7 @@ export type NormalizeBasisName =
3232
* A functional basis implementation: given an array of input channel *values*
3333
* for the current series, returns the corresponding basis number (divisor).
3434
*/
35-
export type NormalizeBasisFunction<T = any> = (values: T[]) => number;
35+
export type NormalizeBasisFunction<T = any> = ReducerFunction<T, number>;
3636

3737
/**
3838
* How to normalize series values; one of:

src/transforms/normalize.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {extent, deviation, max, mean, median, min, sum} from "d3";
22
import {defined} from "../defined.js";
3-
import {percentile, take} from "../options.js";
3+
import {percentile, taker} from "../options.js";
44
import {mapX, mapY} from "./map.js";
55

66
export function normalizeX(basis, options) {
@@ -15,7 +15,7 @@ export function normalizeY(basis, options) {
1515

1616
export function normalize(basis) {
1717
if (basis === undefined) return normalizeFirst;
18-
if (typeof basis === "function") return normalizeBasis((I, S) => basis(take(S, I)));
18+
if (typeof basis === "function") return normalizeBasis(taker(basis));
1919
if (/^p\d{2}$/i.test(basis)) return normalizeAccessor(percentile(basis));
2020
switch (`${basis}`.toLowerCase()) {
2121
case "deviation":

src/transforms/window.js

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {deviation, max, min, median, mode, variance} from "d3";
22
import {defined} from "../defined.js";
3-
import {percentile, take} from "../options.js";
3+
import {percentile, taker} from "../options.js";
44
import {warn} from "../warnings.js";
55
import {mapX, mapY} from "./map.js";
66

@@ -51,24 +51,24 @@ function maybeShift(shift) {
5151

5252
function maybeReduce(reduce = "mean") {
5353
if (typeof reduce === "string") {
54-
if (/^p\d{2}$/i.test(reduce)) return reduceNumbers(percentile(reduce));
54+
if (/^p\d{2}$/i.test(reduce)) return reduceAccessor(percentile(reduce));
5555
switch (reduce.toLowerCase()) {
5656
case "deviation":
57-
return reduceNumbers(deviation);
57+
return reduceAccessor(deviation);
5858
case "max":
59-
return reduceArray(max);
59+
return reduceArray((I, V) => max(I, (i) => V[i]));
6060
case "mean":
6161
return reduceMean;
6262
case "median":
63-
return reduceNumbers(median);
63+
return reduceAccessor(median);
6464
case "min":
65-
return reduceArray(min);
65+
return reduceArray((I, V) => min(I, (i) => V[i]));
6666
case "mode":
67-
return reduceArray(mode);
67+
return reduceArray((I, V) => mode(I, (i) => V[i]));
6868
case "sum":
6969
return reduceSum;
7070
case "variance":
71-
return reduceNumbers(variance);
71+
return reduceAccessor(variance);
7272
case "difference":
7373
return reduceDifference;
7474
case "ratio":
@@ -80,7 +80,7 @@ function maybeReduce(reduce = "mean") {
8080
}
8181
}
8282
if (typeof reduce !== "function") throw new Error(`invalid reduce: ${reduce}`);
83-
return reduceArray(reduce);
83+
return reduceArray(taker(reduce));
8484
}
8585

8686
function slice(I, i, j) {
@@ -91,29 +91,29 @@ function slice(I, i, j) {
9191
// function f to handle that itself (e.g., by filtering as needed). The D3
9292
// reducers (e.g., min, max, mean, median) do, and it’s faster to avoid
9393
// redundant filtering.
94-
function reduceNumbers(f) {
94+
function reduceAccessor(f) {
9595
return (k, s, strict) =>
9696
strict
9797
? {
9898
mapIndex(I, S, T) {
99-
const C = Float64Array.from(I, (i) => (S[i] === null ? NaN : S[i]));
99+
const s = (i) => (S[i] == null ? NaN : +S[i]);
100100
let nans = 0;
101-
for (let i = 0; i < k - 1; ++i) if (isNaN(C[i])) ++nans;
101+
for (let i = 0; i < k - 1; ++i) if (isNaN(s(i))) ++nans;
102102
for (let i = 0, n = I.length - k + 1; i < n; ++i) {
103-
if (isNaN(C[i + k - 1])) ++nans;
104-
T[I[i + s]] = nans === 0 ? f(C.subarray(i, i + k)) : NaN;
105-
if (isNaN(C[i])) --nans;
103+
if (isNaN(s(i + k - 1))) ++nans;
104+
T[I[i + s]] = nans === 0 ? f(slice(I, i, i + k), s) : NaN;
105+
if (isNaN(s(i))) --nans;
106106
}
107107
}
108108
}
109109
: {
110110
mapIndex(I, S, T) {
111-
const C = Float64Array.from(I, (i) => (S[i] === null ? NaN : S[i]));
111+
const s = (i) => (S[i] == null ? NaN : +S[i]);
112112
for (let i = -s; i < 0; ++i) {
113-
T[I[i + s]] = f(C.subarray(0, i + k));
113+
T[I[i + s]] = f(slice(I, 0, i + k), s);
114114
}
115115
for (let i = 0, n = I.length - s; i < n; ++i) {
116-
T[I[i + s]] = f(C.subarray(i, i + k));
116+
T[I[i + s]] = f(slice(I, i, i + k), s);
117117
}
118118
}
119119
};
@@ -128,18 +128,18 @@ function reduceArray(f) {
128128
for (let i = 0; i < k - 1; ++i) count += defined(S[I[i]]);
129129
for (let i = 0, n = I.length - k + 1; i < n; ++i) {
130130
count += defined(S[I[i + k - 1]]);
131-
if (count === k) T[I[i + s]] = f(take(S, slice(I, i, i + k)));
131+
if (count === k) T[I[i + s]] = f(slice(I, i, i + k), S);
132132
count -= defined(S[I[i]]);
133133
}
134134
}
135135
}
136136
: {
137137
mapIndex(I, S, T) {
138138
for (let i = -s; i < 0; ++i) {
139-
T[I[i + s]] = f(take(S, slice(I, 0, i + k)));
139+
T[I[i + s]] = f(slice(I, 0, i + k), S);
140140
}
141141
for (let i = 0, n = I.length - s; i < n; ++i) {
142-
T[I[i + s]] = f(take(S, slice(I, i, i + k)));
142+
T[I[i + s]] = f(slice(I, i, i + k), S);
143143
}
144144
}
145145
};

test/output/aaplCloseNormalize.svg

Lines changed: 72 additions & 0 deletions
Loading

test/plots/aapl-close.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,19 @@ export async function aaplCloseGridIterable() {
4949
const AAPL = await d3.csv<any>("data/aapl.csv", d3.autoType);
5050
return Plot.lineY(AAPL, {x: "Date", y: "Close"}).plot({y: {grid: [100, 120, 140]}});
5151
}
52+
53+
export async function aaplCloseNormalize() {
54+
const aapl = await d3.csv<any>("data/aapl.csv", d3.autoType);
55+
const x = new Date("2014-01-01");
56+
const X = Plot.valueof(aapl, "Date");
57+
return Plot.plot({
58+
y: {type: "log", grid: true, tickFormat: ".1f"},
59+
marks: [
60+
Plot.ruleY([1]),
61+
Plot.lineY(
62+
aapl,
63+
Plot.normalizeY((I, Y) => Y[I.find((i) => X[i] >= x)], {x: X, y: "Close"})
64+
)
65+
]
66+
});
67+
}

0 commit comments

Comments
 (0)