Skip to content

descending shorthand #1591

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

Merged
merged 5 commits into from
May 22, 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
4 changes: 2 additions & 2 deletions docs/features/facets.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Plot.plot({
y: "variety",
fy: "site",
stroke: "year",
sort: {y: "x", fy: "x", reduce: "median", reverse: true}
sort: {y: "-x", fy: "-x", reduce: "median"}
})
]
})
Expand Down Expand Up @@ -81,7 +81,7 @@ Plot.plot({
fy: "site",
stroke: "yield",
strokeWidth: 2,
sort: {y: "x1", fy: "x1", reduce: "median", reverse: true}
sort: {y: "-x1", fy: "-x1", reduce: "median"}
}))
]
})
Expand Down
24 changes: 19 additions & 5 deletions docs/features/scales.md
Original file line number Diff line number Diff line change
Expand Up @@ -969,21 +969,35 @@ Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "y"}})

The sort option is an object whose keys are ordinal scale names, such as *x* or *fx*, and whose values are mark channel names, such as **y**, **y1**, or **y2**. By specifying an existing channel rather than a new value, you avoid repeating the order definition and can refer to channels derived by [transforms](./transforms.md) (such as [stack](../transforms/stack.md) or [bin](../transforms/bin.md)). When sorting the *x* domain, if no **x** channel is defined, **x2** will be used instead if available, and similarly for *y* and **y2**; this is useful for marks that implicitly stack such as [area](../marks/area.md), [bar](../marks/bar.md), and [rect](../marks/rect.md). A sort value may also be specified as *width* or *height*, representing derived channels |*x2* - *x1*| and |*y2* - *y1*| respectively.

Note that there may be multiple associated values in the secondary dimension for a given value in the primary ordinal dimension. The secondary values are therefore grouped for each associated primary value, and each group is then aggregated by applying a reducer. Lastly the primary values are sorted based on the associated reduced value in natural ascending order to produce the domain. The default reducer is *max*, but may be changed by specifying the *reduce* option. The above code is shorthand for:
Note that there may be multiple associated values in the secondary dimension for a given value in the primary ordinal dimension. The secondary values are therefore grouped for each associated primary value, and each group is then aggregated by applying a reducer. The default reducer is *max*, but may be changed by specifying the **reduce** option. Lastly the primary values are by default sorted based on the associated reduced value in natural ascending order to produce the domain. The above code is shorthand for:

```js
Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "y", reduce: "max"}})
Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "y", reduce: "max", order: "ascending"}})
```

Generally speaking, a reducer only needs to be specified when there are multiple secondary values for a given primary value. See the [group transform](../transforms/group.md) for the list of supported reducers.

For descending rather than ascending order, use the *reverse* option:
For descending rather than ascending order, set the **order** option to *descending*:

```js
Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "y", order: "descending"}})
```

Alternatively, the *-channel* shorthand option, which changes the default **order** to *descending*:

```js
Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "-y"}})
```

Setting **order** to null will disable sorting, preserving the order of the data. (When an aggregating transform is used, such as [group](../transforms/group.md) or [bin](../transforms/bin.md), note that the data may already have been sorted and thus the order may differ from the input data.)

Alternatively, set the **reverse** option to true. This produces a different result than descending order for null or unorderable values: descending order puts nulls last, whereas reversed ascending order puts nulls first.

```js
Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "y", reverse: true}})
```

An additional *limit* option truncates the domain to the first *n* values after sorting. If *limit* is negative, the last *n* values are used instead. Hence, a positive *limit* with *reverse* = true will return the top *n* values in descending order. If *limit* is an array [*lo*, *hi*], the *i*th values with *lo* ≤ *i* < *hi* will be selected. (Note that like the [basic filter transform](../transforms/filter.md), limiting the *x* domain here does not affect the computation of the *y* domain, which is computed independently without respect to filtering.)
An additional **limit** option truncates the domain to the first *n* values after ordering. If **limit** is negative, the last *n* values are used instead. Hence, a positive **limit** with **reverse** = true will return the top *n* values in descending order. If **limit** is an array [*lo*, *hi*], the *i*th values with *lo* ≤ *i* < *hi* will be selected. (Note that like the [basic filter transform](../transforms/filter.md), limiting the *x* domain here does not affect the computation of the *y* domain, which is computed independently without respect to filtering.)

