Swift Charts has no candlestick mark, but marks compose: several marks inside one ForEach draw as one glyph per data point. Wrap the composition in a ChartContent type and it becomes a reusable mark of your own.
Composition Inside the Chart
A line with visible points is already composition: two marks per data point.
Chart {
ForEach(Array(numbers.enumerated()), id: \.element) { index, number in
LineMark(x: .value("index", index), y: .value("value", number))
PointMark(x: .value("index", index), y: .value("value", number))
}
}
A Candlestick From Two RectangleMarks
RectangleMark takes an x value plus yStart and yEnd, and a width in points. A thin rectangle spans low to high, a wide one spans open to close.
Chart {
ForEach(Array(zip(candles.indices, candles)), id: \.1) { index, candle in
RectangleMark(
x: .value("index", index),
yStart: .value("low", candle.low),
yEnd: .value("high", candle.high),
width: 4
)
RectangleMark(
x: .value("index", index),
yStart: .value("open", candle.open),
yEnd: .value("close", candle.close),
width: 16
)
.foregroundStyle(.red)
}
}
Extract a Mark With ChartContent
ChartContent is to marks what View is to views: one body requirement returning some ChartContent. Take PlottableValue parameters to match the built-in marks' API.
struct CandlestickMark<X: Plottable, Y: Plottable>: ChartContent {
let x: PlottableValue<X>
let low: PlottableValue<Y>
let high: PlottableValue<Y>
let open: PlottableValue<Y>
let close: PlottableValue<Y>
var body: some ChartContent {
RectangleMark(x: x, yStart: low, yEnd: high, width: 4)
RectangleMark(x: x, yStart: open, yEnd: close, width: 16)
}
}
The custom mark inherits every standard mark modifier, so callers style it like a built-in.
Chart {
ForEach(0...10, id: \.self) { index in
CandlestickMark(
x: .value("index", index),
low: .value("low", 1),
high: .value("high", 9),
open: .value("open", 3),
close: .value("close", 7)
)
.foregroundStyle(.green)
}
}