diff --git a/src/channel.d.ts b/src/channel.d.ts
index 1b10f889ec..8694b40f81 100644
--- a/src/channel.d.ts
+++ b/src/channel.d.ts
@@ -1,6 +1,7 @@
import type {Interval} from "./interval.js";
import type {Reducer} from "./reducer.js";
import type {ScaleName, ScaleType} from "./scales.js";
+import type {CompareFunction} from "./transforms/basic.js";
import type {BinOptions} from "./transforms/bin.js";
/** Lazily-constructed channel values derived from data. */
@@ -192,7 +193,10 @@ export interface ChannelDomainOptions {
*/
reduce?: Reducer | boolean | null;
- /** If true, use descending instead of ascending order. */
+ /** How to order reduced values. */
+ order?: CompareFunction | "ascending" | "descending" | null;
+
+ /** If true, reverse the order after sorting. */
reverse?: boolean;
/**
diff --git a/src/channel.js b/src/channel.js
index 100688908f..8889995368 100644
--- a/src/channel.js
+++ b/src/channel.js
@@ -1,4 +1,4 @@
-import {InternSet, rollup, sort} from "d3";
+import {InternSet, rollups} from "d3";
import {ascendingDefined, descendingDefined} from "./defined.js";
import {first, isColor, isEvery, isIterable, isOpacity, labelof, map, maybeValue, range, valueof} from "./options.js";
import {registry} from "./scales/index.js";
@@ -78,11 +78,11 @@ export function inferChannelScale(name, channel) {
// computed; i.e., if the scale’s domain is set explicitly, that takes priority
// over the sort option, and we don’t need to do additional work.
export function channelDomain(data, facets, channels, facetChannels, options) {
- const {reverse: defaultReverse, reduce: defaultReduce = true, limit: defaultLimit} = options;
+ const {order: defaultOrder, reverse: defaultReverse, reduce: defaultReduce = true, limit: defaultLimit} = options;
for (const x in options) {
if (!registry.has(x)) continue; // ignore unknown scale keys (including generic options)
- let {value: y, reverse = defaultReverse, reduce = defaultReduce, limit = defaultLimit} = maybeValue(options[x]);
- if (reverse === undefined) reverse = y === "width" || y === "height"; // default to descending for lengths
+ 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
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}`);
@@ -106,12 +106,13 @@ export function channelDomain(data, facets, channels, facetChannels, options) {
: values(channels, y, y === "y" ? "y2" : y === "x" ? "x2" : undefined);
const reducer = maybeReduce(reduce === true ? "max" : reduce, YV);
X.domain = () => {
- let domain = rollup(
+ let domain = rollups(
range(XV),
(I) => reducer.reduceIndex(I, YV),
(i) => XV[i]
);
- domain = sort(domain, reverse ? descendingGroup : ascendingGroup);
+ if (order) domain.sort(order);
+ if (reverse) domain.reverse();
if (lo !== 0 || hi !== Infinity) domain = domain.slice(lo, hi);
return domain.map(first);
};
@@ -154,6 +155,17 @@ function values(channels, name, alias) {
throw new Error(`missing channel: ${name}`);
}
+function maybeOrder(order) {
+ if (order == null || typeof order === "function") return order;
+ switch (`${order}`.toLowerCase()) {
+ case "ascending":
+ return ascendingGroup;
+ case "descending":
+ return descendingGroup;
+ }
+ throw new Error(`invalid order: ${order}`);
+}
+
function ascendingGroup([ak, av], [bk, bv]) {
return ascendingDefined(av, bv) || ascendingDefined(ak, bk);
}
diff --git a/test/output/channelDomainAscending.svg b/test/output/channelDomainAscending.svg
new file mode 100644
index 0000000000..246008a690
--- /dev/null
+++ b/test/output/channelDomainAscending.svg
@@ -0,0 +1,104 @@
+
\ No newline at end of file
diff --git a/test/output/channelDomainAscendingReverse.svg b/test/output/channelDomainAscendingReverse.svg
new file mode 100644
index 0000000000..39d7e4999b
--- /dev/null
+++ b/test/output/channelDomainAscendingReverse.svg
@@ -0,0 +1,104 @@
+
\ No newline at end of file
diff --git a/test/output/athletesNationality.svg b/test/output/channelDomainComparator.svg
similarity index 91%
rename from test/output/athletesNationality.svg
rename to test/output/channelDomainComparator.svg
index c75e3a4220..aeba256354 100644
--- a/test/output/athletesNationality.svg
+++ b/test/output/channelDomainComparator.svg
@@ -57,13 +57,8 @@
SWE
COL
-
-
-
-
-
-
-
+
+ nationality
diff --git a/test/output/channelDomainComparatorReverse.svg b/test/output/channelDomainComparatorReverse.svg
new file mode 100644
index 0000000000..39d7e4999b
--- /dev/null
+++ b/test/output/channelDomainComparatorReverse.svg
@@ -0,0 +1,104 @@
+
\ No newline at end of file
diff --git a/test/output/channelDomainDefault.svg b/test/output/channelDomainDefault.svg
new file mode 100644
index 0000000000..246008a690
--- /dev/null
+++ b/test/output/channelDomainDefault.svg
@@ -0,0 +1,104 @@
+
\ No newline at end of file
diff --git a/test/output/channelDomainDefaultReverse.svg b/test/output/channelDomainDefaultReverse.svg
new file mode 100644
index 0000000000..39d7e4999b
--- /dev/null
+++ b/test/output/channelDomainDefaultReverse.svg
@@ -0,0 +1,104 @@
+
\ No newline at end of file
diff --git a/test/output/channelDomainDescending.svg b/test/output/channelDomainDescending.svg
new file mode 100644
index 0000000000..aeba256354
--- /dev/null
+++ b/test/output/channelDomainDescending.svg
@@ -0,0 +1,104 @@
+
\ No newline at end of file
diff --git a/test/output/channelDomainDescendingReverse.svg b/test/output/channelDomainDescendingReverse.svg
new file mode 100644
index 0000000000..8ed811bdc9
--- /dev/null
+++ b/test/output/channelDomainDescendingReverse.svg
@@ -0,0 +1,104 @@
+
\ No newline at end of file
diff --git a/test/output/channelDomainNull.svg b/test/output/channelDomainNull.svg
new file mode 100644
index 0000000000..cac4f22fb2
--- /dev/null
+++ b/test/output/channelDomainNull.svg
@@ -0,0 +1,104 @@
+
\ No newline at end of file
diff --git a/test/output/channelDomainNullReverse.svg b/test/output/channelDomainNullReverse.svg
new file mode 100644
index 0000000000..5a29c5b1cf
--- /dev/null
+++ b/test/output/channelDomainNullReverse.svg
@@ -0,0 +1,104 @@
+
\ No newline at end of file
diff --git a/test/output/channelDomainReduceCount.svg b/test/output/channelDomainReduceCount.svg
new file mode 100644
index 0000000000..a66b64bd3f
--- /dev/null
+++ b/test/output/channelDomainReduceCount.svg
@@ -0,0 +1,6249 @@
+
\ No newline at end of file
diff --git a/test/output/channelDomainReduceDefault.svg b/test/output/channelDomainReduceDefault.svg
new file mode 100644
index 0000000000..d77099e68f
--- /dev/null
+++ b/test/output/channelDomainReduceDefault.svg
@@ -0,0 +1,2608 @@
+
\ No newline at end of file
diff --git a/test/plots/athletes-nationality.ts b/test/plots/athletes-nationality.ts
deleted file mode 100644
index b5ea9a40fd..0000000000
--- a/test/plots/athletes-nationality.ts
+++ /dev/null
@@ -1,17 +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", reverse: true, limit: 20}}))
- ]
- });
-}
diff --git a/test/plots/channel-domain.ts b/test/plots/channel-domain.ts
new file mode 100644
index 0000000000..31c4ec16d7
--- /dev/null
+++ b/test/plots/channel-domain.ts
@@ -0,0 +1,71 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+async function countNationality(sort: Plot.ChannelDomainSort) {
+ const athletes = await d3.csv("data/athletes.csv", d3.autoType);
+ return Plot.barX(athletes, Plot.groupY({x: "count"}, {y: "nationality", sort})).plot();
+}
+
+export async function channelDomainDefault() {
+ return countNationality({y: "x", limit: 20});
+}
+
+export async function channelDomainDefaultReverse() {
+ return countNationality({y: "x", reverse: true, limit: 20});
+}
+
+export async function channelDomainAscending() {
+ return countNationality({y: "x", order: "ascending", limit: 20});
+}
+
+export async function channelDomainAscendingReverse() {
+ return countNationality({y: "x", order: "ascending", reverse: true, limit: 20});
+}
+
+export async function channelDomainDescending() {
+ return countNationality({y: "x", order: "descending", limit: 20});
+}
+
+export async function channelDomainDescendingReverse() {
+ return countNationality({y: "x", order: "descending", reverse: true, limit: 20});
+}
+
+export async function channelDomainComparator() {
+ return countNationality({y: "x", order: ([, a], [, b]) => d3.descending(a, b), limit: 20});
+}
+
+export async function channelDomainComparatorReverse() {
+ return countNationality({y: "x", order: ([, a], [, b]) => d3.ascending(a, b), reverse: true, limit: 20});
+}
+
+// This test avoids the group transform because the group transform always sorts
+// groups in natural ascending order by key. (Perhaps there should be an option
+// to disable that behavior?)
+async function groupNationality(sort: Plot.ChannelDomainSort) {
+ const athletes = await d3.csv("data/athletes.csv", d3.autoType);
+ const nationalities = d3.groups(athletes, (d) => d.nationality);
+ const count = Object.assign(([, D]) => D.length, {label: "Frequency"});
+ const key = Object.assign(([d]) => d, {label: "nationality"});
+ return Plot.barX(nationalities, {x: count, y: key, sort}).plot();
+}
+
+export async function channelDomainNull() {
+ return groupNationality({y: "x", order: null, limit: 20});
+}
+
+export async function channelDomainNullReverse() {
+ return groupNationality({y: "x", order: null, reverse: true, limit: 20});
+}
+
+async function weightNationality(sort: Plot.ChannelDomainSort) {
+ const athletes = await d3.csv("data/athletes.csv", d3.autoType);
+ return Plot.tickX(athletes, {x: "weight", y: "nationality", sort}).plot();
+}
+
+export async function channelDomainReduceCount() {
+ return weightNationality({y: "x", reduce: "count", order: "descending", limit: 20});
+}
+
+export async function channelDomainReduceDefault() {
+ return weightNationality({y: "x", order: "descending", limit: 20}); // reduce: "max"
+}
diff --git a/test/plots/d3-survey-2015.ts b/test/plots/d3-survey-2015.ts
index 1abb8a3bc1..170d6c61a4 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", reverse: true}
+ sort: {y: "x", order: "descending"}
}),
Plot.ruleX([0])
]
diff --git a/test/plots/index.ts b/test/plots/index.ts
index baa80ae0ff..e44bbd19fa 100644
--- a/test/plots/index.ts
+++ b/test/plots/index.ts
@@ -18,7 +18,6 @@ export * from "./athletes-height-weight-bin.js";
export * from "./athletes-height-weight-sex.js";
export * from "./athletes-height-weight-sport.js";
export * from "./athletes-height-weight.js";
-export * from "./athletes-nationality.js";
export * from "./athletes-sample.js";
export * from "./athletes-sex-weight.js";
export * from "./athletes-sort.js";
@@ -46,6 +45,7 @@ export * from "./cars-hexbin.js";
export * from "./cars-jitter.js";
export * from "./cars-mpg.js";
export * from "./cars-parcoords.js";
+export * from "./channel-domain.js";
export * from "./clamp.js";
export * from "./collapsed-histogram.js";
export * from "./country-centroids.js";
@@ -198,10 +198,10 @@ export * from "./penguin-quantile-unknown.js";
export * from "./penguin-sex-mass-culmen-species.js";
export * from "./penguin-sex.js";
export * from "./penguin-size-symbols.js";
-export * from "./penguin-species.js";
export * from "./penguin-species-island-relative.js";
export * from "./penguin-species-island-sex.js";
export * from "./penguin-species-island.js";
+export * from "./penguin-species.js";
export * from "./penguin-voronoi-1d.js";
export * from "./polylinear.js";
export * from "./population-by-latitude.js";
diff --git a/test/plots/learning-poverty.ts b/test/plots/learning-poverty.ts
index 266e3bd8f3..9d72ceab17 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", reverse: true}
+ sort: {y: "x", order: "descending"}
}),
Plot.ruleX([0])
]
diff --git a/test/plots/metro-unemployment-ridgeline.ts b/test/plots/metro-unemployment-ridgeline.ts
index cbeea3191c..4aa85a3b7b 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", reverse: true}}),
+ Plot.line(data, {x: "date", y: "unemployment", sort: {fy: "y", order: "descending"}}),
Plot.ruleY([0])
]
});
diff --git a/test/plots/movies-rating-by-genre.ts b/test/plots/movies-rating-by-genre.ts
index 97f0256413..12040d2d89 100644
--- a/test/plots/movies-rating-by-genre.ts
+++ b/test/plots/movies-rating-by-genre.ts
@@ -37,7 +37,7 @@ export async function moviesRatingByGenre() {
sort: {
fy: "x",
reduce: "median",
- reverse: true
+ order: "descending"
}
}
)