Skip to content

sort aliases #770

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 2 commits into from
Feb 20, 2022
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ If an ordinal scale’s domain is not set, it defaults to natural ascending orde
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) (such as [stack](#stack) or [bin](#bin)). For marks that implicitly stack ([area](#area), [bar](#bar), and [rect](#rect)), the stacked dimension is aliased: when stacking on *x*, *x* is an alias for *x2*, and when stacking on *y*, *y* is an alias for *y2*.
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) (such as [stack](#stack) or [bin](#bin)). When sorting on the *x*, if no such channel is defined, the *x2* channel will be used instead if available, and similarly for *y* and *y2*; this is useful for marks that implicitly stack such as [area](#area), [bar](#bar), and [rect](#rect). 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:

Expand Down
28 changes: 19 additions & 9 deletions src/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export function channelSort(channels, facetChannels, data, options) {
const {reverse: defaultReverse, reduce: defaultReduce = true, limit: defaultLimit} = options;
for (const x in options) {
if (!registry.has(x)) continue; // ignore unknown scale keys
const {value: y, reverse = defaultReverse, reduce = defaultReduce, limit = defaultLimit} = maybeValue(options[x]);
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
if (reduce == null || reduce === false) continue; // disabled reducer
const X = channels.find(([, {scale}]) => scale === x) || facetChannels && facetChannels.find(([, {scale}]) => scale === x);
if (!X) throw new Error(`missing channel for scale: ${x}`);
Expand All @@ -33,14 +34,10 @@ export function channelSort(channels, facetChannels, data, options) {
return domain;
};
} else {
let YV;
if (y === "data") {
YV = data;
} else {
const Y = channels.find(([name]) => name === y);
if (!Y) throw new Error(`missing channel: ${y}`);
YV = Y[1].value;
}
const YV = y === "data" ? data
: y === "height" ? difference(channels, "y1", "y2")
: y === "width" ? difference(channels, "x1", "x2")
: values(channels, y, y === "y" ? "y2" : y === "x" ? "x2" : undefined);
const reducer = maybeReduce(reduce === true ? "max" : reduce, YV);
X[1].domain = () => {
let domain = rollup(range(XV), I => reducer.reduce(I, YV), i => XV[i]);
Expand All @@ -52,6 +49,19 @@ export function channelSort(channels, facetChannels, data, options) {
}
}

function difference(channels, k1, k2) {
const X1 = values(channels, k1);
const X2 = values(channels, k2);
return Float64Array.from(X2, (x2, i) => Math.abs(x2 - X1[i]));
}

function values(channels, name, alias) {
let channel = channels.find(([n]) => n === name);
if (!channel && alias !== undefined) channel = channels.find(([n]) => n === alias);
if (channel) return channel[1].value;
throw new Error(`missing channel: ${name}`);
}

function ascendingGroup([ak, av], [bk, bv]) {
return ascending(av, bv) || ascending(ak, bk);
}
Expand Down
14 changes: 1 addition & 13 deletions src/transforms/stack.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {InternMap, cumsum, group, groupSort, greatest, max, min, rollup, sum} from "d3";
import {ascendingDefined} from "../defined.js";
import {field, lazyChannel, maybeLazyChannel, maybeZ, mid, range, valueof, maybeZero, isOptions, maybeValue} from "../options.js";
import {field, lazyChannel, maybeLazyChannel, maybeZ, mid, range, valueof, maybeZero} from "../options.js";
import {basic} from "./basic.js";

export function stackX(stackOptions = {}, options = {}) {
Expand Down Expand Up @@ -46,29 +46,17 @@ export function stackY2(stackOptions = {}, options = {}) {
}

export function maybeStackX({x, x1, x2, ...options} = {}) {
options = aliasSort(options, "x");
if (x1 === undefined && x2 === undefined) return stackX({x, ...options});
([x1, x2] = maybeZero(x, x1, x2));
return {...options, x1, x2};
}

export function maybeStackY({y, y1, y2, ...options} = {}) {
options = aliasSort(options, "y");
if (y1 === undefined && y2 === undefined) return stackY({y, ...options});
([y1, y2] = maybeZero(y, y1, y2));
return {...options, y1, y2};
}

function aliasSort(options, name) {
let {sort} = options;
if (!isOptions(sort)) return options;
for (const x in sort) {
const {value: y, ...rest} = maybeValue(sort[x]);
if (y === name) sort = {...sort, [x]: {value: `${y}2`, ...rest}};
}
return {...options, sort};
}

// The reverse option is ambiguous: it is both a stack option and a basic
// transform. If only one options object is specified, we interpret it as a
// stack option, and therefore must remove it from the propagated options.
Expand Down
4 changes: 4 additions & 0 deletions test/data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ https://github.com/hemanrobinson/preattentive/blob/a58dd4795d0ee063a38a2d7bf3381
ggplot2 “diamonds” dataset (carat and price columns only)
https://github.com/tidyverse/ggplot2/blob/master/data-raw/diamonds.csv

## energy-production.csv
U.S. Energy Information Administration; monthly energy review, primary energy production by source, Jan. 2022
https://www.eia.gov/totalenergy/data/monthly/index.php

## gistemp.csv
NASA Goddard Institute for Space Studies
https://data.giss.nasa.gov/gistemp/
Expand Down
Loading