Charts

Line chart

One to four series over an ordered x axis, with gridlines, real text ticks and a readout per point.

Built on
<svg>vector-effectcontainer queries@starting-stylecolor-mix()
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
5.3 kB
Built from
Chart legend
  1. Wk 1
    • This quarter182
    • Last quarter164
  2. Wk 2
    • This quarter204
    • Last quarter171
  3. Wk 3
    • This quarter198
    • Last quarter189
  4. Wk 4
    • This quarter241
    • Last quarter176
  5. Wk 5
    • This quarter263
    • Last quarter202
  6. Wk 6
    • This quarter249
    • Last quarter214
  7. Wk 7
    • This quarter288
    • Last quarter208
  8. Wk 8
    • This quarter312
    • Last quarter226
Try its options

Loading its options.

Code
import { LineChart } from "@/components/equip/line-chart";

const weeks = ["Wk 1", "Wk 2", "Wk 3", "Wk 4", "Wk 5", "Wk 6", "Wk 7", "Wk 8"];

export default function Demo() {
  return (
    <LineChart
      label="Weekly sign-ups, this quarter against last"
      labels={weeks}
      series={[
        { label: "This quarter", values: [182, 204, 198, 241, 263, 249, 288, 312] },
        { label: "Last quarter", values: [164, 171, 189, 176, 202, 214, 208, 226] },
      ]}
      height={64}
      ticks={2}
      smooth
      style={{ width: 216 }}
    />
  );
}
Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<figure aria-label="Weekly sign-ups, this quarter against last. This quarter from 182 to 312, highest 312 in Wk 8; Last quarter from 164 to 226, highest 226 in Wk 8" class="eq-line-chart" data-points="hidden" style="width:216px;--_h:64px">
  <ul role="list" aria-hidden="true" class="eq-chart-legend eq-line-chart-legend" data-orientation="row">
    <li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">This quarter</span></li>
    <li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Last quarter</span></li>
  </ul>
  <div class="eq-line-chart-body">
    <div class="eq-line-chart-y" aria-hidden="true">
      <span class="eq-line-chart-ytick" style="--_y:100%">0</span>
      <span class="eq-line-chart-ytick" style="--_y:50%">200</span>
      <span class="eq-line-chart-ytick" style="--_y:0%">400</span>
    </div>
    <div class="eq-line-chart-plot">
      <svg class="eq-line-chart-grid" aria-hidden="true" viewBox="0 0 100 100" preserveAspectRatio="none">
        <line class="eq-line-chart-gridline" data-base="true" x1="0" x2="100" y1="100" y2="100" />
        <line class="eq-line-chart-gridline" x1="0" x2="100" y1="50" y2="50" />
        <line class="eq-line-chart-gridline" x1="0" x2="100" y1="0" y2="0" />
      </svg>
      <svg class="eq-line-chart-lines" aria-hidden="true" viewBox="0 0 100 100" preserveAspectRatio="none">
        <path class="eq-line-chart-line" data-series="0" d="M0 54.5 C4.76 52.67 9.53 49 14.29 49 C19.05 49 23.81 50.5 28.57 50.5 C33.33 50.5 38.1 42.46 42.86 39.75 C47.62 37.04 52.38 34.25 57.14 34.25 C61.9 34.25 66.67 37.75 71.43 37.75 C76.19 37.75 80.95 30.62 85.71 28 C90.47 25.37 95.24 24 100 22" />
        <path class="eq-line-chart-line" data-series="1" d="M0 59 C4.76 58.42 9.53 58.29 14.29 57.25 C19.05 56.21 23.81 52.75 28.57 52.75 C33.33 52.75 38.1 56 42.86 56 C47.62 56 52.38 51.08 57.14 49.5 C61.9 47.92 66.67 46.5 71.43 46.5 C76.19 46.5 80.95 48 85.71 48 C90.47 48 95.24 45 100 43.5" />
      </svg>
      <ol class="eq-line-chart-cols" role="list" aria-label="Weekly sign-ups, this quarter against last">
        <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:0%;--_w:7.14%;--_c:0%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:54.5%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:59%"><rect x="1" y="1" width="8" height="8" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Wk 1</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">This quarter</span><span class="eq-chart-legend-value">182</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Last quarter</span><span class="eq-chart-legend-value">164</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:7.15%;--_w:14.29%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:49%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:57.25%"><rect x="1" y="1" width="8" height="8" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Wk 2</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">This quarter</span><span class="eq-chart-legend-value">204</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Last quarter</span><span class="eq-chart-legend-value">171</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:21.43%;--_w:14.29%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:50.5%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:52.75%"><rect x="1" y="1" width="8" height="8" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Wk 3</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">This quarter</span><span class="eq-chart-legend-value">198</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Last quarter</span><span class="eq-chart-legend-value">189</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:35.72%;--_w:14.29%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:39.75%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:56%"><rect x="1" y="1" width="8" height="8" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Wk 4</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">This quarter</span><span class="eq-chart-legend-value">241</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Last quarter</span><span class="eq-chart-legend-value">176</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:50%;--_w:14.29%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:34.25%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:49.5%"><rect x="1" y="1" width="8" height="8" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Wk 5</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">This quarter</span><span class="eq-chart-legend-value">263</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Last quarter</span><span class="eq-chart-legend-value">202</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:64.29%;--_w:14.29%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:37.75%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:46.5%"><rect x="1" y="1" width="8" height="8" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Wk 6</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">This quarter</span><span class="eq-chart-legend-value">249</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Last quarter</span><span class="eq-chart-legend-value">214</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:78.57%;--_w:14.29%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:28%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:48%"><rect x="1" y="1" width="8" height="8" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Wk 7</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">This quarter</span><span class="eq-chart-legend-value">288</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Last quarter</span><span class="eq-chart-legend-value">208</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:92.86%;--_w:7.14%;--_c:100%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:22%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:43.5%"><rect x="1" y="1" width="8" height="8" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Wk 8</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">This quarter</span><span class="eq-chart-legend-value">312</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Last quarter</span><span class="eq-chart-legend-value">226</span></li></ul></span></li>
      </ol>
    </div>
    <div class="eq-line-chart-x" aria-hidden="true"><span class="eq-line-chart-xtick" data-edge="start" style="--_x:0%">Wk 1</span><span class="eq-line-chart-xtick" data-minor="true" style="--_x:28.57%">Wk 3</span><span class="eq-line-chart-xtick" style="--_x:57.14%">Wk 5</span><span class="eq-line-chart-xtick" data-minor="true" style="--_x:85.71%">Wk 7</span></div>
  </div>