```js
Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "y", limit: 5}})
Expand All @@ -992,7 +1006,7 @@ Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "y", limit: 5}})
If different sort options are needed for different ordinal scales, the channel name can be replaced with a *value* object with additional per-scale options.

```js
Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: {value: "y", reverse: true}}})
Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: {value: "y", order: "descending"}}})
```

If the input channel is *data*, then the reducer is passed groups of the mark’s data; this is typically used in conjunction with a custom reducer function, as when the built-in single-channel reducers are insufficient.
Expand Down
2 changes: 1 addition & 1 deletion docs/features/shorthand.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ Plot.tickX(numbers).plot()
```
:::

We could even use [Plot.vectorX](../marks/vector.md) here to draw little up-pointing arrows. (Typically the vector mark is used in conjunction with the *rotate* and *length* options to control the direction and magnitude of each vector.)
We could even use [Plot.vectorX](../marks/vector.md) here to draw little up-pointing arrows. (Typically the vector mark is used in conjunction with the **rotate** and **length** options to control the direction and magnitude of each vector.)

:::plot https://observablehq.com/@observablehq/plot-shorthand-one-dimensional-vector
```js
Expand Down
2 changes: 1 addition & 1 deletion docs/marks/bar.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Ordinal domains are sorted naturally (alphabetically) by default. Either set the

:::plot https://observablehq.com/@observablehq/plot-vertical-bars
```js
Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "y", reverse: true}}).plot()
Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "-y"}}).plot()
```
:::

Expand Down
2 changes: 1 addition & 1 deletion docs/transforms/group.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Plot.plot({
x: {label: null, tickRotate: 90},
y: {grid: true},
marks: [
Plot.barY(olympians, Plot.groupX({y: "count"}, {x: "sport", sort: {x: "y", reverse: true}})),
Plot.barY(olympians, Plot.groupX({y: "count"}, {x: "sport", sort: {x: "-y"}})),
Plot.ruleY([0])
]
})
Expand Down
4 changes: 2 additions & 2 deletions docs/transforms/sort.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Plot.plot({
fill: "currentColor",
stroke: "var(--vp-c-bg)",
strokeWidth: 1,
sort: sorted ? {channel: "r", order: "descending"} : null
sort: sorted ? {channel: "-r"} : null
}))
]
})
Expand Down Expand Up @@ -134,7 +134,7 @@ Sorts the data by the specified *order*, which is one of:
- a field name
- a {*channel*, *order*} object

In the object case, the **channel** option specifies the name of the channel, while the **order** option specifies *ascending* (the default) or *descending* order. For example, `sort: {channel: "r", order: "descending"}` will sort by descending radius (**r**).
In the object case, the **channel** option specifies the name of the channel, while the **order** option specifies *ascending* (the default) or *descending* order. You can also use the shorthand *-name* to sort by descending order of the channel with the given *name*. For example, `sort: {channel: "-r"}` will sort by descending radius (**r**).

In the function case, if the sort function does not take exactly one argument, it is interpreted as a comparator function; otherwise it is interpreted as an accessor function.

Expand Down
5 changes: 4 additions & 1 deletion src/channel.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ export type ChannelValueBinSpec = ChannelValue | ({value: ChannelValue} & BinOpt
*/
export type ChannelValueDenseBinSpec = ChannelValue | ({value: ChannelValue; scale?: Channel["scale"]} & Omit<BinOptions, "interval">); // prettier-ignore

/** A channel name, or an implied one for domain sorting. */
type ChannelDomainName = ChannelName | "data" | "width" | "height";

/**
* The available inputs for imputing scale domains. In addition to a named
* channel, an input may be specified as:
Expand All @@ -177,7 +180,7 @@ export type ChannelValueDenseBinSpec = ChannelValue | ({value: ChannelValue; sca
* custom **reduce** function, as when the built-in single-channel reducers are
* insufficient.
*/
export type ChannelDomainValue = ChannelName | "data" | "width" | "height" | null;
export type ChannelDomainValue = ChannelDomainName | `-${ChannelDomainName}` | null;

/** Options for imputing scale domains from channel values. */
export interface ChannelDomainOptions {
Expand Down
4 changes: 3 additions & 1 deletion src/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ export function channelDomain(data, facets, channels, facetChannels, options) {
for (const x in options) {
if (!registry.has(x)) continue; // ignore unknown scale keys (including generic options)
let {value: y, order = defaultOrder, reverse = defaultReverse, reduce = defaultReduce, limit = defaultLimit} = maybeValue(options[x]); // prettier-ignore
order = order === undefined ? y === "width" || y === "height" ? descendingGroup : ascendingGroup : maybeOrder(order); // prettier-ignore
const negate = y?.startsWith("-");
if (negate) y = y.slice(1);
order = order === undefined ? negate !== (y === "width" || y === "height") ? descendingGroup : ascendingGroup : maybeOrder(order); // prettier-ignore
if (reduce == null || reduce === false) continue; // disabled reducer
const X = x === "fx" || x === "fy" ? reindexFacetChannel(facets, facetChannels[x]) : findScaleChannel(channels, x);
if (!X) throw new Error(`missing channel for scale: ${x}`);
Expand Down
2 changes: 1 addition & 1 deletion src/mark.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export interface MarkOptions {
* with a *value* object and per-scale options:
*
* ```js
* sort: {y: {value: "x", reverse: true}}
* sort: {y: {value: "-x"}}
* ```
*
* When sorting the mark’s index, the **sort** option is instead one of:
Expand Down
4 changes: 1 addition & 3 deletions src/marks/dot.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ const defaults = {
};

export function withDefaultSort(options) {
return options.sort === undefined && options.reverse === undefined
? sort({channel: "r", order: "descending"}, options)
: options;
return options.sort === undefined && options.reverse === undefined ? sort({channel: "-r"}, options) : options;
}

export class Dot extends Mark {
Expand Down
2 changes: 1 addition & 1 deletion src/transforms/basic.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export type SortOrder =
| CompareFunction
| ChannelValue
| {value?: ChannelValue; order?: CompareFunction | "ascending" | "descending"}
| {channel?: ChannelName; order?: CompareFunction | "ascending" | "descending"};
| {channel?: ChannelName | `-${ChannelName}`; order?: CompareFunction | "ascending" | "descending"};

/**
* Applies a transform to *options* to sort the mark’s index by the specified
Expand Down
5 changes: 4 additions & 1 deletion src/transforms/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ function sortData(compare) {

function sortValue(value) {
let channel, order;
({channel, value, order = ascendingDefined} = {...maybeValue(value)});
({channel, value, order} = {...maybeValue(value)});
const negate = channel?.startsWith("-");
if (negate) channel = channel.slice(1);
if (order === undefined) order = negate ? descendingDefined : ascendingDefined;
if (typeof order !== "function") {
switch (`${order}`.toLowerCase()) {
case "ascending":
Expand Down
2 changes: 1 addition & 1 deletion src/transforms/dodge.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function dodge(y, x, anchor, padding, r, options) {
let {channels, sort, reverse} = options;
channels = maybeNamed(channels);
if (channels?.r === undefined) options = {...options, channels: {...channels, r: {value: r, scale: "r"}}};
if (sort === undefined && reverse === undefined) options.sort = {channel: "r", order: "descending"};
if (sort === undefined && reverse === undefined) options.sort = {channel: "-r"};
}
return initializer(options, function (data, facets, channels, scales, dimensions, context) {
let {[x]: X, r: R} = channels;
Expand Down
2 changes: 1 addition & 1 deletion src/transforms/stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function stack(x, y = one, kx, ky, {offset, order, reverse}, options) {
const [Y2, setY2] = column(y);
Y1.hint = Y2.hint = lengthy;
offset = maybeOffset(offset);
order = maybeOrder(order, offset, ky);
order = maybeOrder(order, offset, ky); // TODO shorthand -order with reverse?
return [
basic(options, (data, facets, plotOptions) => {
const X = x == null ? undefined : setX(maybeApplyInterval(valueof(data, x), plotOptions?.[kx]));
Expand Down
104 changes: 104 additions & 0 deletions test/output/channelDomainMinus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading