Plot: Display events using rect mark

Hi everyone :wave: ,

I’d like to show events or “time blocks” on a plot. Very similar to a calendar view. The x-axis shows the date, while the y-axis shows the time of the day.

However, I can’t get the blocks to render using the rect mark.
Would really appreciate any help/guidance on how I could accomplish this!

Best
Chris

Notebook:

1 Like

I think there are a few problems:

  • Your fill is white, so you won’t see anything.
  • You use d3.utcDay, which means that for sessions that start and end on the same day the start and end value will be the same, resulting in a width of zero.
  • For the time scale you force the date to 1970-01-01. This will likely have side effects. Instead I’d recommend to use +d3.utcFormat('%H%M')(date) so that you end up with zero-padded HHMM converted to int.
  • If your sessions span multiple days you’ll likely have to preprocess them and split them into multiple data times. Perhaps Plot has already some binning options for this.
  • Nitpick: You may want to preprocess your data anyway, so that you don’t have to instantiate a Date over and over again.
4 Likes

Like @mootari said, it’s tricky if you have sessions which span multiple days: you’d need to create multiple elements to display such sessions. Your data doesn’t appear to have any such sessions, at least in UTC time, but, here’s how you could do it:

sessions = (await FileAttachment("sessions.json").json())
  .map(({start, end}) => ({
    start: new Date(start),
    end: new Date(end)
  }))
  .flatMap(({start, end}) => {
    const days = d3.utcDay.range(d3.utcDay(start), end);
    return days.map((day, i) => ({
      start: i === 0 ? start : day,
      end: i === days.length - 1 ? end : days[i + 1]
    }));
  })
  .map(({start, end}) => ({
    start,
    end: new Date(end - 1) // convert to inclusive upper bound
  }))

I did a slightly tricky thing which is that I subtracted one millisecond from the end dates. This is mainly so that when you have sessions that span multiple days, the sessions end at 23:59:59.999Z on the same day rather than on 0:0:0.000Z on the next day.

Then, if you want the intraday time on the y scale, you can map all the dates to the same day 1970-01-01 like you may have been doing before, like so:

new Date(d.start - d3.utcDay(d.start))

d3.utcDay(d.start) returns the UTC midnight preceding or equal to d.start, so by subtracting the two, you count the number of milliseconds since UTC midnight. If you pass that to the Date constructor, it’ll give you the equivalent UTC time on January 1, 1970. You probably also want to reverse the y scale so that time goes down ↓ like in a typical calendar.

Likewise if you want the x scale to show the start and end day, you can use d3.utcDay to round d.start down, and d3.utcDay.ceil to round d.end up. All together that might look like this:

Plot.plot({
  x: {
    tickFormat: "%d.%m",
    round: true
  },
  y: {
    grid: true,
    tickFormat: "%I %p",
    reverse: true,
    nice: true
  },
  marks: [
    Plot.rect(sessions, {
      x1: d => d3.utcDay(d.start),
      x2: d => d3.utcDay.ceil(d.end),
      y1: d => new Date(d.start - d3.utcDay(d.start)),
      y2: d => new Date(d.end - d3.utcDay(d.end)),
      insetLeft: 0.5,
      insetRight: 0.5
    }),
    Plot.ruleY([new Date("1970-01-01"), new Date("1970-01-01T23:59:59.999Z")])
  ]
})
1 Like

Awesome! Thanks so much @mbostock for your answer!