</figure>

Examples

Revenue by month

One series has no legend; hover or tab to a month for its exact value, and the figure's name already says the range and the peak.

  1. Jan
    • Revenue$48.2K
  2. Feb
    • Revenue$51.9K
  3. Mar
    • Revenue$53.4K
  4. Apr
    • Revenue$58.1K
  5. May
    • Revenue$57.6K
  6. Jun
    • Revenue$62.3K
  7. Jul
    • Revenue$66.8K
  8. Aug
    • Revenue$71.2K
  9. Sep
    • Revenue$69.9K
  10. Oct
    • Revenue$75.4K
  11. Nov
    • Revenue$81.3K
  12. Dec
    • Revenue$86.7K
Code
import { LineChart } from "@/components/equip/line-chart";

const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

export default function Example() {
  return (
    <LineChart
      label="Monthly recurring revenue, 2026"
      labels={months}
      series={[{ label: "Revenue", values: [48200, 51900, 53400, 58100, 57600, 62300, 66800, 71200, 69900, 75400, 81300, 86700] }]}
      format={{ style: "currency", currency: "USD", notation: "compact", maximumFractionDigits: 1 }}
      smooth
      points
      style={{ maxWidth: 520 }}
    />
  );
}
HTMLworks in any framework, with the part's CSS
<figure aria-label="Monthly recurring revenue, 2026: from $48.2K to $86.7K, highest $86.7K in Dec" class="eq-line-chart" data-points="shown" style="max-width:520px;--_h:140px">
  <div class="eq-line-chart-body">
    <div class="eq-line-chart-y" aria-hidden="true">
      <span class="eq-line-chart-ytick" style="--_y:100%">$0</span>
      <span class="eq-line-chart-ytick" style="--_y:75%">$25K</span>
      <span class="eq-line-chart-ytick" style="--_y:50%">$50K</span>
      <span class="eq-line-chart-ytick" style="--_y:25%">$75K</span>
      <span class="eq-line-chart-ytick" style="--_y:0%">$100K</span>
    </div>
    <div class="eq-line-chart-plot">
      <svg class="eq-line-chart-grid" aria-hidden="true" viewBox="0 0 100 100" preserveAspectRatio="none">
        <line class="eq-line-chart-gridline" data-base="true" x1="0" x2="100" y1="100" y2="100" />
        <line class="eq-line-chart-gridline" x1="0" x2="100" y1="75" y2="75" />
        <line class="eq-line-chart-gridline" x1="0" x2="100" y1="50" y2="50" />
        <line class="eq-line-chart-gridline" x1="0" x2="100" y1="25" y2="25" />
        <line class="eq-line-chart-gridline" x1="0" x2="100" y1="0" y2="0" />
      </svg>
      <svg class="eq-line-chart-lines" aria-hidden="true" viewBox="0 0 100 100" preserveAspectRatio="none">
        <path class="eq-line-chart-line" data-series="0" d="M0 51.8 C3.03 50.57 6.06 48.97 9.09 48.1 C12.12 47.23 15.15 47.6 18.18 46.6 C21.21 45.6 24.24 41.9 27.27 41.9 C30.3 41.9 33.33 42.4 36.36 42.4 C39.39 42.4 42.42 39.23 45.45 37.7 C48.48 36.17 51.52 34.68 54.55 33.2 C57.58 31.72 60.61 28.8 63.64 28.8 C66.67 28.8 69.7 30.1 72.73 30.1 C75.76 30.1 78.79 26.5 81.82 24.6 C84.85 22.7 87.88 20.58 90.91 18.7 C93.94 16.82 96.97 15.1 100 13.3" />
      </svg>
      <ol class="eq-line-chart-cols" role="list" aria-label="Monthly recurring revenue, 2026">
        <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:0%;--_w:4.55%;--_c:0%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:51.8%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Jan</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Revenue</span><span class="eq-chart-legend-value">$48.2K</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:4.54%;--_w:9.09%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:48.1%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Feb</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Revenue</span><span class="eq-chart-legend-value">$51.9K</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:13.63%;--_w:9.09%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:46.6%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Mar</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Revenue</span><span class="eq-chart-legend-value">$53.4K</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:22.72%;--_w:9.09%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:41.9%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Apr</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Revenue</span><span class="eq-chart-legend-value">$58.1K</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:31.81%;--_w:9.09%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:42.4%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">May</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Revenue</span><span class="eq-chart-legend-value">$57.6K</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:40.9%;--_w:9.09%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:37.7%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Jun</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Revenue</span><span class="eq-chart-legend-value">$62.3K</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:50%;--_w:9.09%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:33.2%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Jul</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Revenue</span><span class="eq-chart-legend-value">$66.8K</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:59.09%;--_w:9.09%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:28.8%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Aug</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Revenue</span><span class="eq-chart-legend-value">$71.2K</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:68.18%;--_w:9.09%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:30.1%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Sep</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Revenue</span><span class="eq-chart-legend-value">$69.9K</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:77.27%;--_w:9.09%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:24.6%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Oct</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Revenue</span><span class="eq-chart-legend-value">$75.4K</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:86.36%;--_w:9.09%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:18.7%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Nov</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Revenue</span><span class="eq-chart-legend-value">$81.3K</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:95.45%;--_w:4.55%;--_c:100%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:13.3%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Dec</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Revenue</span><span class="eq-chart-legend-value">$86.7K</span></li></ul></span></li>
      </ol>
    </div>
    <div class="eq-line-chart-x" aria-hidden="true"><span class="eq-line-chart-xtick" data-edge="start" style="--_x:0%">Jan</span><span class="eq-line-chart-xtick" data-minor="true" style="--_x:18.18%">Mar</span><span class="eq-line-chart-xtick" style="--_x:36.36%">May</span><span class="eq-line-chart-xtick" data-minor="true" style="--_x:54.55%">Jul</span><span class="eq-line-chart-xtick" style="--_x:72.73%">Sep</span><span class="eq-line-chart-xtick" data-minor="true" style="--_x:90.91%">Nov</span></div>
  </div>
</figure>

Latency percentiles

