Sorting facets in Plot

In this experiment, I’m trying use Plot to chart cumulative percentages of fully vaccinated people in different areas of Italy.

I’m wondering if there is a way to sort facets:

  1. by highest milestone (i.e., dots in chart)
    then, in case of equal milestones
  2. by milestone date
    then, if dates are equal too
  3. by area name.

E.g., Given the following data:

[
{area: "ABR", date: 2021-04-26, milestone: 0.1},
{area: "ABR", date: 2021-05-21, milestone: 0.2},
{area: "ABR", date: 2021-07-19, milestone: 0.5},
{area: "ABR", date: 2021-08-26, milestone: 0.7},
{area: "BAS", date: 2021-04-21, milestone: 0.1},
{area: "BAS", date: 2021-05-22, milestone: 0.2},
{area: "BAS", date: 2021-07-24, milestone: 0.5},
{area: "BAS", date: 2021-09-04, milestone: 0.7},
{area: "CAL", date: 2021-04-30, milestone: 0.1},
{area: "CAL", date: 2021-05-27, milestone: 0.2},
{area: "CAL", date: 2021-07-23, milestone: 0.5},
]

The facet sorting should be: ABR, BAS, CAL

1 Like

Since Plot 0.2 you can add

Plot.dot(data, {
  x: "date",
  sort: {fy: "x", reduce: "max"},
  …
})

which will do the point #1 (you can even remove reduce: "max" since it’s the default reduce). For more complex sorting you could do:

Plot.dot(data, {
  x: "date",
  sort: {fy: "data", reduce: f},
  …
})

where f is a function of the data. See allow sort to reduce data by mbostock · Pull Request #517 · observablehq/plot · GitHub for details. I don’t think we’re supporting a more complex sorting from inside the mark—for this you’d have to compute the order you want outside (with d3.groupSort, possibly).

2 Likes

Thank you @Fil for your feedback. Trying with:

    Plot.dot(data, {
      sort: { fy: "x", reduce: 'max' },
      x: "date",
      title: "milestone",
      fill: (d) => color(d.milestone)
    })

I get this:

So close, except for the first three facets that should be placed at bottom of the chart. Looking inside the code, it looks like this happens because the reducer sorts facets according to the max date in each series, regardless of the milestone value for such a date.

Even using reduce: f, I can only access to dates and not to their milestone values.

When you said:

I don’t think we’re supporting a more complex sorting from inside the mark—for this you’d have to compute the order you want outside (with d3.groupSort, possibly).

did you mean sorting data in advance and then passing them to Plot?

In your case you can do the following:

      sort: {
        fy: "data",
        reduce: (group) => [
          1 - d3.max(group, (d) => d.milestone), // milestone as a POSITIVE number
          d3.max(group, (d) => +d.date), // date as a number
          group[0].area
        ].join('/') // joined as a comparable string!
      }

The ideal (most semantic) version would be:

      sort: {
        fy: "data",
        reduce: (group) => [
          -d3.max(group, (d) => d.milestone), // numeric, reversed
          d3.max(group, (d) => d.date), // date
          group[0]["area"]
        ]
      }

without the “join” that creates a comparable string from the array — so in case of equality on the first element of the array (which would stay numeric), the second element would be used to separate, etc. But we can’t do this since Plot uses d3.ascending, which doesn’t do array comparisons.

Equivalently, you could write, in the definition of the fy scale:

domain: d3.groupSort(
  data,
  (group) =>
    [
      1 - d3.max(group, (d) => d.milestone),
      d3.max(group, (d) => +d.date),
      group[0]["area"]
    ].join("/"),
  (d) => d.area
)
2 Likes

Thank you @Fil, very neat!

Just a remark for those who’ll read this thread:

Even using reduce: f, I can only access to dates and not to their milestone values.

I was mistaken, it depends on the channel you are passing.

1 Like