PhoenixKitWeb.Components.Core.Chart (phoenix_kit v2.55.1)

Copy Markdown View Source

Server-rendered SVG chart primitives — no JavaScript, no external libraries. Charts re-render whenever LiveView assigns change, so they are live by construction. (They snap rather than tween: SVG geometry attributes are not CSS-transitionable.)

All charts draw in currentColor: set a text colour on the component (class="text-primary") and stroke, fill and gradient all follow it in light and dark themes.

Sizing

Each chart fills its container. The SVG uses preserveAspectRatio="none", so it stretches to whatever box you give it while strokes stay crisp via vector-effect="non-scaling-stroke". Give the wrapper a height — otherwise you inherit the viewBox's intrinsic ratio, which is rarely what you want:

<div class="h-48 w-full">
  <.line_chart id="p" data={@points} class="text-primary" />
</div>

Heavy stretching still distorts anything measured in user space: dash patterns and the bars' rounded corners. Strokes are protected; those are not.

Data

Values are plain numbers — callers map their domain (time, money, watts) to x/y first, which is what keeps these primitives generic: a price curve, a CPU graph and a signup funnel are all just {x, y} pairs.

Points may be {x, y} tuples or %{x: x, y: y} maps, and Decimal values are converted automatically (money and metrics arrive that way from Ecto). Points that aren't numeric are dropped rather than crashing the page; if nothing usable is left, the :empty slot renders.

Series are sorted by x, so out-of-order data plots correctly instead of zigzagging.

Origin: extracted from NordSwitch's live price charts; also intended as the drawing layer for phoenix_kit_dashboards widgets.

Summary

Functions

A vertical bar chart, measured from a zero baseline.

A line chart for a numeric series, optionally filled.

A minimal inline sparkline for a list of values.

Functions

bar_chart(assigns)

A vertical bar chart, measured from a zero baseline.

Examples

<.bar_chart
  id="daily-kwh"
  data={Enum.map(@days, &%{label: &1.date, value: &1.kwh})}
  aria_label="Daily consumption"
  class="text-secondary"
/>

Bars carry a native <title>, so hovering one shows its label and value with no JS. Negative values hang below the baseline instead of disappearing. A datum may set class: to colour one bar differently (%{label: "Fri", value: 22, class: "text-error"}).

Attributes

  • id (:string) (required) - Unique id (applied to the SVG element).
  • data (:list) (required) - List of %{label: term, value: number, class: optional_string}.
  • width (:integer) - Defaults to 960.
  • height (:integer) - Defaults to 200.
  • show_labels (:boolean) - Render labels beneath the bars. Defaults to false.
  • baseline (:boolean) - Draw the zero line when any value is negative (its position is not otherwise implied). Defaults to true.
  • class (:any) - Classes for the wrapper (set text-* for colour). Defaults to nil.
  • aria_label (:string) - Accessible name for the chart. Defaults to nil.
  • value_format (:any) - 1-arity fun formatting a value for its tooltip. No generic chart can infer units or locale, so pass your own. It receives the NUMBER, not your original struct — non-numeric values are dropped before this runs — so write &"$#{&1}", not &Money.to_string/1. Defaults to a compact numeric rendering. Defaults to nil.
  • Global attributes are accepted. Extra attributes for the wrapper.

Slots

  • empty - Rendered instead of the SVG when there is no usable data.

line_chart(assigns)

A line chart for a numeric series, optionally filled.

Examples

<%!-- a day of prices: x = minute of day, y = EUR/MWh --%>
<.line_chart
  id="price-day"
  data={Enum.map(@slots, &{minute_of_day(&1.starts_at), &1.eur_mwh})}
  x_domain={{0, 1440}}
  step
  marker_x={minute_of_day(@now)}
  aria_label="Electricity price today"
  class="text-primary"
/>

<%!-- a bare line, auto domains --%>
<.line_chart id="signups" data={@signups} area={false} />

<%!-- hover readout: native tooltips, no JavaScript --%>
<.line_chart
  id="price-today"
  data={Enum.map(@slots, &{minute_of_day(&1.starts_at), &1.eur_kwh})}
  x_domain={{0, 1440}}
  step
  hover
  value_format={&"€#{&1}/kWh"}
  x_format={&clock_label/1}
/>

Crosshair readout

