Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

document basic transforms, initializers, valueof… #1369

Merged
merged 4 commits into from
Mar 26, 2023
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
37 changes: 36 additions & 1 deletion src/options.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,43 @@
import type {ChannelTransform, ChannelValue} from "./channel.js";
import type {Data} from "./mark.js";

export function valueof(data: Data | null, value: ChannelValue | null, type?: any): any[] | null;
/** Array, Float32Array, etc. */
type ArrayishConstructor = (new (...args: any) => any) & {from: (data: Data) => Iterable<any> & ArrayLike<any>};

/**
* Given some *data* and a channel *value* definition (such as a field name or
* function accessor), returns an array of the specified *type* containing the
* corresponding values derived from *data*. If *type* is not specified, it
* defaults to Array; otherwise it must be an Array or TypedArray subclass.
*
* The returned array is not guaranteed to be new; when the *value* is a channel
* transform or an array that is an instance of the given *type*, the array may
* be returned as-is without making a copy.
*/
export function valueof(data: Data | null, value: ChannelValue | null, type?: ArrayConstructor): any[] | null;
export function valueof<T extends ArrayishConstructor>(data: Data | null, value: ChannelValue | null, type: T): InstanceType<T> | null; // prettier-ignore

/**
* Returns a [*column*, *setColumn*] helper for deriving columns; *column* is a
* channel transform that returns whatever value was most recently passed to
* *setColumn*. If *setColumn* is not called, then the channel transform returns
* undefined.
*
* If a *source* is specified, then *column*.label exposes the given *source*’s
* label, if any: if *source* is a string as when representing a named field of
* data, then *column*.label is *source*; otherwise *column*.label propagates
* *source*.label. This allows derived columns to propagate a human-readable
* axis or legend label.
*/
export function column(source?: any): [ChannelTransform, <T>(value: T) => T];

/**
* A channel transform that returns the data as-is, avoiding an extra copy when
* defining a channel as being equal to the data. For example, to re-use the
* given *data* for the **fill** channel:
*
* ```js
* Plot.raster(data, {width: 300, height: 200, fill: Plot.identity})
* ```
*/
export const identity: ChannelTransform;
135 changes: 124 additions & 11 deletions src/transforms/basic.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,39 @@ import type {Context} from "../context.js";
import type {Dimensions} from "../dimensions.js";
import type {ScaleFunctions} from "../scales.js";

/**
* A mark transform function is passed the mark’s *data* and a nested index into
* the data, *facets*. The transform function returns new mark data and facets;
* the returned **data** defaults to the passed *data*, and the returned
* **facets** defaults to the passed *facets*. The mark is the *this* context.
* Transform functions can also trigger side-effects, say to populate
* lazily-derived columns; see also Plot.column.
*/
export type TransformFunction = (data: any[], facets: number[][]) => {data?: any[]; facets?: number[][]};

