diff --git a/docs/transforms/map.md b/docs/transforms/map.md index 4575520d9a..589f6e29f0 100644 --- a/docs/transforms/map.md +++ b/docs/transforms/map.md @@ -114,6 +114,7 @@ The following map methods are supported: * *rank* - the rank of each value in the sorted array * *quantile* - the rank, normalized between 0 and 1 * a function to be passed an array of values, returning new values +* a function to be passed an index and array of channel values, returning new values * an object that implements the *mapIndex* method 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. diff --git a/src/reducer.d.ts b/src/reducer.d.ts index 03fc739d18..f567a77a5b 100644 --- a/src/reducer.d.ts +++ b/src/reducer.d.ts @@ -52,8 +52,10 @@ export type ReducerName = | ReducerPercentile; /** - * A shorthand functional reducer implementation: given an array of input - * channel *values*, returns the corresponding reduced output value. + * A shorthand functional reducer implementation: given an *index* and the + * corresponding input channel *values* array, returns the corresponding reduced + * output value. If the function only takes a single argument, it is instead + * passed a subset of values from the input channel. */ export type ReducerFunction = ((index: number[], values: S[]) => T) | ((values: S[]) => T); diff --git a/src/transforms/map.d.ts b/src/transforms/map.d.ts index b069e41a42..e4178edaef 100644 --- a/src/transforms/map.d.ts +++ b/src/transforms/map.d.ts @@ -2,11 +2,15 @@ import type {ChannelName, ChannelValue} from "../channel.js"; import type {Transformed} from "./basic.js"; /** - * A shorthand functional map implementation: given an array of input channel - * *values*, returns the corresponding array of mapped output channel values. - * The returned array must have the same length as the given input. + * A shorthand functional map implementation: given an *index* and the + * corresponding input channel *values* array, returns the corresponding array + * of mapped output channel values. The returned array must have the same length + * as the given input index. If the function only takes a single argument, it is + * instead passed a subset of values from the input channel. */ -export type MapFunction = (values: S[]) => T[]; +export type MapFunction = + | ((index: number[], values: S[]) => ArrayLike) + | ((values: S[]) => ArrayLike); /** * The built-in map implementations; one of: diff --git a/src/transforms/map.js b/src/transforms/map.js index 2ab21e0a02..5b0e5a270c 100644 --- a/src/transforms/map.js +++ b/src/transforms/map.js @@ -1,5 +1,5 @@ import {count, group, rank} from "d3"; -import {column, identity, isObject, maybeInput, maybeZ, take, valueof} from "../options.js"; +import {column, identity, isObject, maybeInput, maybeZ, taker, valueof} from "../options.js"; import {basic} from "./basic.js"; export function mapX(mapper, options = {}) { @@ -50,14 +50,14 @@ function maybeMap(map) { if (map == null) throw new Error("missing map"); if (typeof map.mapIndex === "function") return map; if (typeof map.map === "function" && isObject(map)) return mapMap(map); // N.B. array.map - if (typeof map === "function") return mapFunction(map); + if (typeof map === "function") return mapFunction(taker(map)); switch (`${map}`.toLowerCase()) { case "cumsum": return mapCumsum; case "rank": - return mapFunction(rank); + return mapFunction((I, V) => rank(I, (i) => V[i])); case "quantile": - return mapFunction(rankQuantile); + return mapFunction((I, V) => rankQuantile(I, (i) => V[i])); } throw new Error(`invalid map: ${map}`); } @@ -67,15 +67,15 @@ function mapMap(map) { return {mapIndex: map.map.bind(map)}; } -function rankQuantile(V) { - const n = count(V) - 1; - return rank(V).map((r) => r / n); +function rankQuantile(I, f) { + const n = count(I, f) - 1; + return rank(I, f).map((r) => r / n); } function mapFunction(f) { return { mapIndex(I, S, T) { - const M = f(take(S, I)); + const M = f(I, S); if (M.length !== I.length) throw new Error("map function returned a mismatched length"); for (let i = 0, n = I.length; i < n; ++i) T[I[i]] = M[i]; } diff --git a/test/output/randomWalk.svg b/test/output/randomWalk.svg index f091a6ec7e..90d16b5cdc 100644 --- a/test/output/randomWalk.svg +++ b/test/output/randomWalk.svg @@ -14,32 +14,38 @@ } - - - - - - - - - - - - + + + + + + + + + + + + + + + - −35 - −30 - −25 - −20 - −15 - −10 - −5 - 0 - 5 - 10 - 15 - 20 + −8 + −6 + −4 + −2 + 0 + 2 + 4 + 6 + 8 + 10 + 12 + 14 + 16 + 18 + 20 @@ -65,10 +71,7 @@ 400 450 - - - - - + + \ No newline at end of file diff --git a/test/output/randomWalkCustomMap1.svg b/test/output/randomWalkCustomMap1.svg new file mode 100644 index 0000000000..90d16b5cdc --- /dev/null +++ b/test/output/randomWalkCustomMap1.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + −8 + −6 + −4 + −2 + 0 + 2 + 4 + 6 + 8 + 10 + 12 + 14 + 16 + 18 + 20 + + + + + + + + + + + + + + + 0 + 50 + 100 + 150 + 200 + 250 + 300 + 350 + 400 + 450 + + + + + \ No newline at end of file diff --git a/test/output/randomWalkCustomMap2.svg b/test/output/randomWalkCustomMap2.svg new file mode 100644 index 0000000000..90d16b5cdc --- /dev/null +++ b/test/output/randomWalkCustomMap2.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + −8 + −6 + −4 + −2 + 0 + 2 + 4 + 6 + 8 + 10 + 12 + 14 + 16 + 18 + 20 + + + + + + + + + + + + + + + 0 + 50 + 100 + 150 + 200 + 250 + 300 + 350 + 400 + 450 + + + + + \ No newline at end of file diff --git a/test/output/randomWalkCustomMap3.svg b/test/output/randomWalkCustomMap3.svg new file mode 100644 index 0000000000..90d16b5cdc --- /dev/null +++ b/test/output/randomWalkCustomMap3.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + −8 + −6 + −4 + −2 + 0 + 2 + 4 + 6 + 8 + 10 + 12 + 14 + 16 + 18 + 20 + + + + + + + + + + + + + + + 0 + 50 + 100 + 150 + 200 + 250 + 300 + 350 + 400 + 450 + + + + + \ No newline at end of file diff --git a/test/plots/random-walk.ts b/test/plots/random-walk.ts index 014abe2b8c..e3743bd039 100644 --- a/test/plots/random-walk.ts +++ b/test/plots/random-walk.ts @@ -1,12 +1,30 @@ import * as Plot from "@observablehq/plot"; import * as d3 from "d3"; +const random = () => d3.randomNormal.source(d3.randomLcg(42))(); + export async function randomWalk() { - const randomNormal = d3.randomNormal.source(d3.randomLcg(42))(); - return Plot.plot({ - marks: [ - Plot.lineY(d3.cumsum({length: 500} as any, randomNormal), {stroke: "red"}), - Plot.lineY({length: 500}, Plot.mapY("cumsum", {y: randomNormal, stroke: "blue"})) - ] - }); + return Plot.lineY({length: 500}, Plot.mapY("cumsum", {y: random()})).plot(); +} + +export async function randomWalkCustomMap1() { + const cumsum = (I, V) => ((sum) => Float64Array.from(I, (i) => (sum += V[i])))(0); + return Plot.lineY({length: 500}, Plot.mapY(cumsum, {y: random()})).plot(); +} + +export async function randomWalkCustomMap2() { + const cumsum = (V) => ((sum) => Float64Array.from(V, (v) => (sum += v)))(0); + return Plot.lineY({length: 500}, Plot.mapY(cumsum, {y: random()})).plot(); +} + +export async function randomWalkCustomMap3() { + const cumsum = { + mapIndex(I, S, T) { + let sum = 0; + for (const i of I) { + T[i] = sum += S[i]; + } + } + }; + return Plot.lineY({length: 500}, Plot.mapY(cumsum, {y: random()})).plot(); }