With zero off the scale hugs the data; each series has its own dash, and the readout lists all three at the focused hour.

  1. 09:00
    • p50142ms
    • p95182ms
    • p99301ms
  2. 10:00
    • p50148ms
    • p95190ms
    • p99322ms
  3. 11:00
    • p50151ms
    • p95214ms
    • p99380ms
  4. 12:00
    • p50163ms
    • p95262ms
    • p99471ms
  5. 13:00
    • p50155ms
    • p95231ms
    • p99402ms
  6. 14:00
    • p50149ms
    • p95198ms
    • p99340ms
  7. 15:00
    • p50146ms
    • p95187ms
    • p99318ms
  8. 16:00
    • p50150ms
    • p95196ms
    • p99333ms
  9. 17:00
    • p50158ms
    • p95221ms
    • p99389ms
  10. 18:00
    • p50147ms
    • p95184ms
    • p99312ms
Code
import { LineChart } from "@/components/equip/line-chart";

const hours = ["09:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00", "16:00", "17:00", "18:00"];

export default function Example() {
  return (
    <LineChart
      label="API latency in milliseconds, today"
      labels={hours}
      series={[
        { label: "p50", values: [142, 148, 151, 163, 155, 149, 146, 150, 158, 147] },
        { label: "p95", values: [182, 190, 214, 262, 231, 198, 187, 196, 221, 184] },
        { label: "p99", values: [301, 322, 380, 471, 402, 340, 318, 333, 389, 312] },
      ]}
      zero={false}
      format={{ style: "unit", unit: "millisecond", unitDisplay: "narrow" }}
      height={160}
      style={{ maxWidth: 520 }}
    />
  );
}
HTMLworks in any framework, with the part's CSS
<figure aria-label="API latency in milliseconds, today. p50 from 142ms to 147ms, highest 163ms in 12:00; p95 from 182ms to 184ms, highest 262ms in 12:00; p99 from 301ms to 312ms, highest 471ms in 12:00" class="eq-line-chart" data-points="hidden" style="max-width:520px;--_h:160px">
  <ul role="list" aria-hidden="true" class="eq-chart-legend eq-line-chart-legend" data-orientation="row">
    <li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p50</span></li>
    <li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p95</span></li>
    <li class="eq-chart-legend-item" data-series="2"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p99</span></li>
  </ul>
  <div class="eq-line-chart-body">
    <div class="eq-line-chart-y" aria-hidden="true">
      <span class="eq-line-chart-ytick" style="--_y:100%">100ms</span>
      <span class="eq-line-chart-ytick" style="--_y:75%">200ms</span>
      <span class="eq-line-chart-ytick" style="--_y:50%">300ms</span>
      <span class="eq-line-chart-ytick" style="--_y:25%">400ms</span>
      <span class="eq-line-chart-ytick" style="--_y:0%">500ms</span>
    </div>
    <div class="eq-line-chart-plot">
      <svg class="eq-line-chart-grid" aria-hidden="true" viewBox="0 0 100 100" preserveAspectRatio="none">
        <line class="eq-line-chart-gridline" data-base="true" x1="0" x2="100" y1="100" y2="100" />
        <line class="eq-line-chart-gridline" x1="0" x2="100" y1="75" y2="75" />
        <line class="eq-line-chart-gridline" x1="0" x2="100" y1="50" y2="50" />
        <line class="eq-line-chart-gridline" x1="0" x2="100" y1="25" y2="25" />
        <line class="eq-line-chart-gridline" x1="0" x2="100" y1="0" y2="0" />
      </svg>
      <svg class="eq-line-chart-lines" aria-hidden="true" viewBox="0 0 100 100" preserveAspectRatio="none">
        <path class="eq-line-chart-line" data-series="0" d="M0 89.5 L11.11 88 L22.22 87.25 L33.33 84.25 L44.44 86.25 L55.56 87.75 L66.67 88.5 L77.78 87.5 L88.89 85.5 L100 88.25" />
        <path class="eq-line-chart-line" data-series="1" d="M0 79.5 L11.11 77.5 L22.22 71.5 L33.33 59.5 L44.44 67.25 L55.56 75.5 L66.67 78.25 L77.78 76 L88.89 69.75 L100 79" />
        <path class="eq-line-chart-line" data-series="2" d="M0 49.75 L11.11 44.5 L22.22 30 L33.33 7.25 L44.44 24.5 L55.56 40 L66.67 45.5 L77.78 41.75 L88.89 27.75 L100 47" />
      </svg>
      <ol class="eq-line-chart-cols" role="list" aria-label="API latency in milliseconds, today">
        <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:0%;--_w:5.56%;--_c:0%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:89.5%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:79.5%"><rect x="1" y="1" width="8" height="8" /></svg><svg class="eq-line-chart-point" data-series="2" aria-hidden="true" viewBox="0 0 10 10" style="--_y:49.75%"><path d="M5 0 10 5 5 10 0 5Z" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">09:00</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p50</span><span class="eq-chart-legend-value">142ms</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p95</span><span class="eq-chart-legend-value">182ms</span></li><li class="eq-chart-legend-item" data-series="2"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p99</span><span class="eq-chart-legend-value">301ms</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:5.55%;--_w:11.11%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:88%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:77.5%"><rect x="1" y="1" width="8" height="8" /></svg><svg class="eq-line-chart-point" data-series="2" aria-hidden="true" viewBox="0 0 10 10" style="--_y:44.5%"><path d="M5 0 10 5 5 10 0 5Z" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">10:00</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p50</span><span class="eq-chart-legend-value">148ms</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p95</span><span class="eq-chart-legend-value">190ms</span></li><li class="eq-chart-legend-item" data-series="2"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p99</span><span class="eq-chart-legend-value">322ms</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:16.66%;--_w:11.11%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:87.25%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:71.5%"><rect x="1" y="1" width="8" height="8" /></svg><svg class="eq-line-chart-point" data-series="2" aria-hidden="true" viewBox="0 0 10 10" style="--_y:30%"><path d="M5 0 10 5 5 10 0 5Z" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">11:00</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p50</span><span class="eq-chart-legend-value">151ms</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p95</span><span class="eq-chart-legend-value">214ms</span></li><li class="eq-chart-legend-item" data-series="2"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p99</span><span class="eq-chart-legend-value">380ms</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:27.77%;--_w:11.11%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:84.25%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:59.5%"><rect x="1" y="1" width="8" height="8" /></svg><svg class="eq-line-chart-point" data-series="2" aria-hidden="true" viewBox="0 0 10 10" style="--_y:7.25%"><path d="M5 0 10 5 5 10 0 5Z" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">12:00</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p50</span><span class="eq-chart-legend-value">163ms</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p95</span><span class="eq-chart-legend-value">262ms</span></li><li class="eq-chart-legend-item" data-series="2"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p99</span><span class="eq-chart-legend-value">471ms</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:38.88%;--_w:11.11%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:86.25%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:67.25%"><rect x="1" y="1" width="8" height="8" /></svg><svg class="eq-line-chart-point" data-series="2" aria-hidden="true" viewBox="0 0 10 10" style="--_y:24.5%"><path d="M5 0 10 5 5 10 0 5Z" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">13:00</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p50</span><span class="eq-chart-legend-value">155ms</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p95</span><span class="eq-chart-legend-value">231ms</span></li><li class="eq-chart-legend-item" data-series="2"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p99</span><span class="eq-chart-legend-value">402ms</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:50%;--_w:11.11%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:87.75%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:75.5%"><rect x="1" y="1" width="8" height="8" /></svg><svg class="eq-line-chart-point" data-series="2" aria-hidden="true" viewBox="0 0 10 10" style="--_y:40%"><path d="M5 0 10 5 5 10 0 5Z" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">14:00</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p50</span><span class="eq-chart-legend-value">149ms</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p95</span><span class="eq-chart-legend-value">198ms</span></li><li class="eq-chart-legend-item" data-series="2"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p99</span><span class="eq-chart-legend-value">340ms</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:61.11%;--_w:11.11%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:88.5%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:78.25%"><rect x="1" y="1" width="8" height="8" /></svg><svg class="eq-line-chart-point" data-series="2" aria-hidden="true" viewBox="0 0 10 10" style="--_y:45.5%"><path d="M5 0 10 5 5 10 0 5Z" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">15:00</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p50</span><span class="eq-chart-legend-value">146ms</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p95</span><span class="eq-chart-legend-value">187ms</span></li><li class="eq-chart-legend-item" data-series="2"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p99</span><span class="eq-chart-legend-value">318ms</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:72.22%;--_w:11.11%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:87.5%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:76%"><rect x="1" y="1" width="8" height="8" /></svg><svg class="eq-line-chart-point" data-series="2" aria-hidden="true" viewBox="0 0 10 10" style="--_y:41.75%"><path d="M5 0 10 5 5 10 0 5Z" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">16:00</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p50</span><span class="eq-chart-legend-value">150ms</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p95</span><span class="eq-chart-legend-value">196ms</span></li><li class="eq-chart-legend-item" data-series="2"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p99</span><span class="eq-chart-legend-value">333ms</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:83.33%;--_w:11.11%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:85.5%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:69.75%"><rect x="1" y="1" width="8" height="8" /></svg><svg class="eq-line-chart-point" data-series="2" aria-hidden="true" viewBox="0 0 10 10" style="--_y:27.75%"><path d="M5 0 10 5 5 10 0 5Z" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">17:00</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p50</span><span class="eq-chart-legend-value">158ms</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p95</span><span class="eq-chart-legend-value">221ms</span></li><li class="eq-chart-legend-item" data-series="2"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p99</span><span class="eq-chart-legend-value">389ms</span></li></ul></span></li>
        <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:94.44%;--_w:5.56%;--_c:100%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:88.25%"><circle cx="5" cy="5" r="5" /></svg><svg class="eq-line-chart-point" data-series="1" aria-hidden="true" viewBox="0 0 10 10" style="--_y:79%"><rect x="1" y="1" width="8" height="8" /></svg><svg class="eq-line-chart-point" data-series="2" aria-hidden="true" viewBox="0 0 10 10" style="--_y:47%"><path d="M5 0 10 5 5 10 0 5Z" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">18:00</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p50</span><span class="eq-chart-legend-value">147ms</span></li><li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p95</span><span class="eq-chart-legend-value">184ms</span></li><li class="eq-chart-legend-item" data-series="2"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p99</span><span class="eq-chart-legend-value">312ms</span></li></ul></span></li>
      </ol>
    </div>
    <div class="eq-line-chart-x" aria-hidden="true"><span class="eq-line-chart-xtick" data-edge="start" style="--_x:0%">09:00</span><span class="eq-line-chart-xtick" data-minor="true" style="--_x:22.22%">11:00</span><span class="eq-line-chart-xtick" style="--_x:44.44%">13:00</span><span class="eq-line-chart-xtick" data-minor="true" style="--_x:66.67%">15:00</span><span class="eq-line-chart-xtick" style="--_x:88.89%">17:00</span></div>
  </div>
