Facet custom sort

I’m trying to have some sorted facets based on a interval variable containing the next values:

["0 - 1.3",  "1.3 - 5.04", "5.04 - 7.7", "7.7 - 10.2", "10.2 - 14.5"]

I want to override alphabetical sort that considers that ‘10.2 - 14.5’ < ‘5.04 - 7.7’

Here is the complete dataset Sun hours study / Beatriz Martínez | Observable

Thanks!

For example, you could sort the domain of the y facets like so:

Plot.plot({
  marginLeft: 100,
  fy: { domain: d3.sort(new Set(Plot.valueof(seasonsrdata, "sun_interval")), parseFloat) },
  marks: [Plot.dotX(seasonsrdata, { fy: "sun_interval", x: "sun" })]
})

I understand now, facet settings act as axis settings.

I also tried to use another numerical variable to sort the facets (i.e. sun_min) and plot custom tick labels based on the intervals. It didn´t work.

How can I get sth like this working?

Plot.plot({
  marginLeft: 100,
   y: { ticks: ["a", "b", "c", "d", "e"] },
  marks: [Plot.dotX(seasonsrdata, { fy: "sun_min", x: "sun" })]
})

Thanks!

There’s no y scale in the code you posted, so setting y options will not have any effect.

Are you trying to order the fy scale? Faceting should only be used for ordinal or nominal data; Plot doesn’t currently support automatic binning of quantitative for facets. Though see #14 to upvote and for workarounds. That said, you can use faceting here for quantitative data if the domain has low cardinality.

Can you explain in more words what you are trying to accomplish? Is “a” supposed to be the label for minimum sun_min value? If so, the fy tickFormat option is probably what you want. But make sure the labels match the expected values.

Plot.plot({
  marginLeft: 80,
  fy: { tickFormat: (d, i) => `${["a", "b", "c", "d", "e"][i]} (${d.toFixed(2)})` },
  marks: [Plot.dotX(seasonsrdata, { fy: "sun_min", x: "sun" })]
})

Sorry about the copy-pasted code. I was trying to sort the fy scale in this case but I guess it would work the same way when sorting the y scale.
The variable sun_min can be considered ordinal as far as represents the lower bound of the sun_interval. A couple of data objects as a sample (I have 365, one record for each day of the year)

[{
  date: 2021-09-27
  sun: 0
  sun_interval: "0 - 1.3"
  sun_min: 0
  sun_max: 1.3
},
{
  date: 2021-09-28
  sun: 2.2
  sun_interval: "1.3 - 5.04"
  sun_min: 1.3
  sun_max: 5.04
}]

What I am really trying is to do group by sun interval, count the days in each interval, and show them sorted ‘numerically’ instead of ‘alphabetically’. The range “10.2 - 14.5” is in the middle of the scale :confused:

Plot.plot({
  marginLeft: 100,
  x: {label: "days"}, 
  marks: [
    Plot.barX(
      seasonsrdata.filter(d => d.city == 'City 26'), 
      Plot.groupY({x: "count"}, {
      y: "sun_interval"
    }))
  ]
})

I found grouping by sun_min a workaround to sort my bars, but then I couldn’t find the way to change the tick labels.
The faceting issue is explained because there is also a city variable, and I wanted to have a set of grouped bars.

Thank you for your time and your patience. I’m just landing here :slight_smile:

Ordinal scale domains are sorted naturally by default, and since “10.2 - 14.5” is a string, it is considered ahead of “5.05 - 7.7”. As you noted, if you use sun_min instead of sun_interval, you get the expected order because sun_min is defined as numbers instead of strings.

Plot.plot({
  marginLeft: 100,
  x: { label: "days" },
  marks: [
    Plot.barX(
      seasonsrdata.filter((d) => d.city == "City 26"),
      Plot.groupY({ x: "count" }, { y: "sun_min" })
    )
  ]
})

So, one solution is to specify the tickFormat option to control how a given sun_min value is displayed. Here’s one way to do that by finding an arbitrary row in the data with a given sun_min and returning the corresponding sun_interval:

Plot.plot({
  marginLeft: 100,
  x: { label: "days" },
  y: {
    label: "sun_interval",
    tickFormat: (y) => seasonsrdata.find((d) => d.sun_min === y).sun_interval
  },
  marks: [
    Plot.barX(
      seasonsrdata.filter((d) => d.city == "City 26"),
      Plot.groupY({ x: "count" }, { y: "sun_min" })
    )
  ]
})

If you had a lookup table from sun_min to sun_interval, you could use that instead.

Another approach is to group by sun_interval, and then specify the y scale’s domain so that the order is what you expect instead of the default natural order.

Plot.plot({
  marginLeft: 100,
  x: { label: "days" },
  y: { domain: ["0 - 1.3", "1.3 - 5.04", "5.04 - 7.7", "7.7 - 10.2", "10.2 - 14.5"] },
  marks: [
    Plot.barX(
      seasonsrdata.filter((d) => d.city == "City 26"),
      Plot.groupY({ x: "count" }, { y: "sun_interval" })
    )
  ]
})

If you don’t want to hardcode the domain, you can use the mark sort option to impute it from the data. Here we say that the y scale domain should be imputed from the data, and we should pull out the first sun_min value from each group to set the order:

Plot.plot({
  marginLeft: 100,
  x: { label: "days" },
  marks: [
    Plot.barX(
      seasonsrdata.filter((d) => d.city == "City 26"),
      Plot.groupY(
        { x: "count" },
        { y: "sun_interval", sort: { y: "data", reduce: ([[d]]) => d.sun_min } }
      )
    )
  ]
})

Another way we could write this is to extract the first number from the sun_interval value already used by the y channel:

Plot.plot({
  marginLeft: 100,
  x: { label: "days" },
  marks: [
    Plot.barX(
      seasonsrdata.filter((d) => d.city == "City 26"),
      Plot.groupY(
        { x: "count" },
        { y: "sun_interval", sort: { y: "y", reduce: ([d]) => parseFloat(d) } }
      )
    )
  ]
})

If you had another channel such as fill which was naturally ordered, you could use that too:

Plot.plot({
  marginLeft: 100,
  x: { label: "days" },
  marks: [
    Plot.barX(
      seasonsrdata.filter((d) => d.city == "City 26"),
      Plot.groupY(
        { x: "count" },
        { y: "sun_interval", fill: "sun_min", sort: { y: "fill" } }
      )
    )
  ]
})

Live examples: Plot: Sorting bars / Observable | Observable

Many thanks for such a complete answer :slight_smile:
I also had tried to use the mark sort without success. It is much more clear now!

thanks for your time!

1 Like