Custom facet sorting (row/column with sort: {field, op} or a sort array) silently degrades to encounter order in one remaining configuration: a crossed facet (both row and column) where the child view has an independent discrete scale sized by range steps.
This is the last known gap after #9818, which fixed crossed-facet custom sort in the general case (#5937, #8675, vega/altair#3481, vega/altair#3588). None of those issues hit this path; filing this so it isn't rediscovered later as "the facet sort bug is back".
Example
{
"data": {"url": "data/cars.json"},
"facet": {
"row": {"field": "Origin", "sort": {"field": "Horsepower", "op": "median"}},
"column": {"field": "Cylinders"}
},
"spec": {
"mark": "bar",
"encoding": {
"x": {"field": "Year", "type": "ordinal"},
"y": {"aggregate": "count"}
}
},
"resolve": {"scale": {"x": "independent"}}
}
(Triggering configuration: crossed facet + custom sort + independent discrete child scale without a fixed size, so layout needs range-step sizing.)
Root cause
When the child has an independent discrete scale with range steps, facet layout needs per-row/column cardinalities, so assembleRowColumnHeaderData sources row_domain/column_domain from the crossed dataset (grouped by row × column) instead of the per-channel aggregates. That crossed aggregate doesn't carry sort.field, so sort metadata is null and both headers and cells (consistently, post-#9818) fall back to encounter order.
Adding the sort field to the crossed aggregate is not sufficient: a row sort by median(x) must be computed grouped by row only; re-aggregating per-cell medians gives op-of-op semantics, which is wrong for non-decomposable ops.
Proposed direction
Keep the crossed dataset for cardinalities, but join sort metadata in from separate row-only/column-only aggregates; i.e., extend the lookup machinery introduced in #9818 to this data path.
Custom facet sorting (
row/columnwithsort: {field, op}or a sort array) silently degrades to encounter order in one remaining configuration: a crossed facet (bothrowandcolumn) where the child view has an independent discrete scale sized by range steps.This is the last known gap after #9818, which fixed crossed-facet custom sort in the general case (#5937, #8675, vega/altair#3481, vega/altair#3588). None of those issues hit this path; filing this so it isn't rediscovered later as "the facet sort bug is back".
Example
{ "data": {"url": "data/cars.json"}, "facet": { "row": {"field": "Origin", "sort": {"field": "Horsepower", "op": "median"}}, "column": {"field": "Cylinders"} }, "spec": { "mark": "bar", "encoding": { "x": {"field": "Year", "type": "ordinal"}, "y": {"aggregate": "count"} } }, "resolve": {"scale": {"x": "independent"}} }(Triggering configuration: crossed facet + custom sort + independent discrete child scale without a fixed size, so layout needs range-step sizing.)
Root cause
When the child has an independent discrete scale with range steps, facet layout needs per-row/column cardinalities, so assembleRowColumnHeaderData sources row_domain/column_domain from the crossed dataset (grouped by row × column) instead of the per-channel aggregates. That crossed aggregate doesn't carry sort.field, so sort metadata is null and both headers and cells (consistently, post-#9818) fall back to encounter order.
Adding the sort field to the crossed aggregate is not sufficient: a row sort by median(x) must be computed grouped by row only; re-aggregating per-cell medians gives op-of-op semantics, which is wrong for non-decomposable ops.
Proposed direction
Keep the crossed dataset for cardinalities, but join sort metadata in from separate row-only/column-only aggregates; i.e., extend the lookup machinery introduced in #9818 to this data path.