Skip to content

Interaction

Charts are HTML, so interaction works the way it does anywhere else on a page.

Hover with CSS only

Most hover effects need no code: style :hover in the row’s CSS.

import { Chart, Plot, Bar, Label, slat } from "@bezda/rhp";

// Point at a row: its band lights up and its exact number appears. Only
// CSS.
const Row = slat(
  {
    css: `
      .row:hover {
        background: color-mix(in srgb, var(--rhp-ink) 8%, transparent);
      }
      .tip { opacity: 0; transition: opacity .15s; }
      .row:hover .tip { opacity: 1; }
    `,
  },
  (d) => (
    <div class="row">
      <Label edge="start">{d.city}</Label>
      <Bar to={d.rain} />
      <Label at={d.rain}>
        <span class="tip">{d.rain} mm</span>
      </Label>
    </div>
  ),
);

export default function Rain() {
  return (
    <Chart scale={[0, 200]}>
      <Plot
        city={["Bergen", "Glasgow", "Dublin", "London"]}
        rain={[184, 124, 81, 56]}
      >
        {Row}
      </Plot>
    </Chart>
  );
}

The fade is on a span inside the Label, not on the Label. rhp moves blocks with transitions of its own, and a transition on a block would replace them.

Clicks

Blocks take handlers like any element: onClick, onPointerEnter, onKeyDown. With many rows, one handler around the chart is simpler: mark blocks with a data- attribute and find it with closest.

What the handler decides goes back into the Plot as data. Here the picked city becomes d.faded for the other rows, and their CSS fades them.

import { createSignal } from "solid-js";
import { Chart, Plot, Bar, Label, slat } from "@bezda/rhp";

// Click a bar to pick its city; the others fade. The pick goes back
// into the
// Plot as data.
const Row = slat(
  {
    css: `
      .bar { cursor: pointer; }
      .faded .bar { opacity: .3; }
    `,
  },
  (d) => (
    <div class={d.faded ? "faded" : ""}>
      <Label edge="start">{d.city}</Label>
      <Bar to={d.rain} class="bar" data-city={d.city} />
      <Label at={d.rain}>{d.rain} mm</Label>
    </div>
  ),
);

export default function Rain() {
  const [picked, setPicked] = createSignal();
  const onClick = (e) => {
    const city = e.target.closest("[data-city]")?.dataset.city;
    if (city) setPicked(picked() === city ? undefined : city);
  };
  return (
    <div onClick={onClick}>
      <Chart scale={[0, 200]}>
        <Plot
          city={["Bergen", "Glasgow", "Dublin", "London"]}
          rain={[184, 124, 81, 56]}
          faded={(d) => picked() !== undefined && d.city !== picked()}
        >
          {Row}
        </Plot>
      </Chart>
    </div>
  );
}

Keyboard

Put a <button> inside a Label (a name you can click, for example) and it gets focus and keyboard presses for free. For a Bar, add tabindex="0" and a key handler.

Good to know

  • Follow the pointer with pointermove, not pointerover. When a hover changes the layout (a badge appears), the browser can fire pointerover again under a pointer that hasn’t moved.
  • A Plot drawn over another one covers it. Give an overlay, such as a line marking today, style={{ "pointer-events": "none" }} so the pointer reaches the Plot under it.
  • Page listeners and tests work as usual. Charts are plain elements in the page: querySelector, testing libraries and click listeners all see them.