observable plot cell: how to order y axis by a specific field

Hi,
I have this plot cell chart, and I would like to sort the y axis items list using the latitude field.

I have added these setting but these do not work.

How to set it properly?

Thank you

        {
          x: "data",
          y: "citta",
          fill: "livello_n",
          inset: 1,
          tip: false,
          sort: "latitude",
          reverse: true
        }

When you set the sort mark option to a field name such as latitude, youā€™re applying a sort transform to sort the order in which the marks are drawn, i.e. the z order. (And here because thereā€™s also a group transform, youā€™re sorting the data prior to grouping, which has no effect on the resulting display.) Likewise the reverse mark option simply reverses the z order.

In Plot speak, what youā€™re asking is how to set the domain of the y scale.

The most literal way of setting the domain would be to supply the list of values in the desired order:

Plot.plot({
  y: {
    label: null,
    domain: [
      "CATANIA",
      "REGGIO CALABRIA",
      "PALERMO",
      "MESSINA",
      "CAGLIARI",
      "NAPOLI",
      "BARI",
      "LATINA",
      "CAMPOBASSO",
      ā€¦
    ]
  },
  marks: ā€¦
})

But there are various ways to do this more implicitly from the data.

One way is to use d3.groupSort: you group by the data by citta and then return the corresponding latitude for that citta. Assuming that your data is consistent and all rows with a given citta have the same latitude, you can pull out the first row for each group and return its latitude:

Plot.plot({
  y: {
    label: null,
    domain: d3.groupSort(
      diecigiorni,
      ([d]) => d.latitude,
      (d) => d.citta
    )
  },
  marks: ā€¦
})

In other cases, you might do something like compute the median value for each group:

Plot.plot({
  y: {
    label: null,
    domain: d3.groupSort(
      diecigiorni,
      (D) => d3.median(D, (d) => d.latitude),
      (d) => d.citta
    )
  },
  marks: ā€¦
})

In Plot, you can implicitly set an ordinal scaleā€™s domain using the mark sort option. This imputes the given ordinal scaleā€™s domain from the values in a given mark channel. This feature is often used to produce a sorted bar chart, where the bars are ordered from shortest to longest or vice versa (second example here).

Itā€™s a little trickier to use this feature here because the latitude field is not used as a visual encoding; itā€™s merely associated with the citta field. And also, youā€™re using a group transform ā€” although the group transform doesnā€™t seem to be necessary here since there is only one row in the dataset per citta and data. So just to demonstrate, if you set the y channel to the latitude field instead of the citta field, and removed the group transform, you can use the sort option like so:

Plot.plot({
  ā€¦,
  marks: [
    Plot.cell(diecigiorni, {
      x: "data",
      y: "latitude",
      fill: "livello_n",
      inset: 1,
      sort: { y: "y" }
    })
  ]
})

If you want the group transform still, itā€™s like so (the group transform groups by x and y, so you can still reference it for imputing the y scale domain after grouping):

Plot.plot({
  ā€¦,
  marks: [
    Plot.cell(
      diecigiorni,
      Plot.group(
        { fill: "max" },
        {
          x: "data",
          y: "latitude",
          fill: "livello_n",
          inset: 1,
          sort: { y: "y" }
        }
      )
    )
  ]
})

But now your y scale ticks are hard to read, clearly, and the chart wouldnā€™t correctly handle two cities having the same latitude. :grin:

So now that weā€™ve seen how the sort option works in the most direct case, letā€™s go back to using the citta field for y and change the sort option to use the reduce option to find the latitude of each city:

Plot.plot({
  ā€¦,
  marks: [
    Plot.cell(diecigiorni, {
      x: "data",
      y: "citta",
      fill: "livello_n",
      inset: 1,
      sort: {
        y: "y",
        reduce: ([y]) => diecigiorni.find((d) => d.citta === y).latitude
      }
    })
  ]
})

Note that the reduce function is passed an array of values from the markā€™s y channel: thatā€™s because there are multiple rows in the data associated with each citta, but to order the y domain we must return a single orderable value for each distinct citta. Again, as long as the data is consistent with the citta and latitude columns, itā€™s fine to just pull out the first value using square brackets. (I also omitted the group transform, but you could put it back in as shown in the other examples; it doesnā€™t affect how the reduce function is defined here.)

Lastly, you could do this by passing data to the sort option for the y scale; that way you donā€™t have to reference diecigiorni in the reduce function. Then the reduce function is passed an array of data instead of an array of y channel values. That looks like this:

Plot.plot({
  ā€¦,
  marks: [
    Plot.cell(diecigiorni, {
      x: "data",
      y: "citta",
      fill: "livello_n",
      inset: 1,
      sort: {
        y: "data",
        reduce: ([d]) => d.latitude
      }
    })
  ]
})

If you still want the group transform, youā€™ll need an extra set of brackets in your reduce function because now the reducer will be passed the grouped data for each y value:

Plot.plot({
  ā€¦,
  marks: [
    Plot.cell(
      diecigiorni,
      Plot.group(
        { fill: "max" },
        {
          x: "data",
          y: "citta",
          fill: "livello_n",
          inset: 1,
          sort: {
            y: "data",
            reduce: ([[d]]) => d.latitude
          }
        }
      )
    )
  ]
})

Hope this helps.

4 Likes

Oh, and to combine with your other question on date formats, here is some complete code:

