Lloyd Richards Design
VISX
D3

Jun 8, 2023

Brushable Timeline with Visx

Learn how to create a brushable timeline with Visx

There are some things that are more tedious to do with D3 than others. Brushable timelines are one of them. In this lab, going to explore how to create a brushable timeline with Visx. Visx is basiclly a low lever wrapper for D3 and accepts a lot of the same data structure such as scales. This makes sprinckling in a few of the more annpying parts such as Axis and Brushes more flexible.

JanFebMarAprMayJunJulAugSepOctNovDecJanCH 1CH 2CH 2CH 3

Brush Start:

Brush End:

What I wanted to do was create a brush which I could use to return back the domain of the scale so I can use it to select or filter data. In this first example I made a quick timetable which can be brushed to highlight any intersecting boxes.

<Brush
  xScale={xScale}
  yScale={yScale}
  height={height}
  width={width}
  onChange={(domain: Bounds | null) => {
    if (!domain) return;
    setSelected([new Date(domain.x0), new Date(domain.x1)]);
  }}
  onClick={() => setSelected(undefined)}
  //   innerRef={brushRef}
  //   useWindowMoveEvents
  //   resizeTriggerAreas={["left", "right"]}
  //   brushDirection="horizontal"
/>

The Brush from Visx was quite easy to drop in, taking the same xScale and yScale I built using D3. There is an onChange that returns the Bound that can be destructured into the domain values of the scales. There are also several default values for the resizeTriggerAreas and brushDirection which can be used to change the direction of the brush, but the default worked for my use case.

Using the value returned from the brush it was then possible to change the fill of the boxes that intersected the brush.

const intersect = (a: [Date, Date], b: [Date, Date]) =>
  a[0] <= b[1] && b[0] <= a[1];
 
export const Timeline = () => {
  // ...
  return (
    <svg>
      {/* ... */}
      {channels.map((c, i) => (
        <rect
          key={`rect-${i}`}
          rx={4}
          x={xScale(c.start)}
          y={yScale(c.channel) || 0}
          width={clampZero(xScale(c.end) - xScale(c.start))}
          height={yScale.bandwidth()}
          fill={
            selected != undefined
              ? intersect(selected, [c.start, c.end])
                ? "tomato"
                : "SteelBlue"
              : "SeaGreen"
          }
        />
      ))}
    </svg>
  );
};

Margin Bug

I noticed while looking at the example that there is a bug with the current version where the margin on the <Brush /> doesnt actually do anything. I've reported this issue to airbnb/visx, but for the time being its needed to change the D3 Pattern to match the translated <g /> similar to how is done in the <ChartArea /> of Visx.