</figure>

Gaps and no data

A null breaks the line instead of drawing a false zero, a value with no neighbours gets a mark, and an empty chart says so.

  1. Mon
    • Devices1,240
  2. Tue
    • Devices1,310
  3. Wed
    • DevicesNo data
  4. Thu
    • Devices1,285
  5. Fri
    • DevicesNo data
  6. Sat
    • Devices980
  7. Sun
    • Devices1,012
No data
Code
import { LineChart } from "@/components/equip/line-chart";

const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 24, width: "100%", maxWidth: 420 }}>
      <LineChart
        label="Active devices, last 7 days; the collector was down on Wednesday and Friday"
        labels={days}
        series={[{ label: "Devices", values: [1240, 1310, null, 1285, null, 980, 1012] }]}
        height={100}
      />
      <LineChart label="Active devices in ap-south-2" labels={days} series={[{ label: "Devices", values: [] }]} height={80} />
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:24px;width:100%;max-width:420px">
  <figure aria-label="Active devices, last 7 days; the collector was down on Wednesday and Friday: from 1,240 to 1,012, highest 1,310 in Tue" class="eq-line-chart" data-points="hidden" style="--_h:100px">
    <div class="eq-line-chart-body">
      <div class="eq-line-chart-y" aria-hidden="true">
        <span class="eq-line-chart-ytick" style="--_y:100%">0</span>
        <span class="eq-line-chart-ytick" style="--_y:66.67%">500</span>
        <span class="eq-line-chart-ytick" style="--_y:33.33%">1,000</span>
        <span class="eq-line-chart-ytick" style="--_y:0%">1,500</span>
      </div>
      <div class="eq-line-chart-plot">
        <svg class="eq-line-chart-grid" aria-hidden="true" viewBox="0 0 100 100" preserveAspectRatio="none">
          <line class="eq-line-chart-gridline" data-base="true" x1="0" x2="100" y1="100" y2="100" />
          <line class="eq-line-chart-gridline" x1="0" x2="100" y1="66.67" y2="66.67" />
          <line class="eq-line-chart-gridline" x1="0" x2="100" y1="33.33" y2="33.33" />
          <line class="eq-line-chart-gridline" x1="0" x2="100" y1="0" y2="0" />
        </svg>
        <svg class="eq-line-chart-lines" aria-hidden="true" viewBox="0 0 100 100" preserveAspectRatio="none">
          <path class="eq-line-chart-line" data-series="0" d="M0 17.33 L16.67 12.67 M83.33 34.67 L100 32.53" />
        </svg>
        <ol class="eq-line-chart-cols" role="list" aria-label="Active devices, last 7 days; the collector was down on Wednesday and Friday">
          <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:0%;--_w:8.33%;--_c:0%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:17.33%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Mon</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Devices</span><span class="eq-chart-legend-value">1,240</span></li></ul></span></li>
          <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:8.34%;--_w:16.67%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:12.67%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Tue</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Devices</span><span class="eq-chart-legend-value">1,310</span></li></ul></span></li>
          <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:25%;--_w:16.67%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Wed</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Devices</span><span class="eq-chart-legend-value">No data</span></li></ul></span></li>
          <li class="eq-line-chart-col" tabindex="0" data-side="start" style="--_a:41.67%;--_w:16.67%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" data-lone="true" aria-hidden="true" viewBox="0 0 10 10" style="--_y:14.33%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Thu</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Devices</span><span class="eq-chart-legend-value">1,285</span></li></ul></span></li>
          <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:58.34%;--_w:16.67%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Fri</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Devices</span><span class="eq-chart-legend-value">No data</span></li></ul></span></li>
          <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:75%;--_w:16.67%;--_c:50%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:34.67%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Sat</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Devices</span><span class="eq-chart-legend-value">980</span></li></ul></span></li>
          <li class="eq-line-chart-col" tabindex="0" data-side="end" style="--_a:91.67%;--_w:8.33%;--_c:100%"><span class="eq-line-chart-guide" aria-hidden="true"></span><svg class="eq-line-chart-point" data-series="0" aria-hidden="true" viewBox="0 0 10 10" style="--_y:32.53%"><circle cx="5" cy="5" r="5" /></svg><span class="eq-line-chart-tip"><span class="eq-line-chart-tip-label">Sun</span><ul role="list" class="eq-chart-legend" data-orientation="column"><li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Devices</span><span class="eq-chart-legend-value">1,012</span></li></ul></span></li>
        </ol>
      </div>
      <div class="eq-line-chart-x" aria-hidden="true"><span class="eq-line-chart-xtick" data-edge="start" style="--_x:0%">Mon</span><span class="eq-line-chart-xtick" data-minor="true" style="--_x:33.33%">Wed</span><span class="eq-line-chart-xtick" style="--_x:66.67%">Fri</span><span class="eq-line-chart-xtick" data-edge="end" data-minor="true" style="--_x:100%">Sun</span></div>
    </div>
  </figure>
  <figure aria-label="Active devices in ap-south-2: no data" class="eq-line-chart" data-points="hidden" style="--_h:80px">
    <div class="eq-line-chart-body">
      <div class="eq-line-chart-y" aria-hidden="true"></div>
      <div class="eq-line-chart-plot">
        <svg class="eq-line-chart-grid" aria-hidden="true" viewBox="0 0 100 100" preserveAspectRatio="none">
          <line class="eq-line-chart-gridline" data-base="" x1="0" x2="100" y1="100" y2="100" />
        </svg>
        <span class="eq-line-chart-empty">No data</span>
      </div>
      <div class="eq-line-chart-x" aria-hidden="true"><span class="eq-line-chart-xtick" data-edge="start" style="--_x:0%">Mon</span><span class="eq-line-chart-xtick" data-minor="true" style="--_x:33.33%">Wed</span><span class="eq-line-chart-xtick" style="--_x:66.67%">Fri</span><span class="eq-line-chart-xtick" data-edge="end" data-minor="true" style="--_x:100%">Sun</span></div>
    </div>
  </figure>
