Give every series in a chart the same contrast, so no series looks more important than another. Take one family per series from a harmony, and draw each with its foregroundTertiary token:
import Charts
struct Booking: Identifiable {
let id = UUID()
let month: String
let kind: String
let count: Int
}
struct BookingsChart: View {
let bookings: [Booking]
let kinds = ["Flights", "Hotels", "Tours"]
let colors = brand.triad.map(\.foregroundTertiary) // _700 in light mode, _300 in dark
var body: some View {
Chart(bookings) { booking in
BarMark(
x: .value("Month", booking.month),
y: .value("Bookings", booking.count)
)
.foregroundStyle(by: .value("Kind", booking.kind))
.position(by: .value("Kind", booking.kind))
}
.chartForegroundStyleScale(domain: kinds, range: colors)
.padding()
.background(Color.backgroundPrimary) // white in light mode, black in dark
}
}foregroundStyle(by:) names each series, and chartForegroundStyleScale(domain:range:) maps the names to your colors in order.
Contrast on every background#
Lines, bars and points need 3:1 against the chart's background, under WCAG's non-text contrast rule, 1.4.11. foregroundTertiary clears it on every background token, in both modes and every hue. On the white and black page above, each series measures at least 6.66:1.
A fixed stop doesn't follow the mode. _600 passes on white, but on a dark backgroundSecondary card it falls to 2.99:1.
Spread the hues for more series#
Use triad for three series and square for four. For any other count, analogous(count:spread:) with 360° divided by the count spaces the hues evenly around the wheel:
let colors = brand.analogous(count: 5, spread: .degrees(72)).map(\.foregroundTertiary)Each series you add brings the closest pair nearer, so past five or six, label the series directly or group the smaller ones.
For data that runs from low to high, such as bookings per day, use a lightness set instead. Its steps differ in lightness, so they also read in grayscale.
Add a second cue besides color#
Series drawn with one token differ only in hue, so they match in grayscale, and some look alike to people with a color vision deficiency. Simulated for deuteranopia (Machado and colleagues, 2009), the chart's Flights and Tours colors in light mode drop from 0.195 apart in OKLab to 0.078. Change "See as" to compare:
Every OKLCH hue at this color's lightness
let color = Color.proBlue._400.toColor()
color.triadSo give each series a second cue. In a line chart, symbol(by:) gives each series its own point shape. In a bar chart, label the bars directly. Separate stacked bars and pie slices with a thin line in the background color. WCAG's use of color rule, 1.4.1, asks for this as well.
See also
- .contrastRatio(to:method:) Check WCAG 2 and APCA contrast in Swift, and keep your color pairs passing with a unit test.
- .harmony(_:) Build the harmony someone picks at runtime, from a picker or a setting, in one call.
- What does equal lightness cost? What equal contrast costs, from quieter reds to olive yellows, and what you can do about each.
- How accessible is it? What the palette guarantees for contrast in every hue and mode, and what you still need to check.