hover={:crosshair} swaps the native tooltips for a crosshair that snaps to the datum under the pointer (the slot, with step) and a readout beside it: the x (x_format), the value (value_format), an optional note of your own (point_note, which is handed the point's rank in the series), and which rows are active at that x — the pairing chart_lanes/1 implies:

<.line_chart
  id="price-today"
  data={@points}
  x_domain={{0, 1440}}
  step
  hover={:crosshair}
  value_format={&"€#{&1}/kWh"}
  x_format={&clock_label/1}
  point_note={&"#{ordinal(&1.rank)} cheapest of #{&1.count}"}
  rows={[%{label: "Boiler", color: "var(--color-warning)", bands: [{360, 420}]}]}
/>

Everything the readout says is computed here, on the server; the PkChartCrosshair hook (shipped in phoenix_kit.js) only places it. The native per-point tooltips are still rendered underneath, so a page whose JavaScript has not loaded keeps the plain readout; the hook turns them off when it mounts.

The chart takes focus: ArrowLeft / ArrowRight step through the points, Home / End jump to the ends, Escape hides the readout, and each reading is announced through a polite live region. On a touch screen a tap shows the readout and leaves it up until a tap elsewhere; a vertical swipe still scrolls the page, a horizontal drag scrubs.

x_format gets the plotted NUMBER, never the struct it came from — a time axis formats back from the number it plotted (clock_label(810) → "13:30" here), since a DateTime x is not a point and is dropped.

With step each point holds its y until the next x (right-open steps — the correct reading for slot/interval data like prices). marker_x draws a dashed vertical "now" line. Domains default to the data's min/max, y padded by 10%; a reversed domain is normalised.

Attributes

  • id (:string) (required) - Unique id (namespaces the SVG gradient defs).
  • data (:list) (required) - Points as {x, y} tuples or %{x: , y: } maps. Non-numeric points are dropped.
  • step (:boolean) - Step interpolation (interval data). Defaults to false.
  • area (:boolean) - Fill a gradient area under the line. Defaults to true.
  • x_domain (:any) - {min, max} for x, or nil to fit data. Defaults to nil.
  • y_domain (:any) - {min, max} for y, or nil to fit data (10% padded). Defaults to nil.
  • marker_x (:any) - x position for a dashed vertical marker, or nil. Defaults to nil.
  • y_invert (:boolean) - Flip the y axis so SMALLER values plot higher. For rank-like data (search position, race place, leaderboard) where 1 is the best result and belongs at the top. Without it callers had to negate their own values, which flips the sign of every domain and marker they pass too. Defaults to false.
  • hover (:any) - true (or :native) adds a hover readout: an invisible band per data point, spanning the stretch of x the point stands for (its step with step, otherwise half-way to each neighbour), with a native tooltip. No JavaScript. Each band also carries data-x / data-y with the raw values, so a host's own hook (a crosshair, a richer popover) can snap to points without reverse-engineering the stretched SVG. :crosshair adds a snapping crosshair and a richer readout on top (see "Crosshair readout"). Defaults to false. Must be one of false, true, :native, or :crosshair.
  • point_note (:any) - With hover={:crosshair}: 1-arity fun given %{x, y, index, rank, count} returning an extra line for the readout (or nil). rank is the point's 1-based place with the series sorted ascending by y (ties share a place), count the number of points, index its position by x. Defaults to nil.
  • rows (:list) - With hover={:crosshair}: things that run over stretches of x, as %{label: _, color: _, bands: [{from, to}]} (bands right-open, in the same units as x; color any CSS colour). The readout lists the rows active at the hovered x, in the order given. Defaults to [].
  • value_format (:any) - With hover: 1-arity fun formatting a y value for the tooltip. It receives the NUMBER. Defaults to a compact numeric rendering, as in bar_chart/1. Defaults to nil.
  • x_format (:any) - With hover: 1-arity fun formatting an x value; when given, the tooltip reads "<x>: <y>" (a time slot and its price, say). Like value_format it receives the NUMBER that was plotted — map it back yourself (minutes of the day to a clock time, say). Defaults to nil.
  • width (:integer) - viewBox width (the SVG scales to its container). Defaults to 960.
  • height (:integer) - viewBox height. Defaults to 240.
  • gridlines (:integer) - Number of horizontal gridlines (0 disables). Defaults to 3.
  • class (:any) - Classes for the wrapper (set text-* for colour). Defaults to nil.
  • aria_label (:string) - Accessible name. role="img" without one is an unlabelled graphic to a screen reader; pass what the chart shows. Unlabelled charts are marked decorative. Defaults to nil.
  • Global attributes are accepted. Extra attributes for the wrapper.

Slots

  • empty - Rendered instead of the SVG when there is no usable data.

sparkline(assigns)

A minimal inline sparkline for a list of values.

Examples

<.sparkline values={Enum.map(@upcoming, & &1.eur_mwh)} class="w-full h-12 text-info" />

A single value draws a flat line rather than nothing — one sample is a real state, and an empty box reads as a rendering bug.

Attributes

  • values (:list) (required) - Numbers, in order. Non-numeric values are dropped.
  • width (:integer) - viewBox width. Defaults to 200.
  • height (:integer) - viewBox height. Defaults to 48.
  • y_invert (:boolean) - Flip the y axis so SMALLER values plot higher — rank-like data. Defaults to false.
  • class (:any) - Classes for the wrapper (set text-* for colour). Defaults to nil.
  • aria_label (:string) - Accessible name. Left unset the sparkline is marked decorative. Defaults to nil.
  • Global attributes are accepted. Extra attributes for the wrapper.

Slots

  • empty - Rendered instead of the SVG when there is no usable data.