calendario = Plot.plot({
  marginLeft: 120,
  marginBottom: 40,
  padding: 0,
  color: {
    domain: [0, 1, 2, 3],
    range: ["#5bd601", "#e8d203", "#ff7f02", "#c40001"]
  },
  label: null,
  x: {
    type: "band",
    tickFormat(x, i, X) {
      const format1 = (x) => x.toLocaleString("it", { day: "numeric" });
      const format2 = (x) => x.toLocaleString("it", { month: "short" });
      const f1 = format1(x);
      const f2 = format2(x);
      return i > 0 && f2 === format2(X[i - 1]) ? f1 : [f1, f2].join("\n");
    }
  },
  marks: [
    Plot.cell(diecigiorni, {
      x: "data",
      y: "citta",
      fill: "livello_n",
      inset: 0.5,
      sort: { y: "data", reduce: ([d]) => -d.latitude },
      tip: true,
      title: (d) =>
        `${d.citta}, livello ${d.livello_n}\n${d.data.toLocaleString("it", {
          day: "numeric",
          month: "long",
          year: "numeric"
        })}`
    })
  ]
})

I also used the tip option instead of a separate tip mark for simplicity. Let me know if you have another question there. :slight_smile:

2 Likes

ā€œThe great and the powerful Bostokā€!!

It is a great honor and an immense pleasure to receive your reply, thank you very much.

I have applied your code and now I have sorted the y axis. I have edited inserting one character, because I want reverse sorting, to have North-South ordering (these are Italian cities, and the colors are related to heat waves).
It works great.

However, I take the opportunity to talk to you about two related things, which seem important to me.

The first one is closely related. This function, to sort the order of ordinal axes, using one of the dataframe fields, seems to me an option that should be ready already. Something that can immediately interest all users who need to use this type of graph.
I know, observable plot doesnā€™t think like that, but it would be nice to change it so that the sort option is available. Also in cell chart.
To be able to simply add something like

sorty: "fieldname",
reverse: true

Before writing here, I tried to sort the dataframe by latitude, but then the sorting by citta was forced in any case. Another option might be to make it depend on the ordering of the dataframe.

The second one is related to tooltips. Related to this calendar view, I have the URLs of the heatwave bulletins in PDF.
I would like to be able to build a sticky tooltip, in which I have an hyperlink to open then PDFS in a _blank window.

In the same way I can do in this map in datawrapper.
I have tried to build this map using Plot, but itā€™s not a native way to do it this in Plot.

So I take advantage of having you here, to tell you that I think itā€™s very useful to be able to customize the tooltips.

:heart_hands: Thank you very much

image

Dear @mbostock I have replied to you in a wrong thread:
https://talk.observablehq.com/t/observable-plot-cell-and-date-label/8116/7

Thank you very much

We discussed the following syntax in #867:

sort: {y: "fieldname", reverse: true}

The issue is that you can already use this to sort by channel values:

sort: {y: "x", reverse: true}

Or more accurately:

sort: {y: "x", order: "descending"}

And more succinctly:

sort: {y: "-x"}

If we allow you to specify a field name as well as a channel name, then there is potential confusion as to whether the name refers to a field or a channel. For example, you could have a CSV file with a column named ā€œxā€. And we likely canā€™t support the convenient shorthand minus syntax for descending order, since a field name could start with a hyphen.

Possibly we could disambiguate using the following syntax for channels:

sort: {y: {channel: "x", order: "descending"}}

And this syntax for fields:

sort: {y: {value: "latitude", order: "descending"}}

We already allow the latter for channel namesā€¦ so Iā€™d probably want to think a bit how to introduce this without breaking backwards compatibility. But in any case itā€™s not really that different than using the reduce option as I showed above.

The current solution is a bit circuitous: you define an extra channel using the channels option, and then you can reference that for sorting. For example:

Plot.plot({
  marks: [
    Plot.barX(clean_monthly_april, {
      y: "title",
      x: "activations",
      channels: {difference: "difference"},
      sort: {y: "difference"}
    })
  ]
})

Anyway, Iā€™ve reopened the issueā€¦ we can probably find an acceptable solution to the ambiguity issue, and I agree this is a common enough concern.

Iā€™m not sure yet how weā€™ll solve this. As you found we have a #1612 for customizing the format of values in the tip mark, but youā€™re asking for something different which is supporting rich text and links, such as through Markdown or HTML. That would be difficult to implement because the tip mark is rendered in SVG, and we intentionally limited its expressiveness so we could make the implementation simpler and focus on displaying readable values. Perhaps we need a separate mark type that uses e.g. foreignObject to embed rich content.

You can generate a map with Plot.geo and then manipulate it a bit further with a little Javascript to add tooltips that are more flexible than what you can do with Plot alone. I generated the following map, which looks a bit like your Datawrapper map, using this technique:

Note that the tooltips are stick, have a clickable link, and can contain arbitrary HTML.

1 Like

You are very kind, thank you very much.
I will use it and once it will be ready I will share the URL here.

I think itā€™s important to have a native way to do it in Plot.

Thank you

1 Like

@mcmcclur I was actually attempting to achieve something similar and posted a question here: Plot Pointer - x,y coordinates I ran into an issue getting the coordinates for the ā€œdotsā€ (as I didnā€™t think to try and replace the marks with something new). My plot has more dots then just the dots which need tooltipsā€¦ wondering if you have any thoughts/insights into how to select specific dots from a set of marks?