Skip to content

Scales and axes

The scale

scale on the Chart sets the range of values it shows, as [min, max]. Every block in every Plot of the chart uses it.

<Chart scale={[0, 100]}>…</Chart>
<Chart scale={[-40, 40]}>…</Chart>      // negative values run the other way
<Chart scale={[0, Math.max(...values())]}>…</Chart> // a scale that follows the data

When the scale changes, everything on it moves together. A Bar that runs past the end of the scale is cut off there. A value Label stays at the end of the scale, still showing its value, so a number past the end can’t widen the page. A Dot, a Tick or a Place keeps the place its value gives it, even when that is outside the chart. To keep those inside, give the slat’s own element overflow: clip in its CSS.

The axis

The Chart draws an axis on its own: grid lines with numbers.

  • ticks picks the lines: a list ([0, 25, 50, 75, 100]), a rough count (5), or false for none.
  • format turns a value into its text: (v) => v + "%".
Phone
64%
Laptop
38%
Watch
91%
import { Chart, Plot, Bar, Label } from "@bezda/rhp";

export default function Battery() {
  return (
    <Chart
      scale={[0, 100]}
      ticks={[0, 25, 50, 75, 100]}
      format={(v) => v + "%"}
    >
      <Plot device={["Phone", "Laptop", "Watch"]} charge={[64, 38, 91]}>
        {(d) => (
          <div>
            <Label edge="start">{d.device}</Label>
            <Bar to={d.charge} />
            <Label at={d.charge}>{d.charge}%</Label>
          </div>
        )}
      </Plot>
    </Chart>
  );
}

Drawing the scale yourself

For a scale that looks different (dashed lines, numbers in a pill, bands between ticks), put a Scale in the Chart. A Scale is a Plot of ticks: its slat is drawn once per tick, and gets d.at, the tick’s value.

May
June
July
import { createSignal } from "solid-js";
import {
  Chart,
  Scale,
  Plot,
  Bar,
  Tick,
  Label,
  slat,
  every,
} from "@bezda/rhp";

// The scale is a slat too: a dashed line and a number per tick.
const Mark = slat(
  {
    room: { after: 22 },
    css: `
      .line {
        --rhp-tick-width: 1px;
        background: none;
        border-left: 1px dashed var(--rhp-grid);
      }
      .number {
        top: calc(100% + 6px);
        translate: -50% 0;
        --rhp-label-gap: 0px;
        color: var(--rhp-muted);
        font-size: 11px;
      }
    `,
  },
  (t) => (
    <div>
      <Tick at={t.at} thick={1} class="line" />
      <Label at={t.at} class="number">
        {t.at}
      </Label>
    </div>
  ),
);

export default function Rainfall() {
  const [max, setMax] = createSignal(60);
  return (
    <>
      <label class="demo-controls">
        Scale up to {max()} mm
        <input
          type="range"
          min="40"
          max="120"
          step="5"
          value={max()}
          onInput={(e) => setMax(+e.target.value)}
        />
      </label>
      <Chart scale={[0, max()]}>
        <Scale ticks={every(10)}>{Mark}</Scale>
        <Plot month={["May", "June", "July"]} rain={[38, 24, 11]}>
          {(d) => (
            <div>
              <Label edge="start">{d.month}</Label>
              <Bar to={d.rain} />
            </div>
          )}
        </Plot>
      </Chart>
    </>
  );
}

A Scale slat also gets:

  • d.next, the next tick’s value, to fill the space between two ticks,
  • d.first and d.last, true at the two ends,
  • d.toEnd, how far the tick is from the end of the scale in pixels, measured in the browser.

A number near the end of the scale can collide with the last one. CSS alone can hide it, and the same rule works when the chart is drawn on a server. rhp gives every Label the room it has left, from its own value to the end of the scale, as calc((1 - var(--rhp-p)) * 100%). Put that in max-width (max-height in a vertical chart) and a number with less than 30px of room is given no size at all:

.num { max-width: calc(((1 - var(--rhp-p)) * 100% - 30px) * 1000); overflow: hidden; }

every(10) gives a tick at each multiple of 10. every(10, { ends: true }) adds the scale’s two ends as ticks.

A Chart with a Scale in it draws no axis of its own.

A second axis

cross on the Chart adds a second scale, [min, max], that runs across the slats: the y of a scatter plot or a line chart. In a horizontal chart it runs from bottom to top, and in a vertical chart from left to right.

An overlap Plot’s slats all share the whole plot, so a Dot or a Label with cross sits at its at on the scale and at its cross on the second axis: a scatter plot.

Corner
Roastery
Kiosk
Book & Bean
Mill St
Harbour
import { Chart, Plot, Dot, Label } from "@bezda/rhp";

// Six cafés: the price on the scale, the rating on a second axis (cross).
export default function Cafes() {
  return (
    <Chart
      scale={[2, 5.5]}
      ticks={[2, 3, 4, 5]}
      format={(p) => "€" + p}
      cross={[3, 5]}
      crossTicks={[3, 4, 5]}
      crossFormat={(r) => r + "★"}
      height={220}
    >
      <Plot
        overlap
        cafe={[
          "Corner",
          "Roastery",
          "Kiosk",
          "Book & Bean",
          "Mill St",
          "Harbour",
        ]}
        price={[3.2, 4.5, 2.4, 3.8, 2.9, 4.1]}
        rating={[4.1, 4.8, 3.2, 4.4, 3.8, 3.5]}
      >
        {(d) => (
          <div>
            <Dot at={d.price} cross={d.rating} size="10px" />
            <Label
              at={d.price}
              cross={d.rating}
              style={{ "--rhp-label-gap": "9px" }}
            >
              {d.cafe}
            </Label>
          </div>
        )}
      </Plot>
    </Chart>
  );
}

A Line puts its points’ y on the second axis: a line chart. fill fills under the line, down to base (0 by default).

import { Chart, Plot, Dot, Line } from "@bezda/rhp";

// Lisbon's average high by month: a Line on the second axis, and a dot each month.
const month = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
const high = [15, 16, 19, 20, 23, 26, 28, 29, 27, 23, 18, 15];

export default function Lisbon() {
  return (
    <Chart
      scale={[0.5, 12.5]}
      ticks={month}
      format={(m) => "JFMAMJJASOND"[m - 1]}
      cross={[10, 30]}
      crossTicks={[10, 20, 30]}
      crossFormat={(t) => t + "°"}
      height={200}
    >
      <Plot overlap points={[month.map((m, i) => [m, high[i]])]}>
        {(d) => <Line points={d.points} fill base={10} />}
      </Plot>
      <Plot overlap month={month} high={high}>
        {(d) => <Dot at={d.month} cross={d.high} size="8px" />}
      </Plot>
    </Chart>
  );
}

crossTicks and crossFormat set the second axis, like ticks and format for the first. A horizontal chart with a second axis is 240px tall unless height says otherwise.

cross is optional. A chart without it draws no second axis, and the CSS for one only reaches the page once some chart on it asks for a second axis.