Financial

Financial charts show prices and ranges over time, from daily candles to rolling bands of volatility.

LLMs

Code

Financial charts are drawn with TanStack Charts, through the kit's Chart components. They paint the chart with the theme's colors, grid and type, and add three palette props. The chart itself is a TanStack definition, written the way TanStack documents it.

Install

npx antuikit@latest init
npx antuikit@latest add tanstack-chart
The first command installs AntuikitProvider and the theme tokens. The second adds the chart components and TanStack Charts itself.

Your first candlestick

A candlestick is three link marks on a time scale: a thin wick from the low to the high for every session, then a thick body from the open to the close, one mark for the sessions that rose and one for those that fell.
import { defineChart, link } from "@tanstack/charts"
import { scaleLinear } from "@tanstack/charts/scales/linear"
import { tooltip } from "@tanstack/charts/tooltip"
import { scaleUtc } from "d3-scale"
import { Chart, chartColors } from "@/components/ui/tanstack-chart"
const sessions = [
{ date: "2013-05-13", open: 64.5, high: 65.41, low: 64.5, close: 64.96 },
{ date: "2013-05-14", open: 64.84, high: 65.03, low: 63.16, close: 63.41 },
{ date: "2013-05-15", open: 62.74, high: 63, low: 60.34, close: 61.26 },
].map((session) => ({ ...session, date: new Date(session.date) }))
const gains = sessions.filter((session) => session.close >= session.open)
const losses = sessions.filter((session) => session.close < session.open)
const definition = defineChart(
{
marks: [
link(sessions, {
x1: "date", y1: "low", x2: "date", y2: "high",
stroke: chartColors.label,
strokeWidth: 1,
}),
link(gains, {
x1: "date", y1: "open", x2: "date", y2: "close",
stroke: chartColors.positive,
strokeWidth: 5,
}),
link(losses, {
x1: "date", y1: "open", x2: "date", y2: "close",
stroke: chartColors.negative,
strokeWidth: 5,
}),
],
scales: {
x: { scale: scaleUtc },
y: { scale: scaleLinear, grid: true, axis: { label: "Price (USD)" } },
},
},
{
tooltip: {
use: tooltip,
content: ([point]) => ({
title: point?.datum.date.toLocaleDateString("en-US", { timeZone: "UTC" }) ?? "",
rows: [
{ label: "Open", value: `$${point?.datum.open}` },
{ label: "High", value: `$${point?.datum.high}` },
{ label: "Low", value: `$${point?.datum.low}` },
{ label: "Close", value: `$${point?.datum.close}` },
],
}),
},
},
)
export function PriceChart() {
return (
<Chart
definition={definition}
ariaLabel="Apple daily open, high, low and close prices, May 13 to 15, 2013"
height={260}
/>
)
}
The chart fills its container's width, so give it a height. ariaLabel is required. The time scale comes from d3-scale: add it with npm install d3-scale and npm install -D @types/d3-scale.

Which component

All three take the same definition and the same palette props.
Chart components

Time scale and trading days

Turn date strings into Date objects before you pass the rows, and pick the scale by how the gaps should look.
  • scaleUtc from d3-scale spaces the sessions by elapsed time, so weekends and holidays leave gaps, and the axis gets calendar ticks.
  • scaleBand<Date> places the sessions next to each other with no gaps. Format its ticks yourself, since a band scale has no calendar.
import { scaleBand } from "@tanstack/charts/scales/band"
const formatDay = (date: Date) =>
date.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" })
scales: {
y: {
scale: () => scaleBand<Date>().paddingInner(0.16),
axis: { ticks: { format: formatDay } },
},
}
Parse and format dates in UTC, as above, so a session does not move to the day before for readers west of Greenwich.

Open to close bars

