diff --git a/README.md b/README.md index bb1176c0d2..5bf49ec073 100644 --- a/README.md +++ b/README.md @@ -2287,7 +2287,7 @@ Most aggregation methods require binding the output channel to an input channel; Plot.groupX({y: "sum"}, {x: "species", y: "body_mass_g"}) ``` -You can control whether a channel is computed before or after grouping. If a channel is declared only in *options* (and it is not a special group-eligible channel such as *x*, *y*, *z*, *fill*, or stroke), it will be computed after grouping and be passed the grouped data: each datum is the array of input data corresponding to the current group. +You can control whether a channel is computed before or after grouping. If a channel is declared only in *options* (and it is not a special group-eligible channel such as *x*, *y*, *z*, *fill*, or *stroke*), it will be computed after grouping and be passed the grouped data: each datum is the array of input data corresponding to the current group. ```js Plot.groupX({y: "count"}, {x: "species", title: group => group.map(d => d.body_mass_g).join("\n")}) diff --git a/src/channel.d.ts b/src/channel.d.ts index 3dd6731a34..48cfe0f1e5 100644 --- a/src/channel.d.ts +++ b/src/channel.d.ts @@ -118,12 +118,12 @@ export interface Channel { /** * A channel’s values may be expressed as: * - * * a function that returns the corresponding value for each datum - * * a field name, to extract the corresponding value for each datum - * * an iterable of values, typically of the same length as the data - * * a channel transform that returns an iterable of values given the data - * * a constant date, number, or boolean - * * null to represent no value + * - a function that returns the corresponding value for each datum + * - a field name, to extract the corresponding value for each datum + * - an iterable of values, typically of the same length as the data + * - a channel transform that returns an iterable of values given the data + * - a constant date, number, or boolean + * - null to represent no value */ export type ChannelValue = | Iterable // column of values @@ -152,10 +152,10 @@ export type ChannelValueIntervalSpec = ChannelValueSpec | {value: ChannelValue; * The available inputs for imputing scale domains. In addition to a named * channel, an input may be specified as: * - * * *data* - impute from mark data - * * *width* - impute from |*x2* - *x1*| - * * *height* - impute from |*y2* - *y1*| - * * null - impute from input order + * - *data* - impute from mark data + * - *width* - impute from |*x2* - *x1*| + * - *height* - impute from |*y2* - *y1*| + * - null - impute from input order */ export type ChannelDomainValue = ChannelName | "data" | "width" | "height" | null; @@ -163,31 +163,15 @@ export type ChannelDomainValue = ChannelName | "data" | "width" | "height" | nul export interface ChannelDomainOptions { /** * How to produce a singular value (for subsequent sorting) from aggregated - * channel values. Defaults to *max*. A reducer may be specified as: + * channel values; one of: * - * * *first* - the first value, in input order - * * *last* - the last value, in input order - * * *count* - the number of elements (frequency) - * * *distinct* - the number of distinct values - * * *sum* - the sum of values - * * *min* - the minimum value - * * *min-index* - the zero-based index of the minimum value - * * *max* - the maximum value - * * *max-index* - the zero-based index of the maximum value - * * *mean* - the mean value (average) - * * *median* - the median value - * * *mode* - the value with the most occurrences - * * *pXX* - the percentile value, where XX is a number in [00,99] - * * *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 to be passed the array of values - * * an object with a *reduce* method - * - * In the last case, the *reduce* method is repeatedly passed an index (an - * array of integers) and the channel’s array of values; it must then return - * the corresponding aggregate value for the bin. + * - true (default) - alias for *max* + * - false or null - disabled; don’t impute the scale domain + * - a named reducer implementation such as *count* or *sum* + * - a function that takes an array of values and returns the reduced value + * - an object that implements the *reduceIndex* method */ - reduce?: Reducer | true; + reduce?: Reducer | boolean | null; /** If true, use descending instead of ascending order. */ reverse?: boolean; @@ -209,7 +193,20 @@ export type ChannelDomainValueSpec = ChannelDomainValue | ({value: ChannelDomain /** How to impute scale domains from channel values. */ export type ChannelDomainSort = {[key in ScaleName]?: ChannelDomainValueSpec} & ChannelDomainOptions; -/** How to reduce channel values, e.g. when binning or grouping. */ +/** + * Output channels for aggregating transforms, such as bin and group. Each + * declared output channel has an associated reducer, and typically a + * corresponding input channel in *options*. Non-grouping channels declared in + * *options* but not *outputs* are computed on reduced data after grouping, + * which defaults to the array of data for the current group. + * + * If **title** is in *options* but not *outputs*, the reducer defaults to + * summarizing the most common values. If **href** is in *options* but not + * *outputs*, the reducer defaults to *first*. When **x1** or **x2** is in + * *outputs*, reads the input channel **x** if **x1** or **x2** is not in + * *options*; likewise for **y1** or **y2**, reads the input channel **y** if + * **y1** or **y2** is not in *options*. + */ export type ChannelReducers = {[key in ChannelName]?: T | {reduce: T; scale?: Channel["scale"]} | null}; /** Abstract (unscaled) values, and associated scale, per channel. */ diff --git a/src/interval.d.ts b/src/interval.d.ts index faf82793ce..6e86bcd6f1 100644 --- a/src/interval.d.ts +++ b/src/interval.d.ts @@ -1,4 +1,9 @@ -/** The built-in time intervals; UTC or local time, depending on context. */ +/** + * The built-in time intervals; UTC or local time, depending on context. The + * *week* interval is an alias for *sunday*. The *quarter* interval is every + * three months, and the *half* interval is every six months, aligned at the + * start of the year. + */ export type TimeIntervalName = | "second" | "minute" @@ -65,15 +70,15 @@ export interface RangeIntervalImplementation extends IntervalImplementation extends RangeIntervalImplementation { /** - * Returns a new date representing the earliest interval boundary date after - * or equal to date. For example, d3.timeDay.ceil(date) typically returns - * 12:00 AM local time on the date following the given date. + * Returns the value representing the least interval boundary value greater + * than or equal to the specified *value*. For example, day.ceil(*date*) + * typically returns 12:00 AM on the date following the given date. * * This method is idempotent: if the specified date is already ceilinged to - * the current interval, a new date with an identical time is returned. - * Furthermore, the returned date is the maximum expressible value of the - * associated interval, such that interval.ceil(interval.ceil(date) + 1) - * returns the following interval boundary date. + * the current interval, the same value is returned. Furthermore, the returned + * value is the maximum expressible value of the associated interval, such + * that ceil(ceil(*value*) + *epsilon*) returns the following interval + * boundary value. */ ceil(value: T): T; } @@ -81,11 +86,32 @@ export interface NiceIntervalImplementation extends RangeIntervalImplementati /** A literal that can be automatically promoted to an interval. */ type LiteralInterval = T extends Date ? TimeIntervalName : T extends number ? number : never; -/** How to partition a continuous range into discrete intervals. */ +/** + * How to partition a continuous range into discrete intervals; one of: + * + * - an object that implements *floor* and *offset* methods + * - a named time interval such as *day* (for date intervals) + * - a number (for number intervals), defining intervals at integer multiples of *n* + */ export type Interval = LiteralInterval | IntervalImplementation; -/** An interval that supports the range method, say for thresholds or ticks. */ +/** + * An interval that also supports the *range* method, used to subdivide a + * continuous range into discrete partitions, say for thresholds or ticks; one + * of: + * + * - an object that implements *floor*, *offset*, and *range* methods + * - a named time interval such as *day* (for date intervals) + * - a number (for number intervals), defining intervals at integer multiples of *n* + */ export type RangeInterval = LiteralInterval | RangeIntervalImplementation; -/** An interval that can be used to nice a scale domain. */ +/** + * A range interval that also supports the *ceil* method, used to nice a scale + * domain; one of: + * + * - an object that implements *floor*, *ceil*, *offset*, and *range* methods + * - a named time interval such as *day* (for date intervals) + * - a number (for number intervals), defining intervals at integer multiples of *n* + */ export type NiceInterval = LiteralInterval | NiceIntervalImplementation; diff --git a/src/marks/vector.d.ts b/src/marks/vector.d.ts index ea044277f9..fef4ba5dbf 100644 --- a/src/marks/vector.d.ts +++ b/src/marks/vector.d.ts @@ -6,6 +6,7 @@ export type VectorShapeName = "arrow" | "spike"; /** A vector shape implementation. */ export interface VectorShapeImplementation { + /** Draws a shape of the given *length* and *radius* to the given *context*. */ draw(context: CanvasPath, length: number, radius: number): void; } @@ -53,9 +54,9 @@ export interface VectorOptions extends MarkOptions { * The vector’s position along its orientation relative to its anchor point; a * constant. Assuming a default **rotate** angle of 0°, one of: * - * * *start* - from [*x*, *y*] to [*x*, *y* - *l*] - * * *middle* (default) - from [*x*, *y* + *l* / 2] to [*x*, *y* - *l* / 2] - * * *end* - from [*x*, *y* + *l*] to [*x*, *y*] + * - *start* - from [*x*, *y*] to [*x*, *y* - *l*] + * - *middle* (default) - from [*x*, *y* + *l* / 2] to [*x*, *y* - *l* / 2] + * - *end* - from [*x*, *y* + *l*] to [*x*, *y*] * * where [*x*, *y*] is the vector’s anchor point and *l* is the vector’s * (possibly scaled) length in pixels. diff --git a/src/reducer.d.ts b/src/reducer.d.ts index 3ccc311069..8602e355b2 100644 --- a/src/reducer.d.ts +++ b/src/reducer.d.ts @@ -1,11 +1,33 @@ type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; +// For internal use. export type ReducerPercentile = | (`p${Digit}${Digit}` & Record) // see https://github.com/microsoft/TypeScript/issues/29729 | "p25" | "p50" | "p75"; +/** + * The built-in reducer implementations; one of: + * + * - *first* - the first value, in input order + * - *last* - the last value, in input order + * - *count* - the number of elements (frequency) + * - *distinct* - the number of distinct values + * - *sum* - the sum of values + * - *proportion* - the sum proportional to the overall total (weighted frequency) + * - *proportion-facet* - the sum proportional to the facet total + * - *deviation* - the standard deviation + * - *min* - the minimum value + * - *min-index* - the zero-based index of the minimum value + * - *max* - the maximum value + * - *max-index* - the zero-based index of the maximum value + * - *mean* - the mean value (average) + * - *median* - the median value + * - *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 + * - *pXX* - the percentile value, where XX is a number in [00,99] + */ export type ReducerName = | "first" | "last" @@ -25,11 +47,30 @@ export type ReducerName = | "mode" | ReducerPercentile; -export type ReducerFunction = (values: any[]) => any; +/** + * A shorthand functional reducer implementation: given an array of input + * channel *values*, returns the corresponding reduced output value. + */ +export type ReducerFunction = (values: S[]) => T; -// TODO scope, label -export interface ReducerImplementation { - reduceIndex(index: number[], values: any[]): any; +/** A reducer implementation. */ +export interface ReducerImplementation { + /** + * Given an *index* representing the contents of the current group and the + * input channel’s array of *values*, returns the corresponding reduced output + * value. If no input channel is supplied (e.g., as with the *count* reducer) + * then *values* may be undefined. + */ + reduceIndex(index: number[], values: S[]): T; + // TODO scope + // TODO label } +/** + * How to reduce aggregated (binned or grouped) values; one of: + * + * - a named reducer implementation such as *count* or *sum* + * - a function that takes an array of values and returns the reduced value + * - an object that implements the *reduceIndex* method + */ export type Reducer = ReducerName | ReducerFunction | ReducerImplementation; diff --git a/src/transforms/bin.d.ts b/src/transforms/bin.d.ts index bde83fc51f..2961eb7687 100644 --- a/src/transforms/bin.d.ts +++ b/src/transforms/bin.d.ts @@ -1,21 +1,118 @@ -import type {ChannelReducers} from "../channel.js"; +import type {ChannelReducers, ChannelValue} from "../channel.js"; import type {RangeInterval} from "../interval.js"; import type {Reducer} from "../reducer.js"; import type {Transformed} from "./basic.js"; +import type {GroupOutputOptions} from "./group.js"; +/** + * The built-in thresholds implementations; one of: + * + * - *auto* (default) - like *scott*, but capped at 200 bins + * - *freedman-diaconis* - the [Freedman–Diaconis rule](https://en.wikipedia.org/wiki/Freedman–Diaconis_rule) + * - *scott* - [Scott’s normal reference rule](https://en.wikipedia.org/wiki/Histogram#Scott.27s_normal_reference_rule) + * - *sturges* - [Sturges’ formula](https://en.wikipedia.org/wiki/Histogram#Sturges.27_formula) + */ export type ThresholdsName = "freedman-diaconis" | "scott" | "sturges" | "auto"; -export type ThresholdsFunction = (values: any[], min: any, max: any) => any[]; +/** + * A functional shorthand thresholds implementation; given an array of observed + * *values* from the domain, and the *min* and *max* representing the extent of + * the domain, returns the corresponding desired thresholds as one of: + * + * - a range interval + * - an array of *n* threshold values for *n* - 1 bins + * - a count representing the desired number of bins (a hint; not guaranteed) + */ +export type ThresholdsFunction = (values: T[], min: T, max: T) => RangeInterval | T[] | number; -export type Thresholds = ThresholdsName | ThresholdsFunction | RangeInterval; +/** + * How to subdivide a continuous domain into discrete bins; one of: + * + * - a named threshold implementation such as *auto* (default) or *sturges* + * - a function that returns an array, count, or range interval + * - a range interval + * - an array of *n* threshold values for *n* - 1 bins + * - a count representing the desired number of bins (a hint; not guaranteed) + * + * When thresholds are specified as a desired number of bins, or with the + * built-in thresholds implementations, + * [d3.ticks](https://github.com/d3/d3-array/blob/main/README.md#ticks) is used + * for numeric domains and + * [d3.utcTicks](https://github.com/d3/d3-time/blob/main/README.md#utcTicks) is + * used for temporal domains. + */ +export type Thresholds = ThresholdsName | ThresholdsFunction | RangeInterval | T[] | number; +/** Options for the bin transform. */ export interface BinOptions { + /** + * If false or zero (default), produce a frequency distribution; if true or a + * positive number, produce a cumulative distribution; if a negative number, + * produce a [complementary cumulative](https://en.wikipedia.org/wiki/Cumulative_distribution_function#Complementary_cumulative_distribution_function_.28tail_distribution.29) + * distribution. + */ cumulative?: boolean | number; + + /** + * The domain of allowed values; optional. If specified as [*min*, *max*], + * values outside this extent will be ignored. If a function, it is passed the + * observed input values and must return the domain [*min*, *max*]. When + * **thresholds** are specified as an interval and no domain is specified, the + * effective domain will be extended to align with the interval. + */ domain?: ((values: any[]) => [min: any, max: any]) | [min: any, max: any]; + + /** + * How to subdivide the domain into discrete bins; defaults to *auto*; one of: + * + * - a named threshold implementation such as *auto* (default) or *sturges* + * - a function that returns an array, count, or range interval + * - a range interval + * - an array of *n* threshold values for *n* - 1 bins + * - a count representing the desired number of bins (a hint; not guaranteed) + * + * For example, for about ten bins: + * + * ```js + * Plot.rectY(numbers, Plot.binX({y: "count"}, {thresholds: 10})) + * ``` + */ thresholds?: Thresholds; + + /** + * How to subdivide the domain into discrete bins; a stricter alternative to + * the **thresholds** option allowing the use of shorthand numeric intervals; + * one of: + * + * - an object that implements *floor*, *offset*, and *range* methods + * - a named time interval such as *day* (for date intervals) + * - a number (for number intervals), defining intervals at integer multiples of *n* + * + * For example, for integer bins: + * + * ```js + * Plot.rectY(numbers, Plot.binX({y: "count"}, {interval: 1})) + * ``` + */ interval?: RangeInterval; } +/** + * How to reduce binned values; one of: + * + * - a generic reducer name, such as *count* or *first* + * - *x* - the middle of the bin’s *x* extent (when binning on *x*) + * - *x1* - the lower bound of the bin’s *x* extent (when binning on *x*) + * - *x2* - the upper bound of the bin’s *x* extent (when binning on *x*) + * - *y* - the middle of the bin’s *y* extent (when binning on *y*) + * - *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 that takes an array of values and returns the reduced value + * - an object that implements the *reduceIndex* method + * + * When a reducer function or implementation is used with the bin transform, it + * is passed the bin extent {x1, x2, y1, y2} as an additional argument. + */ export type BinReducer = | Reducer | BinReducerFunction @@ -27,25 +124,138 @@ export type BinReducer = | "y1" | "y2"; -export type BinReducerFunction = (values: any[], extent: {x1: any; y1: any; x2: any; y2: any}) => any; +/** + * A shorthand functional bin reducer implementation: given an array of input + * channel *values*, and the current bin’s *extent*, returns the corresponding + * reduced output value. + */ +export type BinReducerFunction = (values: S[], extent: {x1: any; y1: any; x2: any; y2: any}) => T; -// TODO scope, label -export interface BinReducerImplementation { - reduceIndex(index: number[], values: any[], extent: {x1: any; y1: any; x2: any; y2: any}): any; +/** A bin reducer implementation. */ +export interface BinReducerImplementation { + /** + * Given an *index* representing the contents of the current bin, the input + * channel’s array of *values*, and the current bin’s *extent*, returns the + * corresponding reduced output value. If no input channel is supplied (e.g., + * as with the *count* reducer) then *values* may be undefined. + */ + reduceIndex(index: number[], values: S[], extent: {x1: any; y1: any; x2: any; y2: any}): T; + // TODO scope + // TODO label } -export interface BinOutputOptions extends BinOptions { - data?: BinReducer | null; - filter?: BinReducer | null; - sort?: BinReducer | null; - reverse?: boolean; -} +/** + * When binning on **x** or **y**, you can specify the channel values as a + * {value} object to provide separate bin options for each. + */ +export type ChannelValueBinSpec = ChannelValue | ({value: ChannelValue} & BinOptions); + +/** Inputs to the binX transform. */ +export type BinXInputs = Omit & {x?: ChannelValueBinSpec} & BinOptions; + +/** Inputs to the binY transform. */ +export type BinYInputs = Omit & {y?: ChannelValueBinSpec} & BinOptions; + +/** Inputs to the bin transform. */ +export type BinInputs = Omit & {x?: ChannelValueBinSpec; y?: ChannelValueBinSpec} & BinOptions; -/** How to reduce binned channel values. */ -export type BinOutputs = ChannelReducers & BinOutputOptions; +/** Output channels (and options) for the bin transform. */ +export type BinOutputs = ChannelReducers & GroupOutputOptions & BinOptions; -export function binX(outputs?: BinOutputs, options?: T & BinOptions): Transformed; +/** + * Bins on the **x** channel; then subdivides bins on the first channel of + * **z**, **fill**, or **stroke**, if any; then further subdivides bins on the + * **y** channel, if any and if none of **y**, **y1**, and **y2** are in + * *outputs*; and lastly for each channel in the specified *outputs*, applies + * the corresponding *reduce* method to produce new channel values from the + * binned input channel values. Each *reduce* method may be one of: + * + * - a named reducer implementation such as *count* or *sum* + * - a function that takes an array of values and returns the reduced value + * - an object that implements the *reduceIndex* method + * + * For example, for a histogram of observed culmen lengths: + * + * ```js + * Plot.rectY(penguins, Plot.binX({y: "count"}, {x: "culmen_length_mm"})) + * ``` + * + * The binX transform is often used with the rectY mark to make histograms; it + * is intended for aggregating continuous quantitative or temporal data, such as + * temperatures or times, into discrete bins. See the groupX transform for + * ordinal or categorical data. + * + * If **x** is not in *options*, it defaults to identity. If **x** is not in + * *outputs*, by default produces **x1** and **x2** output channels representing + * the extent of each bin and an **x** output channel representing the bin + * midpoint, say for for labels. If **y** is not in outputs, **y1** and **y2** + * will be dropped from the returned *options*. The **insetLeft** and + * **insetRight** options default to 0.5. + */ +export function binX(outputs?: BinOutputs, options?: BinXInputs): Transformed; -export function binY(outputs?: BinOutputs, options?: T & BinOptions): Transformed; +/** + * Bins on the **y** channel; then subdivides bins on the first channel of + * **z**, **fill**, or **stroke**, if any; then further subdivides bins on the + * **x** channel, if any and if none of **x**, **x1**, and **x2** are in + * *outputs*; and lastly for each channel in the specified *outputs*, applies + * the corresponding *reduce* method to produce new channel values from the + * binned input channel values. Each *reduce* method may be one of: + * + * - a named reducer implementation such as *count* or *sum* + * - a function that takes an array of values and returns the reduced value + * - an object that implements the *reduceIndex* method + * + * For example, for a histogram of observed culmen lengths: + * + * ```js + * Plot.rectX(penguins, Plot.binY({x: "count"}, {y: "culmen_length_mm"})) + * ``` + * + * The binY transform is often used with the rectX mark to make histograms; it + * is intended for aggregating continuous quantitative or temporal data, such as + * temperatures or times, into discrete bins. See the groupY transform for + * ordinal or categorical data. + * + * If **y** is not in *options*, it defaults to identity. If **y** is not in + * *outputs*, by default produces **y1** and **y2** output channels representing + * the extent of each bin and a **y** output channel representing the bin + * midpoint, say for for labels. If **x** is not in outputs, **x1** and **x2** + * will be dropped from the returned *options*. The **insetTop** and + * **insetBottom** options default to 0.5. + */ +export function binY(outputs?: BinOutputs, options?: BinYInputs): Transformed; -export function bin(outputs?: BinOutputs, options?: T & BinOptions): Transformed; +/** + * Bins on the **x** and **y** channels; then subdivides bins on the first + * channel of **z**, **fill**, or **stroke**, if any; and lastly for each + * channel in the specified *outputs*, applies the corresponding *reduce* method + * to produce new channel values from the binned input channel values. Each + * *reduce* method may be one of: + * + * - a named reducer implementation such as *count* or *sum* + * - a function that takes an array of values and returns the reduced value + * - an object that implements the *reduceIndex* method + * + * For example, for a heatmap of observed culmen lengths and depths: + * + * ```js + * Plot.rect(penguins, Plot.bin({fill: "count"}, {x: "culmen_depth_mm", y: "culmen_length_mm"})) + * ``` + * + * The bin transform is often used with the rect mark to make heatmaps; it is + * intended for aggregating continuous quantitative or temporal data, such as + * temperatures or times, into discrete bins. See the group transform for + * ordinal or categorical data. + * + * If neither **x** nor **y** are in *options*, then **x** and **y** default to + * accessors assuming that *data* contains tuples [[*x₀*, *y₀*], [*x₁*, *y₁*], + * [*x₂*, *y₂*], …]. If **x** is not in *outputs*, by default produces **x1** + * and **x2** output channels representing the horizontal extent of each bin and + * a **x** output channel representing the horizontal midpoint, say for for + * labels. Likewise if **y** is not in *outputs*, by default produces **y1** and + * **y2** output channels representing the vertical extent of each bin and a + * **y** output channel representing the vertical midpoint. The **insetTop**, + * **insetRight**, **insetBottom**, and **insetLeft** options default to 0.5. + */ +export function bin(outputs?: BinOutputs, options?: BinInputs): Transformed; diff --git a/src/transforms/group.d.ts b/src/transforms/group.d.ts index d88d99fa3a..a48caecc4e 100644 --- a/src/transforms/group.d.ts +++ b/src/transforms/group.d.ts @@ -2,19 +2,141 @@ import type {ChannelReducers} from "../channel.js"; import type {Reducer} from "../reducer.js"; import type {Transformed} from "./basic.js"; -export interface GroupOutputOptions { - data?: Reducer | null; - filter?: Reducer | null; - sort?: Reducer | null; +/** Options for outputs of the group (and bin) transform. */ +export interface GroupOutputOptions { + /** + * How to reduce data; defaults to the identity reducer, outputting the subset + * of data for each group in input order. + */ + data?: T; + + /** + * How to filter groups: if the reducer emits a falsey value, the group will + * be dropped; by default, empty groups are dropped. Use null to disable + * filtering and return all groups, for example to impute missing zeroes when + * summing values for a line chart. + */ + filter?: T | null; + + /** + * How to order groups; if null (default), groups are returned in ascending + * natural order along *x*, *y*, and *z* (or *fill* or *stroke*). Group order + * affects draw order of overlapping marks, and may be useful with the stack + * transform which defaults to input order. For example to place the smallest + * group within each stack on the baseline: + * + * ```js + * Plot.groupX({y: "count", sort: "count"}, {fill: "sex", x: "sport"}) + * ``` + */ + sort?: T | null; + + /** If true, reverse the order of generated groups; defaults to false. */ reverse?: boolean; } +/** Output channels (and options) for the group transform. */ export type GroupOutputs = ChannelReducers & GroupOutputOptions; +/** + * Groups on the first channel of **z**, **fill**, or **stroke**, if any, and + * then for each channel in the specified *outputs*, applies the corresponding + * *reduce* method to produce new channel values from the grouped input channel + * values. Each *reduce* method may be one of: + * + * - a named reducer implementation such as *count* or *sum* + * - a function that takes an array of values and returns the reduced value + * - an object that implements the *reduceIndex* method + * + * For example, for a horizontal stacked bar chart: + * + * ```js + * Plot.barX(penguins, Plot.groupZ({x: "proportion"}, {fill: "species"})) + * ``` + */ export function groupZ(outputs?: GroupOutputs, options?: T): Transformed; +/** + * Groups on the **x** channel; then subdivides groups on the first channel of + * **z**, **fill**, or **stroke**, if any; and then for each channel in the + * specified *outputs*, applies the corresponding *reduce* method to produce new + * channel values from the grouped input channel values. Each *reduce* method + * may be one of: + * + * - a named reducer implementation such as *count* or *sum* + * - a function that takes an array of values and returns the reduced value + * - an object that implements the *reduceIndex* method + * + * For example, for a vertical bar chart of species by total mass: + * + * ```js + * Plot.barY(penguins, Plot.groupX({y: "sum"}, {x: "species", y: "body_mass_g"})) + * ``` + * + * The groupX transform is often used with the barY mark to make bar charts; it + * is intended for aggregating ordinal or categorical data, such as names. See + * the binX transform for continuous data. + * + * If **x** is not in *options*, it defaults to identity. If **x** is not in + * *outputs*, it defaults to *first*, and the **x1** and **x2** channels, if + * any, will be dropped from the returned *options*. + */ export function groupX(outputs?: GroupOutputs, options?: T): Transformed; +/** + * Groups on the **y** channel; then subdivides groups on the first channel of + * **z**, **fill**, or **stroke**, if any; and then for each channel in the + * specified *outputs*, applies the corresponding *reduce* method to produce new + * channel values from the grouped input channel values. Each *reduce* method + * may be one of: + * + * - a named reducer implementation such as *count* or *sum* + * - a function that takes an array of values and returns the reduced value + * - an object that implements the *reduceIndex* method + * + * For example, for a horizontal bar chart of species by total mass: + * + * ```js + * Plot.barX(penguins, Plot.groupY({x: "sum"}, {y: "species", x: "body_mass_g"})) + * ``` + * + * The groupY transform is often used with the barX mark to make bar charts; it + * is intended for aggregating ordinal or categorical data, such as names. See + * the binY transform for continuous data. + * + * If **y** is not in *options*, it defaults to identity. If **y** is not in + * *outputs*, it defaults to *first*, and the **y1** and **y2** channels, if + * any, will be dropped from the returned *options*. + */ export function groupY(outputs?: GroupOutputs, options?: T): Transformed; +/** + * Groups on the **x** and **y** channels; then subdivides groups on the first + * channel of **z**, **fill**, or **stroke**, if any; and then for each channel + * in the specified *outputs*, applies the corresponding *reduce* method to + * produce new channel values from the grouped input channel values. Each + * *reduce* method may be one of: + * + * - a named reducer implementation such as *count* or *sum* + * - a function that takes an array of values and returns the reduced value + * - an object that implements the *reduceIndex* method + * + * For example, for a heatmap of penguins by species and island: + * + * ```js + * Plot.cell(penguins, Plot.group({fill: "count"}, {x: "island", y: "species"})) + * ``` + * + * The group transform is often used with the cell mark to make heatmaps; it is + * intended for aggregating ordinal or categorical data, such as names. See the + * bin transform for continuous data. + * + * If neither **x** nor **y** are in *options*, then **x** and **y** default to + * accessors assuming that *data* contains tuples [[*x₀*, *y₀*], [*x₁*, *y₁*], + * [*x₂*, *y₂*], …]. If **x** is not in *outputs*, it defaults to *first*, and + * the **x1** and **x2** channels, if any, will be dropped from the returned + * *options*. Likewise if **y** is not in *outputs*, it defaults to *first*, and + * the **y1** and **y2** channels, if any, will be dropped from the returned + * *options*. + */ export function group(outputs?: GroupOutputs, options?: T): Transformed; diff --git a/src/transforms/hexbin.d.ts b/src/transforms/hexbin.d.ts index 7deacafbb4..6f6adedff3 100644 --- a/src/transforms/hexbin.d.ts +++ b/src/transforms/hexbin.d.ts @@ -17,39 +17,30 @@ export interface HexbinOptions { } /** - * Groups points specified by the *x* and *y* channels into hexagonal bins in - * scaled coordinates (pixels), computing new *x* and *y* channels as the - * centers of each bin, and deriving new output channels by applying the - * specified reducers (such as *count*) to each bin’s values. The first of the - * *z*, *fill*, or *stroke* channels, if any, will be used to subdivide bins. + * Bins hexagonally on the scaled **x** and **y** channels; then subdivides bins + * on the first channel of **z**, **fill**, or **stroke**, if any; and lastly + * for each channel in the specified *outputs*, applies the corresponding + * *reduce* method to produce new channel values from the binned input channel + * values. Each *reduce* method may be one of: * - * The hexbin transform can be applied to any mark that consumes *x* and *y*, - * such as the dot, image, text, and vector marks. For the dot mark, the - * **symbol** option defaults to *hexagon*, and the *r* option defaults to half - * the **binWidth**. If a **fill** output channel is declared, the **stroke** - * option defaults to *none*. + * - a named reducer implementation such as *count* or *sum* + * - a function that takes an array of values and returns the reduced value + * - an object that implements the *reduceIndex* method * - * The reducer for each channel in *outputs* may be specified as: + * For example, for a heatmap of observed culmen lengths and depths: * - * * *first* - the first value, in input order - * * *last* - the last value, in input order - * * *count* - the number of elements (frequency) - * * *distinct* - the number of distinct values - * * *sum* - the sum of values - * * *proportion* - the sum proportional to the overall total (weighted frequency) - * * *proportion-facet* - the sum proportional to the facet total - * * *min* - the minimum value - * * *min-index* - the zero-based index of the minimum value - * * *max* - the maximum value - * * *max-index* - the zero-based index of the maximum value - * * *mean* - the mean value (average) - * * *median* - the median value - * * *deviation* - the standard deviation - * * *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 - * * an object with a *reduceIndex* method + * ```js + * Plot.dot(penguins, Plot.hexbin({fill: "count"}, {x: "culmen_depth_mm", y: "culmen_length_mm"})) + * ``` * - * See also the hexgrid mark. + * The hexbin transform can be applied to any mark that consumes **x** and + * **y**, such as the dot, image, text, and vector marks; it is intended for + * aggregating continuous quantitative or temporal data, such as temperatures or + * times, into discrete hexagonal bins. For the dot mark, the **symbol** option + * defaults to *hexagon*, and the *r* option defaults to half the **binWidth**. + * If a **fill** output channel is declared, the **stroke** option defaults to + * *none*. + * + * To draw empty hexagons, see the hexgrid mark. */ export function hexbin(outputs?: ChannelReducers, options?: T & HexbinOptions): Initialized; diff --git a/src/transforms/map.d.ts b/src/transforms/map.d.ts index 95c4f04b30..2fa3774baa 100644 --- a/src/transforms/map.d.ts +++ b/src/transforms/map.d.ts @@ -1,27 +1,50 @@ import type {ChannelName, ChannelValue} from "../channel.js"; import type {Transformed} from "./basic.js"; -/** A shorthand functional map implementation (from source S to target T). */ +/** + * 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. + */ export type MapFunction = (values: S[]) => T[]; -/** The built-in map implementations. */ +/** + * The built-in map implementations; one of: + * + * - *cumsum* - a cumulative sum + * - *rank* - the rank of each value in the sorted array + * - *quantile* - the rank, normalized between 0 and 1 + */ export type MapName = "cumsum" | "rank" | "quantile"; -/** A map implementation (from source S to target T). */ +/** A map implementation. */ export interface MapImplementation { /** - * This method 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. + * Given an *index* representing the contents of the current series, the input + * channel’s array of *source* values, and the output channel’s array of + * *target* values, populates the slots in *target* specified by *index* with + * the desired mapped output values. */ mapIndex(index: number[], source: S[], target: T[]): void; } -/** How to produce new channel values for each series. */ +/** + * How to produce new channel values for each series; one of: + * + * - a named map implementation such as *cumsum* or *rank* + * - a function to be passed an array of values, returning new values + * - an object that implements the *mapIndex* method + */ export type Map = MapImplementation | MapFunction | MapName; -/** Outputs for the map transform. */ +/** + * Outputs for the map transform. Each declared output channel must have a + * corresponding input channel in *options*. + * + * When **x1** or **x2** is in *outputs*, reads the input channel **x** if + * **x1** or **x2** is not in *options*; likewise for **y1** or **y2**, reads + * the input channel **y** if **y1** or **y2** is not in *options*. + */ export type MapOutputs = {[key in ChannelName]?: Map}; /** Options for the map transform. */ @@ -36,63 +59,56 @@ export interface MapOptions { /** * Groups on the first channel of *z*, *fill*, or *stroke*, if any, and then * applies the specified *map* method to each of the *x*, *x1*, and *x2* - * channels declared in the *options*. The *map* may be specified as: + * channels in the specified *options* to produce new channel values for each + * series. The *map* method may be one of: * - * * *cumsum* - a cumulative sum - * * *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 *mapIndex* method + * - a named map implementation such as *cumsum* or *rank* + * - a function to be passed an array of 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. + * For example, to produce a cumulative sum of random numbers on the *x* + * channel: + * + * ```js + * Plot.mapX("cumsum", {x: d3.randomNormal()}) + * ``` */ export function mapX(map: Map, options?: T & MapOptions): Transformed; /** * Groups on the first channel of *z*, *fill*, or *stroke*, if any, and then * applies the specified map method to each of the *y*, *y1*, and *y2* channels - * declared in the *options*. The *map* may be specified as: + * in the specified *options* to produce new channel values for each series. The + * *map* method may be one of: * - * * *cumsum* - a cumulative sum - * * *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 *mapIndex* method + * - a named map implementation such as *cumsum* or *rank* + * - a function to be passed an array of 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. + * For example, to produce a cumulative sum of random numbers on the *y* + * channel: + * + * ```js + * Plot.mapY("cumsum", {y: d3.randomNormal()}) + * ``` */ export function mapY(map: Map, options?: T & MapOptions): Transformed; /** * Groups on the first channel of *z*, *fill*, or *stroke*, if any, and then for - * each channel declared in the specified *outputs*, applies the corresponding - * *map* method. Each channel in *outputs* must have a corresponding input - * channel in *options*. + * each channel in the specified *outputs*, applies the corresponding *map* + * method to produce new channel values for each series. Each *map* method may + * be one of: + * + * - a named map implementation such as *cumsum* or *rank* + * - a function to be passed an array of values, returning new values + * - an object that implements the *mapIndex* method + * + * For example, to produce a cumulative sum of random numbers on the *y* + * channel: * * ```js * Plot.map({y: "cumsum"}, {y: d3.randomNormal()}) * ``` - * - * Each *map* in *outputs* may be specified as: - * - * * *cumsum* - a cumulative sum - * * *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 *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. */ export function map(outputs?: MapOutputs, options?: T & MapOptions): Transformed; diff --git a/src/transforms/normalize.d.ts b/src/transforms/normalize.d.ts index a5219e22f5..2b9a723232 100644 --- a/src/transforms/normalize.d.ts +++ b/src/transforms/normalize.d.ts @@ -2,6 +2,20 @@ import type {ReducerPercentile} from "../reducer.js"; import type {Transformed} from "./basic.js"; import type {Map} from "./map.js"; +/** + * The built-in normalize basis implementations; one of: + * + * - *first* - the first value, as in an index chart + * - *last* - the last value + * - *min* - the minimum value + * - *max* - the maximum value + * - *mean* - the mean value (average) + * - *median* - the median value + * - *pXX* - the percentile value, where XX is a number in [00,99] + * - *sum* - the sum of values + * - *extent* - the minimum is mapped to zero, and the maximum to one + * - *deviation* - subtract the mean, then divide by the standard deviation + */ export type NormalizeBasisName = | "deviation" | "first" @@ -14,13 +28,23 @@ export type NormalizeBasisName = | "extent" | ReducerPercentile; -export type NormalizeBasisFunction = (index: number[], values: any[]) => number; +/** + * A functional basis implementation: given an array of input channel *values* + * for the current series, returns the corresponding basis number (divisor). + */ +export type NormalizeBasisFunction = (values: T[]) => number; -/** How to normalize series values. */ +/** + * How to normalize series values; one of: + * + * - a named basis method such as *first* or *median* + * - a function that takes an array of series values and returns a basis number + */ export type NormalizeBasis = NormalizeBasisName | NormalizeBasisFunction; /** Options for the normalize transform. */ export interface NormalizeOptions { + /** How to normalize series values; defaults to *first*. */ basis?: NormalizeBasis; } @@ -31,23 +55,9 @@ export interface NormalizeOptions { * if the series values are [*x₀*, *x₁*, *x₂*, …] and the *first* basis is used, * the derived series values would be [*x₀* / *x₀*, *x₁* / *x₀*, *x₂* / *x₀*, …] * as in an index chart. - * - * The **basis** option specifies how to normalize series values. It can be: - * - * * *first* (default) - the first value, as in an index chart - * * *last* - the last value - * * *min* - the minimum value - * * *max* - the maximum value - * * *mean* - the mean value (average) - * * *median* - the median value - * * *pXX* - the percentile value, where XX is a number in [00,99] - * * *sum* - the sum of values - * * *extent* - the minimum is mapped to zero, and the maximum to one - * * *deviation* - subtract the mean, then divide by the standard deviation - * * a function to be passed an array of series values, returning a number */ -export function normalizeX(options?: T & NormalizeOptions): Transformed; export function normalizeX(basis?: NormalizeBasis, options?: T): Transformed; +export function normalizeX(options?: T & NormalizeOptions): Transformed; /** * Groups data into series using the first channel of *z*, *fill*, or *stroke* @@ -56,23 +66,9 @@ export function normalizeX(basis?: NormalizeBasis, options?: T): Transformed< * if the series values are [*y₀*, *y₁*, *y₂*, …] and the *first* basis is used, * the derived series values would be [*y₀* / *y₀*, *y₁* / *y₀*, *y₂* / *y₀*, …] * as in an index chart. - * - * The **basis** option specifies how to normalize series values. It can be: - * - * * *first* (default) - the first value, as in an index chart - * * *last* - the last value - * * *min* - the minimum value - * * *max* - the maximum value - * * *mean* - the mean value (average) - * * *median* - the median value - * * *pXX* - the percentile value, where XX is a number in [00,99] - * * *sum* - the sum of values - * * *extent* - the minimum is mapped to zero, and the maximum to one - * * *deviation* - subtract the mean, then divide by the standard deviation - * * a function to be passed an array of series values, returning a number */ -export function normalizeY(options?: T & NormalizeOptions): Transformed; export function normalizeY(basis?: NormalizeBasis, options?: T): Transformed; +export function normalizeY(options?: T & NormalizeOptions): Transformed; /** * Given a normalize *basis*, returns a corresponding map implementation for use @@ -82,19 +78,5 @@ export function normalizeY(basis?: NormalizeBasis, options?: T): Transformed< * ```js * Plot.map({title: Plot.normalize("first")}, {x: "Date", title: "Close", stroke: "Symbol"}) * ``` - * - * The **basis** option specifies how to normalize series values. It can be: - * - * * *first* (default) - the first value, as in an index chart - * * *last* - the last value - * * *min* - the minimum value - * * *max* - the maximum value - * * *mean* - the mean value (average) - * * *median* - the median value - * * *pXX* - the percentile value, where XX is a number in [00,99] - * * *sum* - the sum of values - * * *extent* - the minimum is mapped to zero, and the maximum to one - * * *deviation* - subtract the mean, then divide by the standard deviation - * * a function to be passed an array of series values, returning a number */ export function normalize(basis: NormalizeBasis): Map; diff --git a/src/transforms/normalize.js b/src/transforms/normalize.js index 0967312325..d5429e9af6 100644 --- a/src/transforms/normalize.js +++ b/src/transforms/normalize.js @@ -57,8 +57,8 @@ function normalizeAccessor(f) { const normalizeExtent = { mapIndex(I, S, T) { - const [s1, s2] = extent(I, (i) => S[i]), - d = s2 - s1; + const [s1, s2] = extent(I, (i) => S[i]); + const d = s2 - s1; for (const i of I) { T[i] = S[i] === null ? NaN : (S[i] - s1) / d; } diff --git a/src/transforms/stack.d.ts b/src/transforms/stack.d.ts index 45b7413e8d..8b0e932852 100644 --- a/src/transforms/stack.d.ts +++ b/src/transforms/stack.d.ts @@ -1,6 +1,19 @@ import type {ChannelValue} from "../channel.js"; import type {Transformed} from "./basic.js"; +/** + * A built-in stack offset method; one of: + * + * - *normalize* - rescale each stack to fill [0, 1] + * - *center* - align the centers of all stacks + * - *wiggle* - translate stacks to minimize apparent movement + * + * If a given stack has zero total value, the *normalize* offset will not adjust + * the stack’s position. Both the *center* and *wiggle* offsets ensure that the + * lowest element across stacks starts at zero for better default axes. The + * *wiggle* offset is recommended for streamgraphs in conjunction with the + * *inside-out* order. For more, see [Byron & Wattenberg](http://leebyron.com/streamgraph/). + */ export type StackOffsetName = | "center" | "normalize" @@ -8,56 +21,69 @@ export type StackOffsetName = | ("expand" & Record) // deprecated; use normalize | ("silhouette" & Record); // deprecated; use center +/** + * A stack offset implementation: given an *index* grouped by facet and *x*, the + * output channel values *y1* and *y2*, and the channel values *z*, mutates the + * values in *y1* and *y2* given by the *index* to translate and scale stacks as + * desired. For the stackX transform, substitute *y* for *x*, and *x1* & *x2* + * for *y1* & *y2*. + */ export type StackOffsetFunction = (index: number[][][], y1: number[], y2: number[], z: any[]) => void; -/** How the baseline of stacked layers may be offset. */ +/** + * How the baseline of stacked layers may be offset; one of: + * + * - a named stack offset method such as *wiggle* or *center* + * - a function to be passed an *index*, *y1*, *y2*, and *z* values + */ export type StackOffset = StackOffsetName | StackOffsetFunction; +/** + * The built-in stack order methods; one of: + * + * - *x* - alias of *value*; for stackX only + * - *y* - alias of *value*; for stackY only + * - *value* - ascending value (or descending with **reverse**) + * - *sum* - total value per series + * - *appearance* - position of maximum value per series + * - *inside-out* (default with *wiggle*) - order the earliest-appearing series on the inside + * + * The *inside-out* order is recommended for streamgraphs in conjunction with + * the *wiggle* offset. For more, see [Byron & Wattenberg](http://leebyron.com/streamgraph/). + */ export type StackOrderName = "value" | "x" | "y" | "z" | "sum" | "appearance" | "inside-out"; -/** How to order layers prior to stacking. */ -export type StackOrder = - | StackOrderName - | (string & Record) // field name; see also https://github.com/microsoft/TypeScript/issues/29729 - | ((d: any, i: number) => any) // function of data - | any[]; // explicit ordinal values +/** + * How to order layers prior to stacking; one of: + * + * - a named stack order method such as *inside-out* or *sum* + * - a field name, for natural order of the corresponding values + * - a function of data, for natural order of the corresponding values + * - an array of explicit *z* values in the desired order + */ +export type StackOrder = StackOrderName | (string & Record) | ((d: any, i: number) => any) | any[]; /** Options for the stack transform. */ export interface StackOptions { /** - * After all values have been stacked from zero, an optional **offset** can be - * applied to translate or scale the stacks: - * - * - null (default) - a zero baseline - * - *normalize* - rescale each stack to fill [0, 1] - * - *center* - align the centers of all stacks - * - *wiggle* - translate stacks to minimize apparent movement - * - a function to be passed a nested index, and start, end, and *z* values - * - * If a given stack has zero total value, the *expand* offset will not adjust - * the stack’s position. Both the *center* and *wiggle* offsets ensure that - * the lowest element across stacks starts at zero for better default axes. - * The *wiggle* offset is recommended for streamgraphs, and if used, changes - * the default **order** to *inside-out*. - * - * For details on the *wiggle* offset, see [Byron & Wattenberg](http://leebyron.com/streamgraph/). + * After stacking, an optional **offset** can be applied to translate and + * scale stacks, say to produce a streamgraph; defaults to null for a zero + * baseline (*y* = 0 for stackY, and *x* = 0 for stackX). If the *wiggle* + * offset is used, the default **order** changes to *inside-out*. */ offset?: StackOffset | null; /** - * The order in which stacks are layered: + * The order in which stacks are layered; one of: * - * - null (default) - input order - * - *value* - ascending value (or descending with **reverse**) - * - *x* - alias of *value*; for stackX only - * - *y* - alias of *value*; for stackY only - * - *sum* - total value per series - * - *appearance* - position of maximum value per series - * - *inside-out* (default with *wiggle*) - order the earliest-appearing series on the inside - * - a named field or function of data - natural order - * - an array enumerating all the *z* values in the desired order + * - null (default) for input order + * - a named stack order method such as *inside-out* or *sum* + * - a field name, for natural order of the corresponding values + * - a function of data, for natural order of the corresponding values + * - an array of explicit *z* values in the desired order * - * For details on the *inside-out* order, see [Byron & Wattenberg](http://leebyron.com/streamgraph/). + * If the *wiggle* **offset** is used, as for a streamgraph, the default + * changes to *inside-out*. */ order?: StackOrder | null; @@ -81,24 +107,24 @@ export interface StackOptions { * the midpoint between *x1* and *x2*, for example to place a label. If not * specified, the input channel *x* defaults to the constant one. */ -export function stackX(options?: T & StackOptions): Transformed; export function stackX(stackOptions?: StackOptions, options?: T): Transformed; +export function stackX(options?: T & StackOptions): Transformed; /** * Like **stackX**, but returns the starting position *x1* as the *x* channel, * for example to position a dot on the left-hand side of each element of a * stack. */ -export function stackX1(options?: T & StackOptions): Transformed; export function stackX1(stackOptions?: StackOptions, options?: T): Transformed; +export function stackX1(options?: T & StackOptions): Transformed; /** * Like **stackX**, but returns the starting position *x2* as the *x* channel, * for example to position a dot on the right-hand side of each element of a * stack. */ -export function stackX2(options?: T & StackOptions): Transformed; export function stackX2(stackOptions?: StackOptions, options?: T): Transformed; +export function stackX2(options?: T & StackOptions): Transformed; /** * Transforms a length channel *y* into starting and ending position channels @@ -109,19 +135,19 @@ export function stackX2(stackOptions?: StackOptions, options?: T): Transforme * *y1* and *y2*, for example to place a label. If not specified, the input * channel *y* defaults to the constant one. */ -export function stackY(options?: T & StackOptions): Transformed; export function stackY(stackOptions?: StackOptions, options?: T): Transformed; +export function stackY(options?: T & StackOptions): Transformed; /** * Like **stackY**, but returns the starting position *y1* as the *y* channel, * for example to position a dot at the bottom of each element of a stack. */ -export function stackY1(options?: T & StackOptions): Transformed; export function stackY1(stackOptions?: StackOptions, options?: T): Transformed; +export function stackY1(options?: T & StackOptions): Transformed; /** * Like **stackY**, but returns the ending position *y2* as the *y* channel, * for example to position a dot at the top of each element of a stack. */ -export function stackY2(options?: T & StackOptions): Transformed; export function stackY2(stackOptions?: StackOptions, options?: T): Transformed; +export function stackY2(options?: T & StackOptions): Transformed; diff --git a/src/transforms/window.d.ts b/src/transforms/window.d.ts index 4cf18610d0..b265a71b1f 100644 --- a/src/transforms/window.d.ts +++ b/src/transforms/window.d.ts @@ -1,60 +1,70 @@ -import type {ReducerPercentile} from "../reducer.js"; +import type {ReducerFunction, ReducerPercentile} from "../reducer.js"; import type {Transformed} from "./basic.js"; import type {Map} from "./map.js"; +/** + * The built-in window reducer implementations; one of: + * + * - *difference* - the difference between the last and first window value + * - *ratio* - the ratio of the last and first window value + * - *first* - the first value + * - *last* - the last value + * - *deviation* - the standard deviation + * - *sum* - the sum of values + * - *min* - the minimum value + * - *max* - the maximum value + * - *mean* - the mean (average) value + * - *median* - the median value + * - *variance* - the variance per [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) + * - *mode* - the mode (most common occurrence) + * - *pXX* - the percentile value, where XX is a number in [00,99] + */ export type WindowReducerName = + | "difference" // specific to window + | "ratio" // specific to window + | "first" + | "last" | "deviation" + | "sum" + | "min" | "max" | "mean" | "median" - | "min" - | "mode" - | "sum" | "variance" - | "difference" - | "ratio" - | "first" - | "last" + | "mode" | ReducerPercentile; -export type WindowReducerFunction = (values: any[]) => any; - -export type WindowReducer = WindowReducerName | WindowReducerFunction; +/** + * How to reduce aggregated (windowed) values; one of: + * + * - a named window reducer implementation such as *mean* or *difference* + * - a function that takes an array of values and returns the reduced value + */ +export type WindowReducer = WindowReducerName | ReducerFunction; +/** Options for the window transform. */ export interface WindowOptions { /** - * The size (number of consecutive values) in the window; includes the current - * value. + * The required size (number of consecutive values) in the window; includes + * the current value. */ k: number; /** * How to produce a summary statistic from the **k** values in the current - * window. The reducer may be specified as: + * window; one of: * - * * *min* - the minimum - * * *max* - the maximum - * * *mean* (default) - the mean (average) - * * *median* - the median - * * *mode* - the mode (most common occurrence) - * * *pXX* - the percentile value, where XX is a number in [00,99] - * * *sum* - the sum of values - * * *deviation* - the standard deviation - * * *variance* - the variance per [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) - * * *difference* - the difference between the last and first window value - * * *ratio* - the ratio of the last and first window value - * * *first* - the first value - * * *last* - the last value - * * a function to be passed an array of **k** values + * - a named window reducer implementation such as *mean* or *difference* + * - a function that takes an array of values and returns the reduced value */ reduce?: WindowReducer; /** * How to align the rolling window, placing the current value: * - * * *start* - as the first element in the window - * * *middle* (default) - in the middle of the window, rounding down if **k** is even - * * *end* - as the last element in the window + * - *start* - as the first element in the window + * - *middle* (default) - in the middle of the window, rounding down if **k** is even + * - *end* - as the last element in the window */ anchor?: "start" | "middle" | "end"; @@ -62,12 +72,6 @@ export interface WindowOptions { shift?: "leading" | "centered" | "trailing"; /** - * If true, the output start values or end values or both (depending on the - * **anchor**) of each series may be undefined since there are not enough - * elements to create a window of size **k**; output values may also be - * undefined if some of the input values in the corresponding window are - * undefined. - * * If false (the default), the window will be automatically truncated as * needed, and undefined input values are ignored. For example, if **k** is 24 * and **anchor** is *middle*, then the initial 11 values have effective @@ -75,48 +79,56 @@ export interface WindowOptions { * effective window sizes of 23, 22, 21, … 12. Values computed with a * truncated window may be noisy; if you would prefer to not show this data, * set the **strict** option to true. + * + * If true, the output start values or end values or both (depending on the + * **anchor**) of each series may be undefined since there are not enough + * elements to create a window of size **k**; output values may also be + * undefined if some of the input values in the corresponding window are + * undefined. */ strict?: boolean; } /** - * Computes a moving window of *x*, *x1*, and *x2* channel values and then - * derives a summary statistic from values in the current window, say to compute - * a rolling average. The window options can be specified as the first argument, - * or grouped with the *options*. For example, the following are equivalent: + * Groups data into series using the first channel of *z*, *fill*, or *stroke* + * (if any), then derives new *x*, *x1*, and *x2* channels by computing a moving + * window of channel values and deriving reduced values from the window. For + * example, to compute a rolling average in *x*: * * ```js * Plot.windowX(24, {x: "Anomaly", y: "Date"}); - * Plot.windowX({k: 24, reduce: "mean", x: "Anomaly", y: "Date"}); - * Plot.windowX({k: 24, reduce: "mean"}, {x: "Anomaly", y: "Date"}); * ``` + * + * If *windowOptions* is a number, it is shorthand for the window size **k**. */ -export function windowX(options?: T & WindowOptions): Transformed; export function windowX(windowOptions?: WindowOptions | number, options?: T): Transformed; +export function windowX(options?: T & WindowOptions): Transformed; /** - * Computes a moving window of *y*, *y1*, and *y2* channel values around and - * then derives a summary statistic from values in the current window, say to - * compute a rolling average. The window options can be specified as the first - * argument, or grouped with the *options*. For example, the following are - * equivalent: + * Groups data into series using the first channel of *z*, *fill*, or *stroke* + * (if any), then derives new *y*, *y1*, and *y2* channels by computing a moving + * window of channel values and deriving reduced values from the window. For + * example, to compute a rolling average in *y*: * * ```js * Plot.windowY(24, {x: "Date", y: "Anomaly"}); - * Plot.windowY({k: 24, reduce: "mean", x: "Date", y: "Anomaly"}); - * Plot.windowY({k: 24, reduce: "mean"}, {x: "Date", y: "Anomaly"}); * ``` + * + * If *windowOptions* is a number, it is shorthand for the window size **k**. */ -export function windowY(options?: T & WindowOptions): Transformed; export function windowY(windowOptions?: WindowOptions | number, options?: T): Transformed; +export function windowY(options?: T & WindowOptions): Transformed; /** - * Returns a window map method suitable for use with Plot.map. The options are - * the window size *k*, or an object with properties *k*, *anchor*, *reduce*, or - * *strict*. + * Given the specified window *options*, returns a corresponding map + * implementation for use with the map transform, allowing the window transform + * to be applied to arbitrary channels instead of only *x* and *y*. For example, + * to compute a rolling average for the *title* channel: * * ```js - * Plot.map({y: Plot.window(24)}, {x: "Date", y: "Close", stroke: "Symbol"}) + * Plot.map({title: Plot.window(24)}, {x: "Date", title: "Anomaly"}) * ``` + * + * If *options* is a number, it is shorthand for the window size **k**. */ export function window(options?: WindowOptions | number): Map; diff --git a/test/plots/function-contour.ts b/test/plots/function-contour.ts index 99b8ad53e5..9206e2eb37 100644 --- a/test/plots/function-contour.ts +++ b/test/plots/function-contour.ts @@ -1,4 +1,5 @@ import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; export async function functionContour() { return Plot.plot({ @@ -12,7 +13,8 @@ export async function functionContour() { x1: 0, y1: 0, x2: 4 * Math.PI, - y2: 4 * Math.PI * (350 / 580) + y2: 4 * Math.PI * (350 / 580), + thresholds: d3.ticks(-80, 50, 10) // testing explicit thresholds }) ] });