Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2126,9 +2126,9 @@ The following aggregation methods are supported:
* *y1* - the lower bound of the bin’s *y* extent (when binning on *y*)
* *y2* - the upper bound of the bin’s *y* extent (when binning on *y*)
* a function to be passed the array of values for each bin and the extent of the bin
* an object with a *reduce* method, and optionally a *scope*
* an object with a *reduceIndex* method, and optionally a *scope*

In the last case, the *reduce* method is repeatedly passed three arguments: the index for each bin (an array of integers), the input channel’s array of values, and the extent of the bin (an object {x1, x2, y1, y2}); it must then return the corresponding aggregate value for the bin. If the reducer object’s *scope* is “data”, then the *reduce* method is first invoked for the full data; the return value of the *reduce* method is then made available as a third argument (making the extent the fourth argument). Similarly if the *scope* is “facet”, then the *reduce* method is invoked for each facet, and the resulting reduce value is made available while reducing the facet’s bins. (This optional *scope* is used by the *proportion* and *proportion-facet* reducers.)
In the last case, the *reduceIndex* method is repeatedly passed three arguments: the index for each bin (an array of integers), the input channel’s array of values, and the extent of the bin (an object {x1, x2, y1, y2}); it must then return the corresponding aggregate value for the bin. If the reducer object’s *scope* is “data”, then the *reduceIndex* method is first invoked for the full data; the return value of the *reduceIndex* method is then made available as a third argument (making the extent the fourth argument). Similarly if the *scope* is “facet”, then the *reduceIndex* method is invoked for each facet, and the resulting reduce value is made available while reducing the facet’s bins. (This optional *scope* is used by the *proportion* and *proportion-facet* reducers.)

Most aggregation methods require binding the output channel to an input channel; for example, if you want the **y** output channel to be a *sum* (not merely a count), there should be a corresponding **y** input channel specifying which values to sum. If there is not, *sum* will be equivalent to *count*.

Expand Down Expand Up @@ -2277,9 +2277,9 @@ The following aggregation methods are supported:
* *deviation* - the standard deviation
* *variance* - the variance per [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm)
* a function - passed the array of values for each group
* an object with a *reduce* method, an optionally a *scope*
* an object with a *reduceIndex* method, an optionally a *scope*

In the last case, the *reduce* method is repeatedly passed two arguments: the index for each group (an array of integers), and the input channel’s array of values; it must then return the corresponding aggregate value for the group. If the reducer object’s *scope* is “data”, then the *reduce* method is first invoked for the full data; the return value of the *reduce* method is then made available as a third argument. Similarly if the *scope* is “facet”, then the *reduce* method is invoked for each facet, and the resulting reduce value is made available while reducing the facet’s groups. (This optional *scope* is used by the *proportion* and *proportion-facet* reducers.)
In the last case, the *reduceIndex* method is repeatedly passed two arguments: the index for each group (an array of integers), and the input channel’s array of values; it must then return the corresponding aggregate value for the group. If the reducer object’s *scope* is “data”, then the *reduceIndex* method is first invoked for the full data; the return value of the *reduceIndex* method is then made available as a third argument. Similarly if the *scope* is “facet”, then the *reduceIndex* method is invoked for each facet, and the resulting reduce value is made available while reducing the facet’s groups. (This optional *scope* is used by the *proportion* and *proportion-facet* reducers.)

Most aggregation methods require binding the output channel to an input channel; for example, if you want the **y** output channel to be a *sum* (not merely a count), there should be a corresponding **y** input channel specifying which values to sum. If there is not, *sum* will be equivalent to *count*.

Expand Down Expand Up @@ -2357,9 +2357,9 @@ 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
* an object that implements the *map* method
* 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 *map* 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.
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.

The Plot.normalizeX and Plot.normalizeY transforms normalize series values relative to the given basis. For example, if the series values are [*y₀*, *y₁*, *y₂*, …] and the *first* basis is used, the mapped series values would be [*y₀* / *y₀*, *y₁* / *y₀*, *y₂* / *y₀*, …] as in an index chart. The **basis** option specifies how to normalize the series values. The following basis methods are supported:

