Stepper Line Chart

I’ve recently joined the ObservableHQ community and would like to ask for your assistance or advice. I need to implement a chart that looks similar to the one below.

image

I’m curious about how to create a similar chart using d3 or plot. I want it to be stepped, with rounded corners and a gradient on state change. If one of the series repeats multiple times in a row (for example, “Sleep-Wake-Sleep”), I’d like the sleep line to be more pronounced.

If you have any ideas, tips, or code examples, I’d greatly appreciate your help!
Thank you in advance!

For stepping, you can massage your data into two parts:

  1. spans of time representing continuous stages, then draw them with Plot.rect and specify y1, y2 for bar height, x1, x2 for bar/stage length
  2. exect moment of stage changes, then draw with Plot.ruleX for vertical lines

Rounded corner I believe is limited to only rx or ry option when drawing with Plot.rect

I don’t think line gradient is directly supported now though, but you might be able to get away with drawing a bunch of tiny consecutive dashes with slightly different colour.

Plot.plot({
  marks: [
    Plot.rect(
      [
        {
          from: 0,
          to: 10,
          level: 3
        },
        {
          from: 10,
          to: 20,
          level: 2
        },
        {
          from: 20,
          to: 30,
          level: 1
        },
        {
          from: 30,
          to: 40,
          level: 4
        }
      ],
      {
        x1: "from",
        x2: "to",
        y1: ({ level }) => level - 0.1,
        y2: ({ level }) => level + 0.1,
        fill: "level",
        ry: 4
      }
    ),
    Plot.ruleX(
      [
        {
          at: 10,
          from: 3,
          to: 2
        },
        {
          at: 20,
          from: 2,
          to: 1
        },
        {
          at: 30,
          from: 1,
          to: 4
        }
      ],
      {
        x: "at",
        y1: "from",
        y2: "to",
        strokeWidth: 2,
        strokeOpacity: 0.2
      }
    )
  ],

  color: {
    domain: [1, 2, 3, 4],
    range: ["Blue", "CornflowerBlue", "DeepSkyBlue", "Red"]
  }
})

There is actually a way to add gradients. Found an example in

Thanks @MP_Li for your time and suggestion, I much appreciate your help mate!

The use of the gradient here is pretty subtle — it does piecewise blending between the two connected values (the two sleep states). Notice that when going from Awake to Light, the gradient skips the light blue of REM.

You can use a variable color encoding to achieve this. I’m sure the code could be simplified but here is my take. You’ll also need a fancier line renderer to have thick horizontal lines with rounded corners, with skinny vertical connectors…

Plot.plot({
  height: 400,
  marginTop: 0,
  style: "background: #292952; color: #ccc; padding: 1rem;",
  x: {
    inset: 10,
    tickSize: -6
  },
  y: {
    padding: 0,
    inset: 20,
    label: null,
    tickSize: 0,
    domain: ["awake", "rem", "light", "deep"]
  },
  marks: [
    () => htl.svg`<defs>
      <linearGradient id="awake-rem" gradientTransform="rotate(90)">
        <stop offset="0%" stop-color="#E24D70" />
        <stop offset="100%" stop-color="#8DC3F9" />
      </linearGradient>
      <linearGradient id="awake-light" gradientTransform="rotate(90)">
        <stop offset="0%" stop-color="#E24D70" />
        <stop offset="100%" stop-color="#538BF7" />
      </linearGradient>
      <linearGradient id="awake-deep" gradientTransform="rotate(90)">
        <stop offset="0%" stop-color="#E24D70" />
        <stop offset="100%" stop-color="#254AA1" />
      </linearGradient>
      <linearGradient id="light-rem" gradientTransform="rotate(90)">
        <stop offset="0%" stop-color="#8DC3F9" />
        <stop offset="100%" stop-color="#538BF7" />
      </linearGradient>
      <linearGradient id="deep-rem" gradientTransform="rotate(90)">
        <stop offset="0%" stop-color="#8DC3F9" />
        <stop offset="100%" stop-color="#254AA1" />
      </linearGradient>
      <linearGradient id="deep-light" gradientTransform="rotate(90)">
        <stop offset="0%" stop-color="#538BF7" />
        <stop offset="100%" stop-color="#254AA1" />
      </linearGradient>
    </defs>`,
    Plot.frame({anchor: "bottom"}),
    Plot.lineY(data, {
      x: "date",
      y: "state",
      z: null,
      curve: "step-after",
      stroke: (d, i, data) => `url(#${[d.state, data[i + 1]?.state].sort().join("-")})`,
      strokeWidth: 2,
      strokeLinecap: "round",
      strokeLinejoin: "round"
    })
  ]
})

https://observablehq.com/d/bd8b0bcccbca5527

3 Likes