/**
* A mark initializer function is passed the mark’s (possibly transformed)
* *data*, a nested index into the data, *facets*, and the mark’s initialized
* *channels*, along with the plot’s *scales*, *dimensions*, and *context*. The
* initializer function returns new mark data, facets, and channels; the
* returned **data** defaults to the passed *data*, the returned **facets**
* defaults to the passed *facets*, and the returned **channels** are merged
* with the passed channels, replacing channels of the same name. The mark
* itself is the *this* context.
*
* Whereas a mark transform operates in abstract data space on channel values
* prior to scale application, a mark initializer runs after the (initial)
* scales are constructed and hence can operate in screen space, such as pixel
* coordinates and colors. For example, an initializer can adjust a mark’s
* positions to avoid occlusion.
*
* If any of the returned derived channels are bound to scales, the associated
* scales will be re-initialized. To avoid a circular dependency, mark
* initializer functions cannot re-initialize position scales (*x*, *y*, *fx*,
* and *fy*). If an initializer desires a channel not supported by the
* downstream mark, additional channels can be declared using the mark
* **channels** option.
*/
export type InitializerFunction = (
data: any[],
facets: number[][],
Expand All @@ -18,30 +49,112 @@ export type InitializerFunction = (
channels?: Channels;
};

export type FilterFunction = (d: any, i: number) => boolean;

/**
* Compares the two values *a* and *b*, returning a negative number if *a* is
* considered less than *b*, a positive number if *a* is considered greater than
* *b*, or zero if *a* and *b* are considered equal.
*/
export type CompareFunction = (a: any, b: any) => number;

/** Mark options with a mark transform. */
export type Transformed<T> = T & {transform: TransformFunction};

/** Mark options with a mark initializer. */
export type Initialized<T> = T & {initializer: InitializerFunction};

/**
* Given an *options* object that may specify some basic transforms (**filter**,
* **sort**, or **reverse**) or a custom **transform**, composes those
* transforms with the given *transform* function, returning a new *options*
* object.
*
* If a custom **transform** is present on the given *options*, any basic
* transforms are ignored. Any additional input *options* are passed through in
* the returned *options* object. This method facilitates applying basic
* transforms prior to applying the given *transform* and is used internally by
* Plot’s built-in transforms.
*
* The given *transform* runs after the existing transforms in *options*. Throws
* an error if the given *options* define an **initializer**, since mark
* transforms must run before mark initializers.
*/
export function transform<T>(options: T, transform: TransformFunction): Transformed<T>;

/**
* Given an *options* object that may specify some basic initializers
* (**filter**, **sort**, or **reverse**) or a custom **initializer**, composes
* those initializers with the given *initializer* function, returning a new
* *options* object.
*
* If a custom **initializer** is present on the given *options*, any basic
* initializers are ignored. Any additional input *options* are passed through
* in the returned *options* object. This method facilitates applying basic
* initializers prior to applying the given *initializer* and is used internally
* by Plot’s built-in initializers.
*
* If the given *initializer* does not need to operate in screen space (after
* scale application), it should instead be implemented as a mark transform for
* simplicity; see Plot.transform.
*/
export function initializer<T>(options: T, initializer: InitializerFunction): Initialized<T>;

export function filter<T>(test: FilterFunction, options?: T): Transformed<T>;
/**
* Applies a transform to *options* to filter the mark’s index according to the
* given *test*, which can be a function (receiving the datum *d* and index *i*)
* or a channel value definition such as a field name; only truthy values are
* retained in the index. For example, to show only data whose body mass is
* greater than 3,000g:
*
* ```js
* Plot.filter((d) => d.body_mass_g > 3000, options)
* ```
*
* Note that filtering only affects the rendered mark index, not the associated
* channel values, and thus has no effect on imputed scale domains.
*/
export function filter<T>(test: ChannelValue, options?: T): Transformed<T>;

/**
* Applies a transform to *options* to reverse the order of the mark’s index,
* say for reverse input order.
*/
export function reverse<T>(options?: T): Transformed<T>;

export function shuffle<T>(options?: T): Transformed<T>;

export interface SortOrderOptions {
channel?: ChannelName;
value?: ChannelValue;
order?: CompareFunction | "ascending" | "descending";
}
/**
* Applies a transform to *options* to randomly shuffles the mark’s index. If a
* **seed** is specified, a linear congruential generator with the given seed is
* used to generate random numbers deterministically; otherwise, Math.random is
* used.
*/
export function shuffle<T>(options?: T & {seed?: number}): Transformed<T>;

export type SortOrder = CompareFunction | ChannelValue | SortOrderOptions;
/**
* How to order values; one of:
*
* - a function for comparing data, returning a signed number
* - a channel value definition for sorting given values in ascending order
* - a {value, order} object for sorting given values
* - a {channel, order} object for sorting the named channel’s values
*/
export type SortOrder =
| CompareFunction
| ChannelValue
| {value?: ChannelValue; order?: CompareFunction | "ascending" | "descending"}
| {channel?: ChannelName; order?: CompareFunction | "ascending" | "descending"};

/**
* Applies a transform to *options* to sort the mark’s index by the specified
* *order*. The *order* is one of:
*
* - a function for comparing data, returning a signed number
* - a channel value definition for sorting given values in ascending order
* - a {value, order} object for sorting given values
* - a {channel, order} object for sorting the named channel’s values
*
* For example, to render marks in order of ascending body mass:
*
* ```js
* Plot.sort("body_mass_g", options)
* ```
*/
export function sort<T>(order: SortOrder, options?: T): Transformed<T>;