How to sort a grouped bar chart in Observable Plot in Javascript / TypeScript

337 Views Asked by At

I have a grouped bar chart made using Observable Plot, and I can't override the auto sort feature in this library. I have 2 possible routes to go, and I'll use whichever can solve the problem.

The first is preferred, which looks like this. The problem is the months aren't sorted properly. They are sorted properly in my array, but Plot.plot is auto sorting them alphabetically. chart1

The 2nd option is using the dates as DateTime, which are sorted properly, however I don't know how to display the names of months instead of numbers for the month. This chart looks like this: enter image description here

The day is irrelevant in this chart.

The difference in these 2 charts is the commented lines that are marked with chart 1 and chart 2 -> which are the x-values of the facet. (I don't understand what a facet is, or how it is different from a bar chart?)

I am looking for any solution that looks good. I also am very new to Observable Plot, so any help is appreciated! Thank you.

If the code needs to be vastly refactored to solve the problem, I am happy to take any suggestions.

Code:

    const chart = Plot.plot({
      x: { axis: null, domain: ["Add", "Remove"] },
      y: { tickFormat: "s", label: "↑ Access Requests", grid: true },
      color: {
        legend: false,
        type: "categorical",
        domain: ["Add", "Remove"],
        range: redGreenColorRange,
      },
      style: {
        background: "transparent",
        fontSize: 16,
      },
      width: 1350,
      height: 500,
      marginTop: 40,
      caption: "",
      facet: {
        data: groupedAddRemove, // typeof AddRemoveBarChartType[]
        label: "",
        //x: "monthAsString", // chart 1
        x: "createdDate", // chart 2
        // sort here?
      },
      marks: [
        Plot.barY(groupedAddRemove, {
          x: "type",
          y: "count",
          fill: "type",
        }),
        Plot.ruleY([0]),
      ],
    });

Data structure:

type AddRemoveBarChartType = {
  createdDate: Date;
  monthAsString: string;
  count: number;
  type: "Add" | "Remove";
};

1

There are 1 best solutions below

0
customcommander On

The x scale isn't right. From the doc:

[…]A scale’s type is most often inferred from associated marks’ channel values: strings and booleans imply an ordinal scale[…]

[…]For an ordinal scale, the domain defaults to the sorted union (all distinct values in natural order) of associated values;[…]

Assuming the following dataset:

[ {month:  "Jan", qty: 10}
, {month:  "Feb", qty: 20}
, {month:  "Mar", qty: 30}
, {month:  "Apr", qty: 40}]

What this means is that if month is associated with the x channel and you don't set the scale and domain for that channel then Plot automatically assumes an ordinal scale (because the values are strings) and the domain is set to all the unique values sorted in alphabetic order: ["Apr","Feb","Jan","Mar"].

And so display in that order:

p = Plot.plot({
  marks: [Plot.dotX(data, {x:'month', r:'qty'})]
})

document.querySelector('div').append(p)
<script src="https://observablehq.com/plot/d3.js"></script>
<script src="https://observablehq.com/plot/plot.js"></script>
<script>
data = [ {month:  "Jan", qty: 10}
       , {month:  "Feb", qty: 20}
       , {month:  "Mar", qty: 30}
       , {month:  "Apr", qty: 40}]
</script>
<div></div>

We (humans) know this isn't the right order for months but Plot doesn't, so we need to set the domain to match the order we want:

p = Plot.plot({
  x: {
    domain: ["Jan","Feb","Mar","Apr"]
  },
  marks: [Plot.dotX(data, {x:'month', r:'qty'})]
})

document.querySelector('div').append(p)
<script src="https://observablehq.com/plot/d3.js"></script>
<script src="https://observablehq.com/plot/plot.js"></script>
<script>
data = [ {month:  "Jan", qty: 10}
       , {month:  "Feb", qty: 20}
       , {month:  "Mar", qty: 30}
       , {month:  "Apr", qty: 40}]
</script>
<div></div>

If you have yyyy-mm-dd strings these will be sorted in the expected order, you just need to format them on the x axis: (I have reordered the data to make that point)

parse  = d3.timeParse('%Y-%m-%d');
format = d3.timeFormat('%b');

p = Plot.plot({
  x: {
    tickFormat: ymd => format(parse(ymd))
  },
  marks: [Plot.dotX(data, {x:'month', r:'qty'})]
})

document.querySelector('div').append(p)
<script src="https://observablehq.com/plot/d3.js"></script>
<script src="https://observablehq.com/plot/plot.js"></script>
<script>
data = [ {month:  "2024-03-08", qty: 30} 
       , {month:  "2024-02-29", qty: 20}
       , {month:  "2024-04-01", qty: 40}
       , {month:  "2024-01-10", qty: 10}]
</script>
<div></div>

If you have a channel dealing with what looks like dates, you really ought to convert those values to proper JavaScript date objects first. Plot will be able to correctly set the scale and domain for you. Plot is likely to emit warnings when it sees this.