</div>

Props

PropTypeDefaultWhat it does
series{ label: string; values: (number | null)[] }[]noneOne to four series, one value per x label. null leaves a gap. Past four the rest are left out.
labelsstring[]noneThe ordered x axis, such as months. Sets how many points each line has.
labelstringnoneNames the figure. The range and peak of each line are added to it.
heightnumber140Height of the plot area in pixels.
smoothbooleanfalseA monotone curve through the points, which never overshoots them.
pointsbooleanfalseA mark on every point, not only the focused or hovered one.
zerobooleantrueStarts the scale at zero. Turn it off to let the scale hug the data.
minnumbernoneFixes the bottom of the scale.
maxnumbernoneFixes the top of the scale.
ticksnumber4About how many gridlines to draw; steps land on 1, 2, 2.5 or 5.
showLegendbooleanseries.length > 1Shows the legend above the plot.
formatIntl.NumberFormatOptionsnoneHow values and ticks are written.
localestring"en-US"Passed to Intl.NumberFormat.
...restComponentProps<"figure">noneEvery native figure attribute.

Anatomy

root.eq-line-chartA figure named with the summary; data-points says whether every point is marked.
legend.eq-line-chart-legendA Chart legend with line marks.
body.eq-line-chart-bodyThe axes and plot; a size container.
y axis.eq-line-chart-y
y tick.eq-line-chart-ytick
plot.eq-line-chart-plot
grid.eq-line-chart-grid
gridline.eq-line-chart-gridlinedata-base marks the zero line.
lines.eq-line-chart-linesDraws in from the left on the spring.
line.eq-line-chart-linedata-series sets its strength and dash.
empty.eq-line-chart-emptyShown instead of lines when there is no data.
columns.eq-line-chart-colsA list with one focusable item per x label.
column.eq-line-chart-col
guide.eq-line-chart-guideThe vertical rule under the pointer or focus.
point.eq-line-chart-pointA shape per series; data-lone marks a value with no neighbours.
tip.eq-line-chart-tipThe readout: visually hidden until its column is hovered or focused.
tip label.eq-line-chart-tip-label
x axis.eq-line-chart-x
x tick.eq-line-chart-xtickdata-minor ticks step aside on a narrow chart.

Keyboard

TabMoves through the x labels; each one shows and reads every series value at that point.

Notes

  • Lines differ in ink strength and in dash, and every readout names its series, so nothing relies on colour alone.
  • The svg is hidden from assistive tech; the list of columns is the text alternative, one item per x label with every value.
  • Each x label is a tab stop, which suits up to a few dozen points; for a long series use a Sparkline or a Table.
  • The chart takes the full width of its container; cap it with max-width or set a width.
  • Ticks and points are HTML over a stretched svg, so text stays crisp and points stay round at any width.
  • Renders on the server with no client JavaScript.
  • The draw-in uses @starting-style; where it is missing the lines are simply there.

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/line-chart.tsx279 lines
import type { CSSProperties, ComponentProps } from "react";
import { ChartLegend } from "./chart-legend";
import "./line-chart.css";