Expand Down Expand Up @@ -2849,7 +2849,7 @@ The following aggregation methods are supported:
* *variance* - the variance per [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm)
* *mode* - the value with the most occurrences
* a function to be passed the array of values for each bin and the extent of the bin
* an object with a *reduce* method
* an object with a *reduceIndex* method

See also the [hexgrid](#hexgrid) mark.

Expand Down
2 changes: 1 addition & 1 deletion src/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export function channelDomain(data, facets, channels, facetChannels, options) {
X.domain = () => {
let domain = rollup(
range(XV),
(I) => reducer.reduce(I, YV),
(I) => reducer.reduceIndex(I, YV),
(i) => XV[i]
);
domain = sort(domain, reverse ? descendingGroup : ascendingGroup);
Expand Down
4 changes: 3 additions & 1 deletion src/marks/auto.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,9 @@ function isOrdinalReduced(reduce, value) {

// https://github.com/observablehq/plot/blob/818562649280e155136f730fc496e0b3d15ae464/src/transforms/group.js#L236
function isReducer(reduce) {
if (typeof reduce?.reduce === "function" && isObject(reduce)) return true; // N.B. array.reduce
if (reduce == null) return false;
if (typeof reduce.reduceIndex === "function") return true;
if (typeof reduce.reduce === "function" && isObject(reduce)) return true; // N.B. array.reduce
if (/^p\d{2}$/i.test(reduce)) return true;
switch (`${reduce}`.toLowerCase()) {
case "first":
Expand Down
2 changes: 1 addition & 1 deletion src/reducer.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export type ReducerFunction = (values: any[]) => any;

// TODO scope, label
export interface ReducerImplementation {
reduce(index: number[], values: any[]): any;
reduceIndex(index: number[], values: any[]): any;
}

export type Reducer = ReducerName | ReducerFunction | ReducerImplementation;
2 changes: 1 addition & 1 deletion src/transforms/bin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type BinReducerFunction = (values: any[], extent: {x1: any; y1: any; x2:

// TODO scope, label
export interface BinReducerImplementation {
reduce(index: number[], values: any[], extent: {x1: any; y1: any; x2: any; y2: any}): any;
reduceIndex(index: number[], values: any[], extent: {x1: any; y1: any; x2: any; y2: any}): any;
}

export interface BinOutputOptions extends BinOptions {
Expand Down
14 changes: 7 additions & 7 deletions src/transforms/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ function binn(
for (const [b, extent] of bin(g)) {
if (filter && !filter.reduce(b, extent)) continue;
groupFacet.push(i++);
groupData.push(reduceData.reduce(b, data, extent));
groupData.push(reduceData.reduceIndex(b, data, extent));
if (K) GK.push(k);
if (Z) GZ.push(G === Z ? f : Z[b[0]]);
if (F) GF.push(G === F ? f : F[b[0]]);
Expand Down Expand Up @@ -437,37 +437,37 @@ function mid1(x1, x2) {
}

const reduceX = {
reduce(I, X, {x1, x2}) {
reduceIndex(I, X, {x1, x2}) {
return mid1(x1, x2);
}
};

const reduceY = {
reduce(I, X, {y1, y2}) {
reduceIndex(I, X, {y1, y2}) {
return mid1(y1, y2);
}
};

const reduceX1 = {
reduce(I, X, {x1}) {
reduceIndex(I, X, {x1}) {
return x1;
}
};

const reduceX2 = {
reduce(I, X, {x2}) {
reduceIndex(I, X, {x2}) {
return x2;
}
};

const reduceY1 = {
reduce(I, X, {y1}) {
reduceIndex(I, X, {y1}) {
return y1;
}
};

const reduceY2 = {
reduce(I, X, {y2}) {
reduceIndex(I, X, {y2}) {
return y2;
}
};
41 changes: 24 additions & 17 deletions src/transforms/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ function groupn(
for (const [x, g] of maybeGroup(gg, X)) {
if (filter && !filter.reduce(g)) continue;
groupFacet.push(i++);
groupData.push(reduceData.reduce(g, data));
groupData.push(reduceData.reduceIndex(g, data));
if (X) GX.push(x);
if (Y) GY.push(y);
if (Z) GZ.push(G === Z ? f : Z[g[0]]);
Expand Down Expand Up @@ -178,7 +178,7 @@ export function maybeOutputs(outputs, inputs, asOutput = maybeOutput) {

export function maybeOutput(name, reduce, inputs, asEvaluator = maybeEvaluator) {
let scale; // optional per-channel scale override
if (isObject(reduce) && typeof reduce.reduce !== "function") (scale = reduce.scale), (reduce = reduce.reduce);
if (isObject(reduce) && "reduce" in reduce) (scale = reduce.scale), (reduce = reduce.reduce); // N.B. array.reduce
const evaluator = asEvaluator(name, reduce, inputs);
const [output, setOutput] = column(evaluator.label);
let O;
Expand Down Expand Up @@ -211,16 +211,16 @@ export function maybeEvaluator(name, reduce, inputs, asReduce = maybeReduce) {
initialize(data) {
V = input === undefined ? data : valueof(data, input);
if (reducer.scope === "data") {
context = reducer.reduce(range(data), V);
context = reducer.reduceIndex(range(data), V);
}
},
scope(scope, I) {
if (reducer.scope === scope) {
context = reducer.reduce(I, V);
context = reducer.reduceIndex(I, V);
}
},
reduce(I, extent) {
return reducer.scope == null ? reducer.reduce(I, V, extent) : reducer.reduce(I, V, context, extent);
return reducer.scope == null ? reducer.reduceIndex(I, V, extent) : reducer.reduceIndex(I, V, context, extent);
}
};
}
Expand All @@ -235,7 +235,9 @@ export function maybeGroup(I, X) {
}

export function maybeReduce(reduce, value, fallback = invalidReduce) {
if (typeof reduce?.reduce === "function" && isObject(reduce)) return reduce; // N.B. array.reduce
if (reduce == null) return fallback(reduce);
if (typeof reduce.reduceIndex === "function") return reduce;
if (typeof reduce.reduce === "function" && isObject(reduce)) return reduceReduce(reduce); // N.B. array.reduce
if (typeof reduce === "function") return reduceFunction(reduce);
if (/^p\d{2}$/i.test(reduce)) return reduceAccessor(percentile(reduce));
switch (`${reduce}`.toLowerCase()) {
Expand Down Expand Up @@ -299,45 +301,50 @@ export function maybeSort(facets, sort, reverse) {
}
}

function reduceReduce(reduce) {
console.warn("deprecated reduce interface; implement reduceIndex instead.");
return {...reduce, reduceIndex: reduce.reduce.bind(reduce)};
}

function reduceFunction(f) {
return {
reduce(I, X, extent) {
reduceIndex(I, X, extent) {
return f(take(X, I), extent);
}
};
}

function reduceAccessor(f) {
return {
reduce(I, X) {
reduceIndex(I, X) {
return f(I, (i) => X[i]);
}
};
}

function reduceMaybeTemporalAccessor(f) {
return {
reduce(I, X) {
reduceIndex(I, X) {
const x = f(I, (i) => X[i]);
return isTemporal(X) ? new Date(x) : x;
}
};
}

export const reduceIdentity = {
reduce(I, X) {
reduceIndex(I, X) {
return take(X, I);
}
};

export const reduceFirst = {
reduce(I, X) {
reduceIndex(I, X) {
return X[I[0]];
}
};

const reduceTitle = {
reduce(I, X) {
reduceIndex(I, X) {
const n = 5;
const groups = sort(
rollup(
Expand All @@ -357,21 +364,21 @@ const reduceTitle = {
};

const reduceLast = {
reduce(I, X) {
reduceIndex(I, X) {
return X[I[I.length - 1]];
}
};

export const reduceCount = {
label: "Frequency",
reduce(I) {
reduceIndex(I) {
return I.length;
}
};

const reduceDistinct = {
label: "Distinct",
reduce: (I, X) => {
reduceIndex(I, X) {
const s = new InternSet();
for (const i of I) s.add(X[i]);
return s.size;
Expand All @@ -382,6 +389,6 @@ const reduceSum = reduceAccessor(sum);

function reduceProportion(value, scope) {
return value == null
? {scope, label: "Frequency", reduce: (I, V, basis = 1) => I.length / basis}
: {scope, reduce: (I, V, basis = 1) => sum(I, (i) => V[i]) / basis};
? {scope, label: "Frequency", reduceIndex: (I, V, basis = 1) => I.length / basis}
: {scope, reduceIndex: (I, V, basis = 1) => sum(I, (i) => V[i]) / basis};
}
2 changes: 1 addition & 1 deletion src/transforms/map.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export type MapFunction<S, T = S> = (values: S[]) => T[];
export type MapName = "cumsum" | "rank" | "quantile";

export interface MapImplementation<S, T = S> {
map(index: number[], source: S[], target: T[]): void;
mapIndex(index: number[], source: S[], target: T[]): void;
}

export type Map = MapImplementation<any> | MapFunction<any> | MapName;
Expand Down
17 changes: 12 additions & 5 deletions src/transforms/map.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {count, group, rank} from "d3";
import {maybeZ, take, valueof, maybeInput, column} from "../options.js";
import {column, isObject, maybeInput, maybeZ, take, valueof} from "../options.js";
import {basic} from "./basic.js";

export function mapX(map, options = {}) {
Expand Down Expand Up @@ -31,7 +31,7 @@ export function map(outputs = {}, options = {}) {
const MX = channels.map(({setOutput}) => setOutput(new Array(data.length)));
for (const facet of facets) {
for (const I of Z ? group(facet, (i) => Z[i]).values() : [facet]) {
channels.forEach(({map}, i) => map.map(I, X[i], MX[i]));
channels.forEach(({map}, i) => map.mapIndex(I, X[i], MX[i]));
}
}
return {data, facets};
Expand All @@ -44,7 +44,9 @@ export function map(outputs = {}, options = {}) {
const mapAlias = map;

function maybeMap(map) {
if (map && typeof map.map === "function") return 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);
switch (`${map}`.toLowerCase()) {
case "cumsum":
Expand All @@ -57,14 +59,19 @@ function maybeMap(map) {
throw new Error(`invalid map: ${map}`);
}

function mapMap(map) {
console.warn("deprecated map interface; implement mapIndex instead.");
return {mapIndex: map.map.bind(map)};
}

function rankQuantile(V) {
const n = count(V) - 1;
return rank(V).map((r) => r / n);
}

function mapFunction(f) {
return {
map(I, S, T) {
mapIndex(I, S, T) {
const M = f(take(S, I));
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];
Expand All @@ -73,7 +80,7 @@ function mapFunction(f) {
}

const mapCumsum = {
map(I, S, T) {
mapIndex(I, S, T) {
let sum = 0;
for (const i of I) T[i] = sum += S[i];
}
Expand Down
6 changes: 3 additions & 3 deletions src/transforms/normalize.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function normalize(basis) {

function normalizeBasis(basis) {
return {
map(I, S, T) {
mapIndex(I, S, T) {
const b = +basis(I, S);
for (const i of I) {
T[i] = S[i] === null ? NaN : S[i] / b;
Expand All @@ -56,7 +56,7 @@ function normalizeAccessor(f) {
}

const normalizeExtent = {
map(I, S, T) {
mapIndex(I, S, T) {
const [s1, s2] = extent(I, (i) => S[i]),
d = s2 - s1;
for (const i of I) {
Expand All @@ -80,7 +80,7 @@ const normalizeLast = normalizeBasis((I, S) => {
});

const normalizeDeviation = {
map(I, S, T) {
mapIndex(I, S, T) {
const m = mean(I, (i) => S[i]);
const d = deviation(I, (i) => S[i]);
for (const i of I) {
Expand Down
Loading