Candlestick / trading charts for Compose Multiplatform, drawn directly on a native Compose
Canvas — no WebView, no JS bridge, no platform chart library underneath.
A wick is the thin line above or below a candle's body, marking the bar's high and low.
- Candlesticks + volume band from any
List<Candle>— implement the smallCandleinterface on your own bar type (zero-copy), or use the bundledOhlcvCandle - Line overlays for host-computed indicators — VWAP, moving averages, band edges
(
overlays: List<LineOverlay>; per-line colour, stroke width, and optional legend label) - Exact-price trade entry/exit markers (
PriceMarkerdiamonds) - Crosshair with OHLCV legend, price axis, adaptive time axis (intraday
HH:MM/ dailyMM-DD) - Cursor-anchored scroll zoom and drag pan, clamped to the data
ChartColors.Dark/ChartColors.Lightpresets, fully customizable- Trade-centric initial framing via
ChartInitialView(calendar or intraday modes) - Pure, unit-tested pan/zoom/viewport math; the renderer is exercised by a Skia screenshot harness
wickplot renders what you give it — the Lightweight-Charts philosophy. Indicator values (VWAP,
moving averages, …) are computed by your app and passed in as plain LineOverlays; the library
never computes market math.
Targets: JVM/Desktop, iOS (arm64, simulatorArm64), wasmJs. License: Apache-2.0.
commonMain.dependencies {
implementation("io.github.earlisreal:wickplot:0.1.2")
}val bars: List<Candle> = listOf(
OhlcvCandle(LocalDateTime(2026, 7, 1, 9, 30), open = 100.0, high = 103.5, low = 99.2, close = 102.8, volume = 120_000),
// ...
)
CandlestickCanvasChart(
bars = bars,
markers = listOf(PriceMarker(barIndex = 12, price = 101.25, isBuy = true)),
title = "ACME · D",
colors = ChartColors.Light, // default; or ChartColors.Dark
overlays = emptyList(), // indicator lines — see "Drawing indicators" below
intraday = false, // switches the time-axis format
)Drag to pan, scroll to zoom (anchored at the cursor), hover for a crosshair with the bar's OHLCV.
The repository includes an interactive desktop preview with the same sample data and gestures as the chart. From the repository root, run:
./gradlew.bat runSample # light theme (default)
./gradlew.bat runSample -Ptheme=dark # dark themeThe theme property accepts only light or dark. The published CandlestickCanvasChart API
also defaults to ChartColors.Light; pass colors = ChartColors.Dark when an application needs
the dark palette.
Light theme:
The highlighted date tag will follow the selected candle on the bottom axis. Its default format
is Sun, Sep 20, 2026 18:56:40, using English names and a 24-hour clock. When every supplied
candle is exactly midnight, the default omits the time: Sun, Sep 20, 2026. Otherwise, when every
candle has zero seconds and nanoseconds, it omits seconds: Sun, Sep 20, 2026 18:56.
If any candle has nonzero seconds or nanoseconds, all date tags include seconds (including :00);
fractional seconds are not printed. Detection uses the entire supplied dataset, so hovering or
panning does not switch precision.
Pass crosshairTimeFormatter to customize the full tag text. For example, this callback always
shows 2026-09-20 18:56, regardless of the data's seconds:
CandlestickCanvasChart(
bars = bars,
markers = emptyList(),
title = "ACME",
crosshairTimeFormatter = { timestamp ->
val hour = timestamp.hour.toString().padStart(2, '0')
val minute = timestamp.minute.toString().padStart(2, '0')
"${timestamp.date} $hour:$minute"
},
)The optional parameter is crosshairTimeFormatter: ((LocalDateTime) -> String)? = null on both
CandlestickCanvasChart and drawCandlestickChart. Omit it or pass null to use the default.
The callback receives the selected candle's kotlinx.datetime.LocalDateTime as supplied, with no
timezone conversion, and controls only the crosshair date tag. Regular axis ticks still use
intraday. The tag stays within the plot edges and uses a single-line ellipsis if it cannot fit.
LocalDateTime cannot distinguish omitted seconds from explicit :00. A dataset containing
only minute-aligned timestamps therefore omits seconds by default, even if the host considers
it a 10s timeframe. The same limitation applies to midnight-only data: it is treated as date-only.
Use the callback when the host needs a fixed precision or a different locale.
Your app computes the values, wickplot draws the lines: anything of shape
List<Candle> → List<LinePoint> plugs straight into overlays.
// A simple moving average — a few lines of your code.
fun sma(bars: List<Candle>, period: Int): List<LinePoint> =
((period - 1) until bars.size).map { i ->
LinePoint(barIndex = i, value = ((i - period + 1)..i).sumOf { bars[it].close } / period)
}
// Session VWAP: cumulative typical price × volume, reset at each new trading day.
fun sessionVwap(bars: List<Candle>): List<LinePoint> {
var tpv = 0.0
var vol = 0.0
var day: LocalDate? = null
return bars.mapIndexedNotNull { i, b ->
if (b.timestamp.date != day) { tpv = 0.0; vol = 0.0; day = b.timestamp.date }
tpv += (b.high + b.low + b.close) / 3.0 * b.volume
vol += b.volume
if (vol > 0.0) LinePoint(barIndex = i, value = tpv / vol) else null
}
}
CandlestickCanvasChart(
bars = bars,
markers = emptyList(),
title = "ACME · 1m",
overlays = listOf(
LineOverlay(points = sessionVwap(bars), label = "VWAP"), // no color → ChartColors.overlay
LineOverlay(points = sma(bars, 20), label = "SMA 20", color = Color(0xFF64B5F6)),
LineOverlay(points = sma(bars, 50), color = Color(0xFFFFB74D), strokeWidth = 2f),
),
intraday = true,
)Overlays with a label show their value at the hovered bar in the crosshair legend, in the
overlay's colour; unlabeled overlays are drawn but stay out of the legend. Multi-line indicators
(Bollinger, Keltner) are just several overlays — one per band edge.
Vico is a great general-purpose Compose chart engine and has a candlestick layer, but no built-in trading overlays. wickplot is purpose-built for trade analysis: volume, indicator line overlays, and exact-price trade markers out of the box, with indicator panes on the roadmap.
- Stacked sub-panes sharing the X axis (for host-computed RSI / MACD / volume studies)
- Android target