export type LineSeries = {
  label: string;
  /** One value per x label, in the same order. null leaves a gap in the line. */
  values: (number | null)[];
};

export type LineChartProps = ComponentProps<"figure"> & {
  /** One to four series. Past four, the rest are left out. */
  series: LineSeries[];
  /** The ordered x axis, such as months or days. Sets how many points each line has. */
  labels: string[];
  /** Names the chart, such as "Revenue by month". The range of each line is added to it. */
  label: string;
  /** Height of the plot area in pixels. */
  height?: number;
  /** Draws a monotone curve through the points instead of straight segments. */
  smooth?: boolean;
  /** Shows a mark on every point, not only the one under the pointer or focus. */
  points?: boolean;
  /** Starts the scale at zero when every value is above it. */
  zero?: boolean;
  /** Fixes the bottom of the scale. */
  min?: number;
  /** Fixes the top of the scale. */
  max?: number;
  /** About how many gridlines to draw. */
  ticks?: number;
  /** Shows the legend above the plot. On by default when there is more than one series. */
  showLegend?: boolean;
  format?: Intl.NumberFormatOptions;
  locale?: string;
};

const MAX_SERIES = 4;
const MAX_X_TICKS = 6;

export function LineChart({
  series,
  labels,
  label,
  height = 140,
  smooth = false,
  points = false,
  zero = true,
  min,
  max,
  ticks = 4,
  showLegend,
  format,
  locale = "en-US",
  className,
  style,
  ...rest
}: LineChartProps) {
  const number = new Intl.NumberFormat(locale, format);
  const lines = series.slice(0, MAX_SERIES);
  const n = labels.length;
  const at = (s: LineSeries, i: number) => {
    const v = s.values[i];
    return typeof v === "number" && Number.isFinite(v) ? v : null;
  };
  const all = lines.flatMap((s) => labels.map((_, i) => at(s, i))).filter((v): v is number => v !== null);
  const empty = n === 0 || all.length === 0;

  const dataLo = empty ? 0 : Math.min(...all);
  const dataHi = empty ? 1 : Math.max(...all);
  const scale = niceScale(zero ? Math.min(0, dataLo) : dataLo, zero ? Math.max(0, dataHi) : dataHi, ticks, min, max);
  const y = (v: number) => round((1 - (v - scale.lo) / (scale.hi - scale.lo)) * 100);
  const x = (i: number) => (n === 1 ? 50 : round((i / (n - 1)) * 100));
  const base = scale.lo <= 0 && scale.hi >= 0 ? 0 : scale.lo;

  const paths = lines.map((s) => {
    const runs: { x: number; y: number }[][] = [];
    let run: { x: number; y: number }[] = [];
    labels.forEach((_, i) => {
      const v = at(s, i);
      if (v === null) {
        if (run.length) runs.push(run);
        run = [];
      } else run.push({ x: x(i), y: y(v) });
    });
    if (run.length) runs.push(run);
    return {
      d: runs.filter((r) => r.length > 1).map((r) => (smooth ? curve(r) : straight(r))).join(" "),
      lone: new Set(runs.filter((r) => r.length === 1).map((r) => r[0]?.x)),
    };
  });

  const describe = (s: LineSeries) => {
    const vals = labels.map((_, i) => at(s, i));
    const known = vals.flatMap((v, i) => (v === null ? [] : [{ v, i }]));
    const first = known[0];
    const last = known[known.length - 1];
    const top = known.reduce<{ v: number; i: number } | undefined>((b, d) => (!b || d.v > b.v ? d : b), undefined);
    if (!first || !last || !top) return "no data";
    return `from ${number.format(first.v)} to ${number.format(last.v)}, highest ${number.format(top.v)} in ${labels[top.i]}`;
  };
  const summary = empty
    ? `${label}: no data`
    : lines.length === 1 && lines[0]
      ? `${label}: ${describe(lines[0])}`
      : `${label}. ${lines.map((s) => `${s.label} ${describe(s)}`).join("; ")}`;

  const step = Math.max(1, Math.ceil(n / MAX_X_TICKS));
  const width = n > 1 ? 100 / (n - 1) : 100;
  const legend = showLegend ?? lines.length > 1;

  return (
    <figure
      aria-label={summary}
      {...rest}
      className={["eq-line-chart", className].filter(Boolean).join(" ")}
      data-points={points ? "shown" : "hidden"}
      style={{ ...style, "--_h": `${height}px` } as CSSProperties}
    >
      {legend && !empty && (
        <ChartLegend className="eq-line-chart-legend" mark="line" items={lines.map((s) => ({ label: s.label }))} aria-hidden="true" />
      )}
      <div className="eq-line-chart-body">
        <div className="eq-line-chart-y" aria-hidden="true">
          {!empty &&
            scale.ticks.map((t) => (
              <span key={t} className="eq-line-chart-ytick" style={{ "--_y": `${y(t)}%` } as CSSProperties}>
                {number.format(t)}
              </span>
            ))}
        </div>
        <div className="eq-line-chart-plot">
          <svg className="eq-line-chart-grid" aria-hidden="true" viewBox="0 0 100 100" preserveAspectRatio="none">
            {!empty &&
              scale.ticks.map((t) => (
                <line key={t} className="eq-line-chart-gridline" data-base={t === base || undefined} x1="0" x2="100" y1={y(t)} y2={y(t)} />
              ))}
            {empty && <line className="eq-line-chart-gridline" data-base="" x1="0" x2="100" y1="100" y2="100" />}
          </svg>
          {empty ? (
            <span className="eq-line-chart-empty">No data</span>
          ) : (
            <>
              <svg className="eq-line-chart-lines" aria-hidden="true" viewBox="0 0 100 100" preserveAspectRatio="none">
                {paths.map((p, k) => p.d && <path key={k} className="eq-line-chart-line" data-series={k} d={p.d} />)}
              </svg>
              <ol className="eq-line-chart-cols" role="list" aria-label={label}>
                {labels.map((name, i) => {
                  const cx = x(i);
                  const a = Math.max(0, cx - width / 2);
                  const b = Math.min(100, cx + width / 2);
                  return (
                    <li
                      key={`${name}-${i}`}
                      className="eq-line-chart-col"
                      tabIndex={0}
                      data-side={n > 1 && i >= n / 2 ? "end" : "start"}
                      style={{ "--_a": `${round(a)}%`, "--_w": `${round(b - a)}%`, "--_c": `${round(((cx - a) / (b - a || 1)) * 100)}%` } as CSSProperties}
                    >
                      <span className="eq-line-chart-guide" aria-hidden="true" />
                      {lines.map((s, k) => {
                        const v = at(s, i);
                        return v === null ? null : (
                          <svg
                            key={k}
                            className="eq-line-chart-point"
                            data-series={k}
                            data-lone={paths[k]?.lone.has(cx) || undefined}
                            aria-hidden="true"
                            viewBox="0 0 10 10"
                            style={{ "--_y": `${y(v)}%` } as CSSProperties}
                          >
                            <Shape series={k} />
                          </svg>
                        );
                      })}
                      <span className="eq-line-chart-tip">
                        <span className="eq-line-chart-tip-label">{name}</span>
                        <ChartLegend
                          mark="line"
                          orientation="column"
                          items={lines.map((s, k) => {
                            const v = at(s, i);
                            return { label: s.label, value: v === null ? "No data" : number.format(v), series: k };
                          })}
                        />
                      </span>
                    </li>
                  );
                })}
              </ol>
            </>
          )}
        </div>
        <div className="eq-line-chart-x" aria-hidden="true">
          {labels.map((name, i) =>
            i % step === 0 ? (
              <span
                key={`${name}-${i}`}
                className="eq-line-chart-xtick"
                data-edge={i === 0 ? "start" : i === n - 1 ? "end" : undefined}
                data-minor={(i / step) % 2 === 1 || undefined}
                style={{ "--_x": `${x(i)}%` } as CSSProperties}
              >
                {name}
              </span>
            ) : null,
          )}
        </div>
      </div>
    </figure>
  );
}

