Skip to content

Commit 5eeed54

Browse files
mbostockFil
authored andcommitted
fix facet channel sort with transform (observablehq#1316)
* fix facet channel sort with transform * marginLeft—less minimalist, but more readable * rule y = 0 --------- Co-authored-by: Philippe Rivière <[email protected]>
1 parent fb24d61 commit 5eeed54

File tree

4 files changed

+294
-3
lines changed

4 files changed

+294
-3
lines changed

src/channel.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,14 @@ export function inferChannelScale(name, channel) {
7171
// Note: mutates channel.domain! This is set to a function so that it is lazily
7272
// computed; i.e., if the scale’s domain is set explicitly, that takes priority
7373
// over the sort option, and we don’t need to do additional work.
74-
export function channelDomain(channels, facetChannels, data, options) {
74+
export function channelDomain(data, facets, channels, facetChannels, options) {
7575
const {reverse: defaultReverse, reduce: defaultReduce = true, limit: defaultLimit} = options;
7676
for (const x in options) {
7777
if (!registry.has(x)) continue; // ignore unknown scale keys (including generic options)
7878
let {value: y, reverse = defaultReverse, reduce = defaultReduce, limit = defaultLimit} = maybeValue(options[x]);
7979
if (reverse === undefined) reverse = y === "width" || y === "height"; // default to descending for lengths
8080
if (reduce == null || reduce === false) continue; // disabled reducer
81-
const X = findScaleChannel(channels, x) || (facetChannels && findScaleChannel(facetChannels, x));
81+
const X = x === "fx" || x === "fy" ? reindexFacetChannel(facets, facetChannels[x]) : findScaleChannel(channels, x);
8282
if (!X) throw new Error(`missing channel for scale: ${x}`);
8383
const XV = X.value;
8484
const [lo = 0, hi = Infinity] = isIterable(limit) ? limit : limit < 0 ? [limit] : [0, limit];
@@ -120,6 +120,21 @@ function findScaleChannel(channels, scale) {
120120
}
121121
}
122122

123+
// Facet channels are not affected by transforms; so, to compute the domain of a
124+
// facet scale, we must first re-index the facet channel according to the
125+
// transformed mark index. Note: mutates channel, but that should be safe here?
126+
function reindexFacetChannel(facets, channel) {
127+
const originalFacets = facets.original;
128+
if (originalFacets === facets) return channel; // not transformed
129+
const V1 = channel.value;
130+
const V2 = (channel.value = []); // mutates channel!
131+
for (let i = 0; i < originalFacets.length; ++i) {
132+
const vi = V1[originalFacets[i][0]];
133+
for (const j of facets[i]) V2[j] = vi;
134+
}
135+
return channel;
136+
}
137+
123138
function difference(channels, k1, k2) {
124139
const X1 = values(channels, k1);
125140
const X2 = values(channels, k2);

src/mark.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,11 @@ export class Mark {
7878
initialize(facets, facetChannels) {
7979
let data = arrayify(this.data);
8080
if (facets === undefined && data != null) facets = [range(data)];
81+
const originalFacets = facets;
8182
if (this.transform != null) ({facets, data} = this.transform(data, facets)), (data = arrayify(data));
83+
if (facets !== undefined) facets.original = originalFacets; // needed up read facetChannels
8284
const channels = Channels(this.channels, data);
83-
if (this.sort != null) channelDomain(channels, facetChannels, data, this.sort); // mutates facetChannels!
85+
if (this.sort != null) channelDomain(data, facets, channels, facetChannels, this.sort); // mutates facetChannels!
8486
return {data, facets, channels};
8587
}
8688
filter(index, channels, values) {

test/output/athletesSortFacet.svg

Lines changed: 262 additions & 0 deletions
Loading

test/plots/athletes-sort.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
import * as Plot from "@observablehq/plot";
22
import * as d3 from "d3";
33

4+
export async function athletesSortFacet() {
5+
const athletes = await d3.csv("data/athletes.csv", d3.autoType);
6+
const female = (d) => d.sex === "female";
7+
return Plot.plot({
8+
marginLeft: 100,
9+
marks: [
10+
Plot.barX(athletes, Plot.groupZ({x: "mean"}, {x: female, fy: "sport", sort: {fy: "x"}})),
11+
Plot.frame({anchor: "left", facet: "super"})
12+
]
13+
});
14+
}
15+
416
export async function athletesSortNullLimit() {
517
const athletes = await d3.csv("data/athletes.csv", d3.autoType);
618
return Plot.plot({

0 commit comments

Comments
 (0)