Skip to Content
Course content

218: Customizing Chart Marks and Axes

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first start playing with Swift Charts, it's tempting to just throw some data at a BarMark or LineMark and let the framework handle the rest. For a quick prototype, that's fine. But if you're building something that people actually have to use to make decisions, the defaults usually aren't enough. I've seen too many developers ship charts that are technically accurate but visually ambiguous because they relied on the "naive" way of letting the system decide the axis and the colors.

Let's look at a real scenario: a daily water intake tracker. You want to show the user how much they've drunk over the last week compared to a goal of 2,500ml. The naive approach is to just plot the values and let Swift Charts auto-scale the Y-axis. It looks like this:

Chart(waterData) { data in
    BarMark(
        x: .value("Day", data.day),
        y: .value("Amount", data.amount)
    )
}

The Problem with "Automatic" Everything

At first glance, this works. But here's where it breaks: if the user drinks 2,400ml every day, the Y-axis might cap exactly at 2,400. The user has no visual reference for their 2,500ml goal. They're staring at a chart where the bars hit the top, but they're still technically failing their goal. It's a classic case of the data being correct, but the communication being wrong.

To fix this, we need to stop treating the axis as a black box. Instead of letting the framework guess, we should explicitly define the scale and customize the marks. By using chartYScale(domain:), we can force the chart to always show up to 3,000ml, ensuring the 2,500ml goal is always visible within the frame. But just fixing the scale isn't enough; the user shouldn't have to squint at the axis to know if they hit their target.

Injecting Meaning into the Marks

The second mistake I see often is using a single, static color for all marks. In our water tracker, a blue bar is just a blue bar. It doesn't tell the user "Success" or "Try Harder." The better way is to tie the foregroundStyle directly to the data logic. I prefer doing this inline within the mark definition to keep the relationship between the value and the visual cue explicit.

Chart(waterData) { data in
    BarMark(
        x: .value("Day", data.day),
        y: .value("Amount", data.amount)
    )
    .foregroundStyle(data.amount >= 2500 ? .green : .blue)
}

Now we're actually communicating. But we can go further. If you want to be a real pro about it, you don't just want a green bar; you want a clear, labeled line marking that goal. This is where RuleMark comes in. Adding a RuleMark at the 2,500 value creates a visual "finish line" that anchors the entire chart.

Fine-Tuning the Axis for Humans

Finally, let's talk about the axis labels. The default labels are often too verbose or too sparse. If you're dealing with milliliters, you don't want the axis to say "2000.0" or "2k". You want it to feel like a health app. Using AxisMarks allows us to intercept the rendering process. I usually recommend using the value closure to format the numbers into something readable, like adding "ml" as a suffix only to the top-most label to reduce visual clutter.

.chartYAxis {
    AxisMarks(values: .automatic) { value in
        AxisGridLine()
        AxisTick()
        AxisValueLabel {
            if let amount = value.as(Double.self) {
                Text("\(Int(amount))ml")
            }
        }
    }
}

By moving from the naive "plot and pray" method to this intentional approach, you've changed the chart from a simple data visualization into a functional tool. You've controlled the scale to provide context, used color to provide instant feedback, and cleaned up the axes to remove friction for the user.




📋 Practical Task

Exercise: The Performance Threshold Dashboard

You are building a server monitoring tool. You have a dataset of "Response Times" (in milliseconds) for the last 10 requests. The critical threshold for a "Slow Response" is 500ms.

  • Create a Chart using BarMark to display the response times.
  • Custom Scaling: Ensure the Y-axis always extends to 1000ms, regardless of the highest data point.
  • Conditional Styling: Set the bar color to .red if the response time is 500ms or higher, and .gray if it is below.
  • The Goal Line: Add a RuleMark at the 500ms mark with a label that says "Critical Threshold".
  • Axis Customization: Customize the Y-axis labels so they append "ms" to the values.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.