function Shape({ series }: { series: number }) {
  if (series === 1) return <rect x="1" y="1" width="8" height="8" />;
  if (series === 2) return <path d="M5 0 10 5 5 10 0 5Z" />;
  if (series === 3) return <path d="M5 0.5 10 9.5H0Z" />;
  return <circle cx="5" cy="5" r="5" />;
}

/* Round steps of 1, 2, 2.5 or 5 times a power of ten, so gridlines land on numbers people say. */
function niceScale(lo: number, hi: number, count: number, fixLo?: number, fixHi?: number) {
  let a = fixLo ?? lo;
  let b = fixHi ?? hi;
  if (!(b > a)) b = a + (Math.abs(a) || 1);
  const raw = (b - a) / Math.max(1, count);
  const mag = 10 ** Math.floor(Math.log10(raw));
  const f = raw / mag;
  /* Take the first round step at or above the ideal one that lands on both ends, so a fixed end gets its own line. */
  const steps = [1, 2, 2.5, 5, 10, 20, 25, 50].map((m) => m * mag).filter((s) => s >= raw * (1 - 1e-9) || s === 10 * mag);
  const fits = (s: number) => {
    const lo2 = fixLo ?? Math.floor(a / s) * s;
    const hi2 = fixHi ?? Math.ceil(b / s) * s;
    const k = (hi2 - lo2) / s;
    return Math.abs(k - Math.round(k)) < 1e-6 && k <= count * 2;
  };
  const step = steps.find(fits) ?? (f <= 1 ? 1 : f <= 2 ? 2 : f <= 2.5 ? 2.5 : f <= 5 ? 5 : 10) * mag;
  if (fixLo === undefined) a = Math.floor(a / step) * step;
  if (fixHi === undefined) b = Math.ceil(b / step) * step;
  const ticks: number[] = [];
  for (let k = Math.ceil(a / step - 1e-9); k * step <= b + step * 1e-9; k++) ticks.push(Math.round(k * step * 1e9) / 1e9 || 0);
  return { lo: a, hi: b, ticks };
}

function straight(r: { x: number; y: number }[]) {
  return r.map((p, i) => `${i === 0 ? "M" : "L"}${p.x} ${p.y}`).join(" ");
}

/* A monotone cubic (Steffen), so the curve never overshoots a point or dips below the baseline. */
function curve(r: { x: number; y: number }[]) {
  const s = r.slice(1).map((p, i) => {
    const q = r[i] ?? p;
    return (p.y - q.y) / (p.x - q.x || 1);
  });
  const m = r.map((p, i) => {
    const s0 = s[i - 1];
    const s1 = s[i];
    if (s0 === undefined) return s1 ?? 0;
    if (s1 === undefined) return s0;
    const h0 = p.x - (r[i - 1]?.x ?? p.x);
    const h1 = (r[i + 1]?.x ?? p.x) - p.x;
    const mid = (s0 * h1 + s1 * h0) / (h0 + h1 || 1);
    return (Math.sign(s0) + Math.sign(s1)) * Math.min(Math.abs(s0), Math.abs(s1), 0.5 * Math.abs(mid));
  });
  let d = `M${r[0]?.x ?? 0} ${r[0]?.y ?? 0}`;
  for (let i = 0; i < r.length - 1; i++) {
    const p = r[i];
    const q = r[i + 1];
    if (!p || !q) continue;
    const h = (q.x - p.x) / 3;
    d += ` C${round(p.x + h)} ${round(p.y + (m[i] ?? 0) * h)} ${round(q.x - h)} ${round(q.y - (m[i + 1] ?? 0) * h)} ${q.x} ${q.y}`;
  }
  return d;
}

