Skip to content

Commit fedb13b

Browse files
mbostockFil
andauthored
index-aware map method (#1559)
* index-aware map method * fix type * fix jsdocs for MapFunction and ReducerFunction * copy edits * test custom map methods * test mapIndex, too --------- Co-authored-by: Philippe Rivière <[email protected]>
1 parent 801b730 commit fedb13b

File tree

9 files changed

+309
-50
lines changed

9 files changed

+309
-50
lines changed

docs/transforms/map.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ The following map methods are supported:
114114
* *rank* - the rank of each value in the sorted array
115115
* *quantile* - the rank, normalized between 0 and 1
116116
* a function to be passed an array of values, returning new values
117+
* a function to be passed an index and array of channel values, returning new values
117118
* an object that implements the *mapIndex* method
118119

119120
If a function is used, it must return an array of the same length as the given input. If a *mapIndex* method is used, it is repeatedly passed the index for each series (an array of integers), the corresponding input channel’s array of values, and the output channel’s array of values; it must populate the slots specified by the index in the output array.

src/reducer.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ export type ReducerName =
5252
| ReducerPercentile;
5353

5454
/**
55-
* A shorthand functional reducer implementation: given an array of input
56-
* channel *values*, returns the corresponding reduced output value.
55+
* A shorthand functional reducer implementation: given an *index* and the
56+
* corresponding input channel *values* array, returns the corresponding reduced
57+
* output value. If the function only takes a single argument, it is instead
58+
* passed a subset of values from the input channel.
5759
*/
5860
export type ReducerFunction<S = any, T = S> = ((index: number[], values: S[]) => T) | ((values: S[]) => T);
5961

src/transforms/map.d.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ import type {ChannelName, ChannelValue} from "../channel.js";
22
import type {Transformed} from "./basic.js";
33

44
/**
5-
* A shorthand functional map implementation: given an array of input channel
6-
* *values*, returns the corresponding array of mapped output channel values.
7-
* The returned array must have the same length as the given input.
5+
* A shorthand functional map implementation: given an *index* and the
6+
* corresponding input channel *values* array, returns the corresponding array
7+
* of mapped output channel values. The returned array must have the same length
8+
* as the given input index. If the function only takes a single argument, it is
9+
* instead passed a subset of values from the input channel.
810
*/
9-
export type MapFunction<S = any, T = S> = (values: S[]) => T[];
11+
export type MapFunction<S = any, T = S> =
12+
| ((index: number[], values: S[]) => ArrayLike<T>)
13+
| ((values: S[]) => ArrayLike<T>);
1014

1115
/**
1216
* The built-in map implementations; one of:

src/transforms/map.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {count, group, rank} from "d3";
2-
import {column, identity, isObject, maybeInput, maybeZ, take, valueof} from "../options.js";
2+
import {column, identity, isObject, maybeInput, maybeZ, taker, valueof} from "../options.js";
33
import {basic} from "./basic.js";
44

55
export function mapX(mapper, options = {}) {
@@ -50,14 +50,14 @@ function maybeMap(map) {
5050
if (map == null) throw new Error("missing map");
5151
if (typeof map.mapIndex === "function") return map;
5252
if (typeof map.map === "function" && isObject(map)) return mapMap(map); // N.B. array.map
53-
if (typeof map === "function") return mapFunction(map);
53+
if (typeof map === "function") return mapFunction(taker(map));
5454
switch (`${map}`.toLowerCase()) {
5555
case "cumsum":
5656
return mapCumsum;
5757
case "rank":
58-
return mapFunction(rank);
58+
return mapFunction((I, V) => rank(I, (i) => V[i]));
5959
case "quantile":
60-
return mapFunction(rankQuantile);
60+
return mapFunction((I, V) => rankQuantile(I, (i) => V[i]));
6161
}
6262
throw new Error(`invalid map: ${map}`);
6363
}
@@ -67,15 +67,15 @@ function mapMap(map) {
6767
return {mapIndex: map.map.bind(map)};
6868
}
6969

70-
function rankQuantile(V) {
71-
const n = count(V) - 1;
72-
return rank(V).map((r) => r / n);
70+
function rankQuantile(I, f) {
71+
const n = count(I, f) - 1;
72+
return rank(I, f).map((r) => r / n);
7373
}
7474

7575
function mapFunction(f) {
7676
return {
7777
mapIndex(I, S, T) {
78-
const M = f(take(S, I));
78+
const M = f(I, S);
7979
if (M.length !== I.length) throw new Error("map function returned a mismatched length");
8080
for (let i = 0, n = I.length; i < n; ++i) T[I[i]] = M[i];
8181
}

test/output/randomWalk.svg

Lines changed: 32 additions & 29 deletions
Loading

test/output/randomWalkCustomMap1.svg

Lines changed: 77 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)