A bar can start anywhere. Give barX an x1 and an x2 to draw each session from its open to its close, and set color to a direction the color scale maps to the kit's up and down colors.
import { barX, defineChart } from "@tanstack/charts"
defineChart({
marks: [
barX(sessions, {
x1: "open",
x2: "close",
y: "date",
color: (row) => (row.close >= row.open ? "Gain" : "Loss"),
radius: 3,
}),
],
color: {
domain: ["Gain", "Loss"],
range: [chartColors.positive, chartColors.negative],
},
})

Up and down colors

chartColors.positive and chartColors.negative are the theme's up and down colors. They follow the theme and dark mode, so use them in place of your own green and red.
  • Color alone does not tell every reader which way a session moved. Add the direction in text: a signed change in the tooltip, and a legend that names each color.
  • Use the same two colors for the tooltip row, the legend and any change tag next to the chart, so they mean the same thing everywhere.
import { Badge } from "@/components/ui/badge"
<Badge color={chartColors.positive} text="Gain" />
<Badge color={chartColors.negative} text="Loss" />

Range and percentile bands

areaY with y1 and y2 fills the space between a lower and an upper value at each date. Draw the middle, or both edges, with lineY on top.
import { areaY, lineY } from "@tanstack/charts"
marks: [
areaY(days, { x: "date", y1: "low", y2: "high", fill: chartColors.series[0], fillOpacity: 0.2 }),
lineY(days, { x: "date", y: "low", stroke: chartColors.series[0] }),
lineY(days, { x: "date", y: "high", stroke: chartColors.series[1] }),
]
For a percentile ribbon, reduce the rows of each date with groupBy and the quantile reducer, then draw the band from the low to the high percentile and the median as a line.
import { groupBy, quantile } from "@tanstack/charts"
const rows = groupBy(observations, {
by: "date",
outputs: {
lower: { value: "unemployed", reduce: quantile(0.1) },
median: { value: "unemployed", reduce: quantile(0.5) },
upper: { value: "unemployed", reduce: quantile(0.9) },
},
})

Moving averages and volatility

rollingWindow adds values computed over the last size rows to each row. With a mean and a deviation, it gives a Bollinger band: the mean, plus and minus two standard deviations.
import { areaY, deviation, lineY, rollingWindow } from "@tanstack/charts"
const rows = rollingWindow(sessions, {
size: 20,
orderBy: "date",
anchor: "end",
partial: false,
outputs: {
mean: { value: "close", reduce: "mean" },
spread: { value: "close", reduce: deviation },
},
})
marks: [
areaY(rows, {
x: "date",
y1: (row) => row.mean - row.spread * 2,
y2: (row) => row.mean + row.spread * 2,
fill: chartColors.series[3],
fillOpacity: 0.18,
}),
lineY(rows, { x: "date", y: "mean", stroke: chartColors.series[3] }),
]
partial: false drops the first 19 rows, whose window is not full yet, so the band does not start with a jump.

Colors

The chart takes the theme's chart colors without any setup. Paint up and down sessions with chartColors.positive and chartColors.negative, and bands and lines with a series color. The Color guideline covers which palette fits which data.
link(gains, { x1: "date", y1: "open", x2: "date", y2: "close", stroke: chartColors.positive, strokeWidth: 5 })
link(losses, { x1: "date", y1: "open", x2: "date", y2: "close", stroke: chartColors.negative, strokeWidth: 5 })
areaY(rows, { x: "date", y1: "lower", y2: "upper", fill: chartColors.series[5], fillOpacity: 0.22 })
Palette props
Taken by Chart, RendererChart and CanvasChart. Rows marked antuikit in the Version column are added by the kit.

Animation

Use RendererChart with TanStack's motion() renderer to animate marks in and between data updates. It skips the animation when the reader has asked for reduced motion.
import { motion } from "@tanstack/charts/motion"
import { RendererChart } from "@/components/ui/tanstack-chart"
const renderer = motion({ initial: "always" })
<RendererChart definition={definition} renderer={renderer} ariaLabel="..." />

API

The marks and transforms financial charts use most. TanStack Charts' documentation has the full reference.
areaY
rollingWindow