function round(n: number) {
  return Math.round(n * 100) / 100;
}
components/equip/line-chart.css297 lines
@layer equip {
  .eq-line-chart {
    display: grid;
    gap: var(--eq-space-3);
    width: 100%;
    min-width: 0;
    margin: 0;
    font: var(--eq-weight) var(--eq-text-xs) / 1.3 var(--eq-font);
    color: var(--eq-ink-2);
  }

  /* The series steps, the same as Chart legend's: ink strength plus a dash per step. */
  .eq-line-chart [data-series] {
    --_tone: var(--eq-chart-1);
    --_dash: none;
  }

  .eq-line-chart [data-series="1"] {
    --_tone: var(--eq-chart-2);
    --_dash: 5 4;
  }

  .eq-line-chart [data-series="2"] {
    --_tone: var(--eq-chart-3);
    --_dash: 0.1 4;
  }

  .eq-line-chart [data-series="3"] {
    --_tone: var(--eq-chart-4);
    --_dash: 8 4 0.1 4;
  }

  .eq-line-chart-body {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    grid-template-rows: var(--_h) auto;
    column-gap: var(--eq-space-2);
    padding-block-start: var(--eq-space-1);
    container-type: inline-size;
  }

  .eq-line-chart-y {
    display: grid;
    grid-template-rows: var(--_h);
    color: var(--eq-ink-3);
    font-variant-numeric: tabular-nums;
    text-align: end;
  }

  /* Every tick shares one cell, so the column is as wide as the widest label. */
  .eq-line-chart-ytick {
    grid-area: 1 / 1;
    align-self: start;
    position: relative;
    top: var(--_y);
    translate: 0 -50%;
    white-space: nowrap;
  }

  .eq-line-chart-plot {
    position: relative;
    min-width: 0;
    container-type: inline-size;
  }

  .eq-line-chart-grid,
  .eq-line-chart-lines {
    position: absolute;
    inset: 0;
    display: block;
    width: 100%;
    height: 100%;
    overflow: visible;
  }

  .eq-line-chart-gridline {
    stroke: var(--eq-line);
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
  }

  .eq-line-chart-gridline[data-base] {
    stroke: var(--eq-line-strong);
  }

  /* The lines draw in from the left once, on the spring. */
  .eq-line-chart-lines {
    clip-path: inset(-8px);
    transition: clip-path var(--eq-dur-move) var(--eq-spring);
  }

  @starting-style {
    .eq-line-chart-lines {
      clip-path: inset(-8px 100% -8px -8px);
    }
  }

  .eq-line-chart-line {
    fill: none;
    stroke: var(--_tone, var(--eq-ink));
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
    stroke-dasharray: var(--_dash);
    vector-effect: non-scaling-stroke;
  }

  .eq-line-chart-empty {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    color: var(--eq-ink-3);
  }

  .eq-line-chart-cols {
    position: absolute;
    inset: 0;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .eq-line-chart-col {
    position: absolute;
    top: 0;
    bottom: 0;
    left: var(--_a);
    width: var(--_w);
    border-radius: var(--eq-radius-s);
    outline: none;
    cursor: default;
  }

  .eq-line-chart-col:focus-visible {
    z-index: 1;
    outline: 2px solid var(--eq-focus);
    outline-offset: 2px;
  }

  .eq-line-chart-guide {
    position: absolute;
    top: 0;
    bottom: 0;
    left: var(--_c);
    width: 1px;
    background: var(--eq-line-strong);
    opacity: 0;
    transition: opacity var(--eq-dur) var(--eq-ease);
  }

  .eq-line-chart-point {
    position: absolute;
    left: var(--_c);
    top: var(--_y);
    width: 8px;
    height: 8px;
    overflow: visible;
    translate: -50% -50%;
    scale: 0;
    fill: var(--_tone, var(--eq-ink));
    transition: scale var(--eq-dur-move) var(--eq-spring);
  }

  .eq-line-chart[data-points="shown"] .eq-line-chart-point {
    scale: 0.75;
  }

  .eq-line-chart-point[data-lone] {
    scale: 1;
  }

  /* The readout. Out of sight it is still text, so a screen reader reads it with its column. */
  .eq-line-chart-tip {
    position: absolute;
    top: 0;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .eq-line-chart-tip-label {
    display: block;
    margin-block-end: var(--eq-space-1);
    color: var(--eq-ink);
    font-weight: var(--eq-weight-strong);
  }

  .eq-line-chart-col:focus-visible .eq-line-chart-guide {
    opacity: 1;
  }

  .eq-line-chart-col:focus-visible .eq-line-chart-point {
    scale: 1;
  }

  .eq-line-chart-col:focus-visible .eq-line-chart-tip {
    z-index: 1;
    width: max-content;
    max-width: calc(50cqi - var(--eq-space-3));
    height: auto;
    margin: 0;
    padding: var(--eq-space-1) var(--eq-space-2);
    overflow: visible;
    clip-path: none;
    white-space: normal;
    border: 1px solid var(--eq-line-strong);
    border-radius: var(--eq-radius-m);
    background: var(--eq-raised);
    box-shadow: var(--eq-shadow);
    -webkit-backdrop-filter: var(--eq-blur);
    backdrop-filter: var(--eq-blur);
  }

  .eq-line-chart-col[data-side="start"]:focus-visible .eq-line-chart-tip {
    left: calc(var(--_c) + var(--eq-space-2));
  }

  .eq-line-chart-col[data-side="end"]:focus-visible .eq-line-chart-tip {
    right: calc(100% - var(--_c) + var(--eq-space-2));
  }

  @media (hover: hover) {
    .eq-line-chart-col:hover {
      z-index: 1;
    }

    .eq-line-chart-col:hover .eq-line-chart-guide {
      opacity: 1;
    }

    .eq-line-chart-col:hover .eq-line-chart-point {
      scale: 1;
    }

    .eq-line-chart-col:hover .eq-line-chart-tip {
      z-index: 1;
      width: max-content;
      max-width: calc(50cqi - var(--eq-space-3));
      height: auto;
      margin: 0;
      padding: var(--eq-space-1) var(--eq-space-2);
      overflow: visible;
      clip-path: none;
      white-space: normal;
      border: 1px solid var(--eq-line-strong);
      border-radius: var(--eq-radius-m);
      background: var(--eq-raised);
      box-shadow: var(--eq-shadow);
      -webkit-backdrop-filter: var(--eq-blur);
      backdrop-filter: var(--eq-blur);
    }

    .eq-line-chart-col[data-side="start"]:hover .eq-line-chart-tip {
      left: calc(var(--_c) + var(--eq-space-2));
    }

    .eq-line-chart-col[data-side="end"]:hover .eq-line-chart-tip {
      right: calc(100% - var(--_c) + var(--eq-space-2));
    }
  }

  .eq-line-chart-x {
    position: relative;
    grid-column: 2;
    height: calc(var(--eq-text-xs) * 1.3);
    margin-block-start: var(--eq-space-2);
    color: var(--eq-ink-3);
  }

  .eq-line-chart-xtick {
    position: absolute;
    left: var(--_x);
    translate: -50% 0;
    white-space: nowrap;
  }

  .eq-line-chart-xtick[data-edge="start"] {
    translate: 0 0;
  }

  .eq-line-chart-xtick[data-edge="end"] {
    translate: -100% 0;
  }

  /* On a narrow chart every other label steps aside so the rest do not collide. */
  @container (max-width: 260px) {
    .eq-line-chart-xtick[data-minor] {
      display: none;
    }
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index