Plot: Rects overflow when domain is explicitly set on binned axis

I have the following plot where the rect “overbleeds” the y axis:

 Plot.plot({
  width: width,
  y: {
    grid: true,
    label: "↑ count"
  },
  x: { domain:[d3.min(data, d => d.date), d3.max(data, d=>d.date)]},
  marks: [
    Plot.rectY(data, Plot.binX({y: "count"}, {x: k => k.date, thresholds: range, fill: "#444791"})),
    Plot.ruleY([0])
  ]
})

working example

Using inset on the axis cures the symptom, but only if another bin-range is chosen and the rects changes their width.
This only happens when the domain is explicitly set with the min and max value.

What can I do to get the plot properly rendered?

There are two ways you could do this:

  • the first is to set the min and max to the first day of the first month, and the last day of the last month, rather than on the first and the last days of the dataset.
x: { domain:[d3.utcMonth.floor(d3.min(data, d => d.date)), d3.utcMonth.ceil(d3.max(data, d=>d.date))]},
  • the second will be to use the forthcoming {clip: true} option in the bar mark; it’s available in the development version, and hopefully should be deployed on observable later this week.

The result of both options are shown below:

(Note that in the second case the clipped bars are difficult to interpret, so I’d recommend the first approach.)

So don’t set it? Why do you want/feel the need to set the domain explicitly?

I’ll also add that you can add nice: range to nice your domain to match the thresholds you are using for bins. This allows you to set the domain semi-explicitly, while still guaranteeing that it aligns with bin thresholds. That would look like this:

Plot.plot({
  width,
  x: {
    domain: d3.extent(data, (d) => d.date), // note: redundant
    nice: range
  },
  y: {
    grid: true
  },
  marks: [
    Plot.rectY(
      data,
      Plot.binX(
        { y: "count" },
        { x: "date", thresholds: range, fill: "#444791" }
      )
    ),
    Plot.ruleY([0])
  ]
})

Oh, one more option is that you can explicitly opt-out of extending the first and last bin to cover a complete interval by computing the thresholds yourself:

Plot.plot({
  width,
  y: {
    grid: true
  },
  marks: [
    Plot.rectY(
      data,
      Plot.binX(
        { y: "count" },
        {
          x: "date",
          thresholds: (_, lo, hi) => range.range(lo, hi),
          fill: "#444791"
        }
      )
    ),
    Plot.ruleY([0])
  ]
})

(I removed explicitly computing the x-domain here because it’s redundant.)

Makes no sense in this example I agree. I mistakenly tried to simplify the example.
In the real use case I want to show a fixed range in the plot. Like the last half year independent from the data that is available. So the starting point is kind of fixed.

I guess I could still make floor work in this case but nice:range seems to do what I want, right of the box.

Many thanks! I am blown away by that quick response and the manifold options you provide.

2 Likes