From 3b9da1a46a8a258f74680a582cefca416022f3d6 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 19 May 2023 10:36:03 -0700 Subject: [PATCH 1/5] descending shorthand --- docs/features/facets.md | 4 +- docs/features/scales.md | 4 +- docs/marks/bar.md | 2 +- docs/transforms/group.md | 2 +- docs/transforms/sort.md | 4 +- src/channel.d.ts | 5 +- src/channel.js | 11 +++ src/mark.d.ts | 2 +- src/marks/dot.js | 4 +- src/transforms/basic.d.ts | 2 +- src/transforms/basic.js | 12 +++ src/transforms/dodge.js | 2 +- src/transforms/stack.js | 2 +- test/output/channelDomainMinus.svg | 104 +++++++++++++++++++++ test/output/channelDomainMinusReverse.svg | 104 +++++++++++++++++++++ test/plots/athletes-nationality.ts | 11 +++ test/plots/ballot-status-race.ts | 3 +- test/plots/channel-domain.ts | 8 ++ test/plots/d3-survey-2015.ts | 2 +- test/plots/fruit-sales.ts | 10 +- test/plots/industry-unemployment-track.ts | 2 +- test/plots/learning-poverty.ts | 2 +- test/plots/metro-unemployment-ridgeline.ts | 2 +- test/plots/movies-profit-by-genre.ts | 2 +- test/plots/movies-rating-by-genre.ts | 5 +- test/plots/penguin-annotated.ts | 5 +- test/plots/us-population-state-age-dots.ts | 2 +- test/plots/us-population-state-age.ts | 2 +- 28 files changed, 280 insertions(+), 40 deletions(-) create mode 100644 test/output/channelDomainMinus.svg create mode 100644 test/output/channelDomainMinusReverse.svg create mode 100644 test/plots/athletes-nationality.ts diff --git a/docs/features/facets.md b/docs/features/facets.md index 31fd211820..ff8fd962c4 100644 --- a/docs/features/facets.md +++ b/docs/features/facets.md @@ -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"} }) ] }) @@ -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"} })) ] }) diff --git a/docs/features/scales.md b/docs/features/scales.md index 52845cf646..75b82b2e7a 100644 --- a/docs/features/scales.md +++ b/docs/features/scales.md @@ -977,10 +977,10 @@ Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "y", reduce: "max"}} 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, use the *reverse* option, or *-name* when referring to a channel: ```js -Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "y", reverse: true}}) +Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "-y"}}) ``` 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.) diff --git a/docs/marks/bar.md b/docs/marks/bar.md index 4b0397f73c..b00b722f92 100644 --- a/docs/marks/bar.md +++ b/docs/marks/bar.md @@ -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() ``` ::: diff --git a/docs/transforms/group.md b/docs/transforms/group.md index dd11175ceb..df85aea289 100644 --- a/docs/transforms/group.md +++ b/docs/transforms/group.md @@ -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]) ] }) diff --git a/docs/transforms/sort.md b/docs/transforms/sort.md index d015f0ff28..02032dd4fc 100644 --- a/docs/transforms/sort.md +++ b/docs/transforms/sort.md @@ -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 })) ] }) @@ -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. diff --git a/src/channel.d.ts b/src/channel.d.ts index 8694b40f81..5ffad5b4a7 100644 --- a/src/channel.d.ts +++ b/src/channel.d.ts @@ -162,6 +162,9 @@ export type ChannelValueBinSpec = ChannelValue | ({value: ChannelValue} & BinOpt */ export type ChannelValueDenseBinSpec = ChannelValue | ({value: ChannelValue; scale?: Channel["scale"]} & Omit); // 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: @@ -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 { diff --git a/src/channel.js b/src/channel.js index 8889995368..14864a5334 100644 --- a/src/channel.js +++ b/src/channel.js @@ -82,7 +82,10 @@ 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 + const negate = y?.startsWith("-"); + if (negate) y = y.slice(1); order = order === undefined ? y === "width" || y === "height" ? descendingGroup : ascendingGroup : maybeOrder(order); // prettier-ignore + if (negate) order = reverseOrder(order); // TODO flip reverse if order is null? 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}`); @@ -174,6 +177,14 @@ function descendingGroup([ak, av], [bk, bv]) { return descendingDefined(av, bv) || ascendingDefined(ak, bk); } +function reverseOrder(order) { + return order === ascendingGroup + ? descendingGroup + : order === descendingGroup + ? ascendingGroup + : (i, j) => order(j, i); +} + export function getSource(channels, key) { let channel = channels[key]; if (!channel) return; diff --git a/src/mark.d.ts b/src/mark.d.ts index 6061501e78..21921fdd79 100644 --- a/src/mark.d.ts +++ b/src/mark.d.ts @@ -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: diff --git a/src/marks/dot.js b/src/marks/dot.js index 1080e48f68..2d969444c6 100644 --- a/src/marks/dot.js +++ b/src/marks/dot.js @@ -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 { diff --git a/src/transforms/basic.d.ts b/src/transforms/basic.d.ts index cfc7c988b2..fddc507801 100644 --- a/src/transforms/basic.d.ts +++ b/src/transforms/basic.d.ts @@ -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 diff --git a/src/transforms/basic.js b/src/transforms/basic.js index 3b4fd032a5..ab6e90ddfe 100644 --- a/src/transforms/basic.js +++ b/src/transforms/basic.js @@ -121,6 +121,10 @@ function sortValue(value) { throw new Error(`invalid order: ${order}`); } } + if (channel?.startsWith("-")) { + channel = channel.slice(1); + order = reverseOrder(order); + } return (data, facets, channels) => { let V; if (channel === undefined) { @@ -135,3 +139,11 @@ function sortValue(value) { return {data, facets: facets.map((I) => I.slice().sort(compareValue))}; }; } + +function reverseOrder(order) { + return order === ascendingDefined + ? descendingDefined + : order === descendingDefined + ? ascendingDefined + : (i, j) => order(j, i); +} diff --git a/src/transforms/dodge.js b/src/transforms/dodge.js index 3f9b6a267a..23c20bd807 100644 --- a/src/transforms/dodge.js +++ b/src/transforms/dodge.js @@ -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; diff --git a/src/transforms/stack.js b/src/transforms/stack.js index 1550d20e81..cad3675c54 100644 --- a/src/transforms/stack.js +++ b/src/transforms/stack.js @@ -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])); diff --git a/test/output/channelDomainMinus.svg b/test/output/channelDomainMinus.svg new file mode 100644 index 0000000000..aeba256354 --- /dev/null +++ b/test/output/channelDomainMinus.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + USA + BRA + GER + AUS + FRA + CHN + GBR + JPN + CAN + ESP + ITA + RUS + NED + POL + ARG + KOR + NZL + UKR + SWE + COL + + + nationality + + + + + + + + + + + 0 + 100 + 200 + 300 + 400 + 500 + + + Frequency → + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/channelDomainMinusReverse.svg b/test/output/channelDomainMinusReverse.svg new file mode 100644 index 0000000000..8ed811bdc9 --- /dev/null +++ b/test/output/channelDomainMinusReverse.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + TUV + SWZ + SOM + NRU + MTN + LBR + GEQ + DMA + CHA + BHU + YEM + TLS + STP + SSD + SOL + MON + LIE + KIR + BRU + BIZ + + + nationality + + + + + + + + + + + 0 + 100 + 200 + 300 + 400 + 500 + + + Frequency → + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/athletes-nationality.ts b/test/plots/athletes-nationality.ts new file mode 100644 index 0000000000..a3da9e658f --- /dev/null +++ b/test/plots/athletes-nationality.ts @@ -0,0 +1,11 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export async function athletesNationality() { + const athletes = await d3.csv("data/athletes.csv", d3.autoType); + return Plot.plot({ + x: {grid: true}, + y: {label: null}, + marks: [Plot.barX(athletes, Plot.groupY({x: "count"}, {y: "nationality", sort: {y: "-x", limit: 20}}))] + }); +} diff --git a/test/plots/ballot-status-race.ts b/test/plots/ballot-status-race.ts index d546871fc0..8a5b314c27 100644 --- a/test/plots/ballot-status-race.ts +++ b/test/plots/ballot-status-race.ts @@ -81,8 +81,7 @@ export async function ballotStatusRace() { title: (d) => `${d.percent.toFixed(1)}%`, sort: { fy: "data", - reduce: (data) => data.find((d) => d.status === "ACCEPTED").percent, - reverse: true + reduce: (data) => -data.find((d) => d.status === "ACCEPTED").percent } }), Plot.ruleX([0]) diff --git a/test/plots/channel-domain.ts b/test/plots/channel-domain.ts index 31c4ec16d7..2904dfb89c 100644 --- a/test/plots/channel-domain.ts +++ b/test/plots/channel-domain.ts @@ -30,6 +30,14 @@ export async function channelDomainDescendingReverse() { return countNationality({y: "x", order: "descending", reverse: true, limit: 20}); } +export async function channelDomainMinus() { + return countNationality({y: "-x", limit: 20}); +} + +export async function channelDomainMinusReverse() { + return countNationality({y: "-x", reverse: true, limit: 20}); +} + export async function channelDomainComparator() { return countNationality({y: "x", order: ([, a], [, b]) => d3.descending(a, b), limit: 20}); } diff --git a/test/plots/d3-survey-2015.ts b/test/plots/d3-survey-2015.ts index 170d6c61a4..3525910c6c 100644 --- a/test/plots/d3-survey-2015.ts +++ b/test/plots/d3-survey-2015.ts @@ -43,7 +43,7 @@ function bars(groups, title) { y: ([key]) => key, fill: "steelblue", insetTop: 1, - sort: {y: "x", order: "descending"} + sort: {y: "-x"} }), Plot.ruleX([0]) ] diff --git a/test/plots/fruit-sales.ts b/test/plots/fruit-sales.ts index 0c3b0dd085..f79235ab2a 100644 --- a/test/plots/fruit-sales.ts +++ b/test/plots/fruit-sales.ts @@ -5,13 +5,7 @@ export async function fruitSales() { const sales = await d3.csv("data/fruit-sales.csv", d3.autoType); return Plot.plot({ marginLeft: 50, - y: { - label: null, - reverse: true - }, - marks: [ - Plot.barX(sales, Plot.groupY({x: "sum"}, {x: "units", y: "fruit", sort: {y: "x", reverse: true}})), - Plot.ruleX([0]) - ] + y: {label: null, reverse: true}, + marks: [Plot.barX(sales, Plot.groupY({x: "sum"}, {x: "units", y: "fruit", sort: {y: "-x"}})), Plot.ruleX([0])] }); } diff --git a/test/plots/industry-unemployment-track.ts b/test/plots/industry-unemployment-track.ts index 6221f60832..0baa29c288 100644 --- a/test/plots/industry-unemployment-track.ts +++ b/test/plots/industry-unemployment-track.ts @@ -12,7 +12,7 @@ export async function industryUnemploymentTrack() { interval: "month", fill: "unemployed", title: "unemployed", - sort: {fy: "fill", reverse: true}, + sort: {fy: "-fill"}, inset: 0 }), Plot.dotX( diff --git a/test/plots/learning-poverty.ts b/test/plots/learning-poverty.ts index 9d72ceab17..d3714f6ba7 100644 --- a/test/plots/learning-poverty.ts +++ b/test/plots/learning-poverty.ts @@ -30,7 +30,7 @@ export async function learningPoverty() { x: (d) => (d.type === "ok" ? -1 : 1) * d.share, // diverging bars y: "Country Name", fill: "type", - sort: {y: "x", order: "descending"} + sort: {y: "-x"} }), Plot.ruleX([0]) ] diff --git a/test/plots/metro-unemployment-ridgeline.ts b/test/plots/metro-unemployment-ridgeline.ts index 4aa85a3b7b..4eef1335be 100644 --- a/test/plots/metro-unemployment-ridgeline.ts +++ b/test/plots/metro-unemployment-ridgeline.ts @@ -21,7 +21,7 @@ export async function metroUnemploymentRidgeline() { }, marks: [ Plot.areaY(data, {x: "date", y: "unemployment", fill: "#eee"}), - Plot.line(data, {x: "date", y: "unemployment", sort: {fy: "y", order: "descending"}}), + Plot.line(data, {x: "date", y: "unemployment", sort: {fy: "-y"}}), Plot.ruleY([0]) ] }); diff --git a/test/plots/movies-profit-by-genre.ts b/test/plots/movies-profit-by-genre.ts index f407be6c3f..f403f9973f 100644 --- a/test/plots/movies-profit-by-genre.ts +++ b/test/plots/movies-profit-by-genre.ts @@ -40,7 +40,7 @@ export async function moviesProfitByGenre() { x: Profit, stroke: "red", strokeWidth: 2, - sort: {y: "x", reverse: true} + sort: {y: "-x"} } ) ) diff --git a/test/plots/movies-rating-by-genre.ts b/test/plots/movies-rating-by-genre.ts index 12040d2d89..4709bb5cb6 100644 --- a/test/plots/movies-rating-by-genre.ts +++ b/test/plots/movies-rating-by-genre.ts @@ -35,9 +35,8 @@ export async function moviesRatingByGenre() { stroke: "Major Genre", r: 2.5, sort: { - fy: "x", - reduce: "median", - order: "descending" + fy: "-x", + reduce: "median" } } ) diff --git a/test/plots/penguin-annotated.ts b/test/plots/penguin-annotated.ts index 3fa24ba587..2b06a872e6 100644 --- a/test/plots/penguin-annotated.ts +++ b/test/plots/penguin-annotated.ts @@ -8,10 +8,7 @@ export async function penguinAnnotated() { x: {insetRight: 10}, marks: [ Plot.frame(), - Plot.barX( - penguins, - Plot.groupY({x: "count"}, {y: "species", fill: "sex", title: "sex", sort: {y: "x", reverse: true}}) - ), + Plot.barX(penguins, Plot.groupY({x: "count"}, {y: "species", fill: "sex", title: "sex", sort: {y: "-x"}})), Plot.text(["Count of penguins\ngrouped by species\n and colored by sex"], { frameAnchor: "bottom-right", dx: -3, diff --git a/test/plots/us-population-state-age-dots.ts b/test/plots/us-population-state-age-dots.ts index 978ce5d365..4ee923d675 100644 --- a/test/plots/us-population-state-age-dots.ts +++ b/test/plots/us-population-state-age-dots.ts @@ -32,7 +32,7 @@ export async function usPopulationStateAgeDots() { textAnchor: "end", dx: -6, text: "state", - sort: {y: "x", reduce: "min", reverse: true} + sort: {y: "-x", reduce: "min"} }) ) ] diff --git a/test/plots/us-population-state-age.ts b/test/plots/us-population-state-age.ts index 501b0a12a9..d9a3b91c5d 100644 --- a/test/plots/us-population-state-age.ts +++ b/test/plots/us-population-state-age.ts @@ -49,7 +49,7 @@ export async function usPopulationStateAgeGrouped() { y: "population", fill: "age", title: "age", - sort: {fx: "y", reverse: true, limit: 6} + sort: {fx: "-y", limit: 6} }), Plot.ruleY([0]) ] From 00338c803787acaa065bd9611fc66abd1e509c49 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 19 May 2023 10:42:12 -0700 Subject: [PATCH 2/5] no double reverse --- test/plots/fruit-sales.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/plots/fruit-sales.ts b/test/plots/fruit-sales.ts index f79235ab2a..482ac61b5c 100644 --- a/test/plots/fruit-sales.ts +++ b/test/plots/fruit-sales.ts @@ -5,7 +5,7 @@ export async function fruitSales() { const sales = await d3.csv("data/fruit-sales.csv", d3.autoType); return Plot.plot({ marginLeft: 50, - y: {label: null, reverse: true}, - marks: [Plot.barX(sales, Plot.groupY({x: "sum"}, {x: "units", y: "fruit", sort: {y: "-x"}})), Plot.ruleX([0])] + y: {label: null}, + marks: [Plot.barX(sales, Plot.groupY({x: "sum"}, {x: "units", y: "fruit", sort: {y: "x"}})), Plot.ruleX([0])] }); } From 5742c3b29b84a4ae527470076ce99434e06b6926 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 22 May 2023 10:20:25 -0700 Subject: [PATCH 3/5] minus shorthand only changes default order --- docs/features/scales.md | 22 ++++++++++++++++++---- docs/features/shorthand.md | 2 +- src/channel.js | 11 +---------- src/transforms/basic.js | 17 ++++------------- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/docs/features/scales.md b/docs/features/scales.md index 75b82b2e7a..59a80ebb66 100644 --- a/docs/features/scales.md +++ b/docs/features/scales.md @@ -969,7 +969,7 @@ 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. Lastly the primary values are by default 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: ```js Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "y", reduce: "max"}}) @@ -977,13 +977,27 @@ Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "y", reduce: "max"}} 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, or *-name* when referring to a channel: +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"}}) ``` -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.) +Setting **order** to null will disable sorting, preserving the order of the mark’s data. (When an aggregating transform is used, such as [group](../transforms/group.md) or [bin](../transforms/bin.md), note that the mark’s data may already have been sorted and thus may the order may differ from the input data.) + +Alternatively, set the **reverse** option to true. This produces a different result than descending order if the domain contains nulls 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.) ```js Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "y", limit: 5}}) @@ -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. diff --git a/docs/features/shorthand.md b/docs/features/shorthand.md index 98a1cf44e2..539640ca5d 100644 --- a/docs/features/shorthand.md +++ b/docs/features/shorthand.md @@ -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 diff --git a/src/channel.js b/src/channel.js index 14864a5334..38edcc3e2a 100644 --- a/src/channel.js +++ b/src/channel.js @@ -84,8 +84,7 @@ export function channelDomain(data, facets, channels, facetChannels, options) { let {value: y, order = defaultOrder, reverse = defaultReverse, reduce = defaultReduce, limit = defaultLimit} = maybeValue(options[x]); // prettier-ignore const negate = y?.startsWith("-"); if (negate) y = y.slice(1); - order = order === undefined ? y === "width" || y === "height" ? descendingGroup : ascendingGroup : maybeOrder(order); // prettier-ignore - if (negate) order = reverseOrder(order); // TODO flip reverse if order is null? + 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}`); @@ -177,14 +176,6 @@ function descendingGroup([ak, av], [bk, bv]) { return descendingDefined(av, bv) || ascendingDefined(ak, bk); } -function reverseOrder(order) { - return order === ascendingGroup - ? descendingGroup - : order === descendingGroup - ? ascendingGroup - : (i, j) => order(j, i); -} - export function getSource(channels, key) { let channel = channels[key]; if (!channel) return; diff --git a/src/transforms/basic.js b/src/transforms/basic.js index ab6e90ddfe..573032fa47 100644 --- a/src/transforms/basic.js +++ b/src/transforms/basic.js @@ -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": @@ -121,10 +124,6 @@ function sortValue(value) { throw new Error(`invalid order: ${order}`); } } - if (channel?.startsWith("-")) { - channel = channel.slice(1); - order = reverseOrder(order); - } return (data, facets, channels) => { let V; if (channel === undefined) { @@ -139,11 +138,3 @@ function sortValue(value) { return {data, facets: facets.map((I) => I.slice().sort(compareValue))}; }; } - -function reverseOrder(order) { - return order === ascendingDefined - ? descendingDefined - : order === descendingDefined - ? ascendingDefined - : (i, j) => order(j, i); -} From c6623aa026aeed4b6a9206e979a2d7e16ee60a0e Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 22 May 2023 10:28:44 -0700 Subject: [PATCH 4/5] edits --- docs/features/scales.md | 10 +++++----- test/plots/athletes-nationality.ts | 11 ----------- test/plots/ballot-status-race.ts | 3 ++- 3 files changed, 7 insertions(+), 17 deletions(-) delete mode 100644 test/plots/athletes-nationality.ts diff --git a/docs/features/scales.md b/docs/features/scales.md index 59a80ebb66..6e9f703aa3 100644 --- a/docs/features/scales.md +++ b/docs/features/scales.md @@ -969,10 +969,10 @@ 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 by default 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. @@ -989,15 +989,15 @@ Alternatively, the *-channel* shorthand option, which changes the default **orde Plot.barY(alphabet, {x: "letter", y: "frequency", sort: {x: "-y"}}) ``` -Setting **order** to null will disable sorting, preserving the order of the mark’s data. (When an aggregating transform is used, such as [group](../transforms/group.md) or [bin](../transforms/bin.md), note that the mark’s data may already have been sorted and thus may the order may differ from the input data.) +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 may the order may differ from the input data.) -Alternatively, set the **reverse** option to true. This produces a different result than descending order if the domain contains nulls or unorderable values: descending order puts nulls last, whereas reversed ascending order puts nulls first. +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}}) diff --git a/test/plots/athletes-nationality.ts b/test/plots/athletes-nationality.ts deleted file mode 100644 index a3da9e658f..0000000000 --- a/test/plots/athletes-nationality.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as Plot from "@observablehq/plot"; -import * as d3 from "d3"; - -export async function athletesNationality() { - const athletes = await d3.csv("data/athletes.csv", d3.autoType); - return Plot.plot({ - x: {grid: true}, - y: {label: null}, - marks: [Plot.barX(athletes, Plot.groupY({x: "count"}, {y: "nationality", sort: {y: "-x", limit: 20}}))] - }); -} diff --git a/test/plots/ballot-status-race.ts b/test/plots/ballot-status-race.ts index 8a5b314c27..689dd22e25 100644 --- a/test/plots/ballot-status-race.ts +++ b/test/plots/ballot-status-race.ts @@ -81,7 +81,8 @@ export async function ballotStatusRace() { title: (d) => `${d.percent.toFixed(1)}%`, sort: { fy: "data", - reduce: (data) => -data.find((d) => d.status === "ACCEPTED").percent + reduce: (data) => data.find((d) => d.status === "ACCEPTED").percent, + order: "descending" } }), Plot.ruleX([0]) From ddd70c7bec47e36852a84a07a44b6b450e80c9d8 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 22 May 2023 15:01:57 -0400 Subject: [PATCH 5/5] Update docs/features/scales.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Philippe Rivière --- docs/features/scales.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/scales.md b/docs/features/scales.md index 6e9f703aa3..3f366fa39a 100644 --- a/docs/features/scales.md +++ b/docs/features/scales.md @@ -989,7 +989,7 @@ Alternatively, the *-channel* shorthand option, which changes the default **orde 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 may the order may differ from the input data.) +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.