Skip to main content

Trading-time axis

A trading-time axis is still a time axis — but it renders in trading time: closed-market spans (overnight, weekends, holidays) collapse out, and pixels are proportional to time within each session. A gap-free intraday candle chart, no dead space between sessions.

src/examples/financial-calendar-chart.tsx
import {
Candlestick,
ChartContainer,
ChartRow,
Layers,
YAxis,
} from '@pond-ts/charts';
import { useSiteChartTheme } from '@site/src/theme/useSiteChartTheme';
import { marketBars, sessionWindow } from './lib/financial-fixtures';

export default function FinancialCalendarChart() {
const theme = useSiteChartTheme();
const set = marketBars();
const { range, bars } = sessionWindow(set, 30);

return (
<ChartContainer
range={range}
width={560}
theme={theme}
calendar={set.calendar}
cursor="crosshair"
>
<ChartRow height={220}>
<YAxis id="price" side="right" format="$,.0f" width={50} />
<Layers>
<Candlestick series={bars} as={set.symbol} showOHLC />
</Layers>
</ChartRow>
</ChartContainer>
);
}

For the tutorial framing see Learn charts, chapter 9; the full financial walkthrough is the @pond-ts/financial section.

Getting one — the calendar prop

Hand <ChartContainer> a calendar and the axis becomes session-aware. calendar is the high-level sugar: the container derives the gap model from it itself. A @pond-ts/financial TradingCalendar is the usual source:

import { TradingCalendar } from '@pond-ts/financial';

const cal = TradingCalendar.fromRules(
{ timeZone: 'America/New_York', open: '09:30', close: '16:00' },
{ from: '2026-01-05', to: '2026-02-13' },
);

<ChartContainer calendar={cal} range={view} width={560}>
<ChartRow height={220}>
<YAxis id="price" side="right" format="$,.0f" />
<Layers>
<Candlestick series={bars} />
</Layers>
</ChartRow>
</ChartContainer>;

TradingCalendar.fromRules(rules, range) is the minimal path — rules is a { timeZone, open, close } (plus optional weekmask, breaks, holidays, earlyCloses), and the second range is { from, to } (the calendar's own span). The chart's view range is a separate [start, end] you choose; cal.sessions() gives you the session bounds to derive it from. For irregular sessions, TradingCalendar.fromSessions(...) takes an explicit session list.

@pond-ts/charts never imports @pond-ts/financial. The calendar prop is typed against a structural TradingCalendarLike shape (anything with a .discontinuities() method), so the charts package stays free of the financial dependency — you supply the calendar.

Memoize the calendar

Build the calendar (or provider) once and pass a stable reference — the x scale and frame rebuild whenever its identity changes. Construct it in a useMemo/useRef, never inline in the render.

Tuning

All of these are <ChartContainer> props, and all are time-axis only — ignored on a value or category axis.

PropTypeDefaultPurpose
calendarTradingCalendarLikeHigh-level: the container derives the gap model from it.
spacing'proportional' | 'uniform''proportional'proportional = true time within a session; uniform = equal width per session (the TradingView ordinal look). Only with calendar.
discontinuitiesDiscontinuityProviderLow-level: pass the gap model yourself (calendar.discontinuities(...)). Takes precedence over calendar.
sessionDividers'none' | 'labeled' | 'all''none'Solid verticals at collapse seams (see below). 'all' = every seam (the TradingView separators), 'labeled' = only labelled seams.

Most charts want calendar + the default proportional spacing. Reach for discontinuities only when you're constructing the provider some other way, or need uniform bar-width spacing over a custom period. The dateStyle (on <XAxis>) and grid (on <ChartContainer>) props also shape this axis — see What changes on the axis below and Axes / Layout.

What changes on the axis

  • Gaps collapse. Closed-market spans have zero pixel width; the axis walks a session-aware tick ladder (hour → day → month → quarter → year, with the day grain thinned by a per-month uniform session stride — each month's first session, then every k-th session, truncated so the gap to the next month start stays ≥ the stride. Marks sit an equal number of sessions apart, so they're evenly spaced in pixels on the collapsed axis, with the slack at the month end — the TradingView behaviour, validated against its output) rather than raw wall-clock ticks.
  • Inline date context (the dateStyle prop). By default (dateStyle="flat") the axis is a single row the TradingView way: each tick that opens a coarser calendar period is relabelled inline to it — the month at a month turn, the year at a year turn, the date at a day turn under an intraday grain — while every other tick stays terse (5, Feb, 14:00); a promoted tick renders bold. Pass dateStyle="stacked" for the two-row layout: the same terse top row over a segmented band row of the next-coarser period — day bands under intraday ticks, month bands under day ticks, year bands under month/quarter ticks. Each band left-aligns its label with a divider at its turn, zebra-shaded by the band's calendar parity; the top-row turn tick joins its divider as one boundary line. Themeable via theme.axis.band.
  • Calendar grid. The vertical gridlines are the full grain populations — every day, month, and aligned clock instant in view, not just the labelled ticks (the labels decorate the grid; they don't define it). Each grain fades as a unit by its calendar density, so zooming out dissolves the fine grain (days) into the coarser one (months) with no pop when the label grain switches — a map-style hierarchical grid. Because the metric is calendar density rather than on-screen spacing, hiding weekends draws fewer day lines at the same weight, not brighter ones. grid={false} drops the grid for a clean backdrop. (This applies to any time axis, but is most visible here, where collapsed sessions pack the day grain tightly.)
  • Session dividers (the sessionDividers prop). Optional solid verticals at collapse seams — the boundaries where closed time was actually removed (on a real exchange calendar, every session open; on a contiguous-session calendar, only where days/hours were excised). Default 'none' — the calendar grid already marks the structure, so dividers are opt-in emphasis: 'all' draws one at every seam in view (the TradingView session-separator look, crowding lines fading out), 'labeled' only at the seams the axis also labels. Independent of gridsessionDividers="all" with grid={false} is separators on a clean plot.
  • Line breaks at sessions. A LineChart can set sessionBreaks to break the line at each session discontinuity instead of connecting across the collapsed gap. This is a scale break (driven by the axis's collapsed gaps), orthogonal to gaps (a data break) — set both independently.

See also