Search code examples
swiftswiftuilabelswiftui-charts

How to Add Labels on a LineMark Chart in SwiftUI


I'm creating a chart in SwiftUI. It's weather related and I have the data successfully displaying. Now, I'm attempting to display the temperature on each symbol in the chart, but it only displays on the first symbol.

Here's what I've done:

  Chart {
     ForEach(temperatures, id: \.tempType) { series in
      ForEach(series.data) { element in
         LineMark(x: .value("Day", element.day), y: .value("Temps", element.temp))
             .interpolationMethod(.catmullRom)
             .foregroundStyle(by: .value("TemperatureType", series.tempType))
              .symbol {
                    Circle()
                       .fill(.yellow)
                       .frame(width: 10)
                       .shadow(radius: 2)
                       }
                       .lineStyle(.init(lineWidth: 5))
                                
        } // end for each
      } // end for each
 }

SwiftUI chart with LineMark

This works. Then, I attempt to add text using this modifier on the LineMark:

.annotation(position: .overlay, alignment: .bottom) {
                                    let text = "\(element.temp)"
                                    Text(text)
                                 }

It only displays the text on the first symbol's data point:

SwiftUI chart with LineMark with text at first data point

Since the annotation modifier is within the ForEach loop, I thought it would display the temperature at each data point, but it doesn't. What's the best way to have the temperature displayed at each symbol instead of only the first?


Solution

  • The short answer is that the .annotation refers to the type of "Mark" that you attach it to - and you are attaching it to a LineMark, so it is the entire line you are "annotating", not the individual points.

    Had you used BarMarks or PointMarks, the annotation will attach to the individual bar or point. So try this instead:

        Chart {
            ForEach(Array(zip(numbers, numbers.indices)), id: \.0) { number, index in
                LineMark(
                    x: .value("Index", index),
                    y: .value("Value", number)
                )
                .lineStyle(.init(lineWidth: 5))
    
                PointMark(
                    x: .value("Index", index),
                    y: .value("Value", number)
                    )
                .annotation(position: .overlay,
                            alignment: .bottom,
                            spacing: 10) {
                    Text("\(number)")
                        .font(.largeTitle)
                }
            }
        }
    

    enter image description here

    To make it compatible with your nice symbols, we need to add a couple of extra steps:

        Chart {
            ForEach(Array(zip(numbers, numbers.indices)), id: \.0) { number, index in
                 
                LineMark(
                    x: .value("Index", index),
                    y: .value("Value", number)
                )
                .interpolationMethod(.catmullRom)
                .lineStyle(.init(lineWidth: 5))
                .symbol {
                    // This still needs to be associated
                    // with the LineMark
                    Circle()
                        .fill(.yellow)
                        .frame(width: 10)
                        .shadow(radius: 2)
                }
                
                PointMark(
                    x: .value("Index", index),
                    y: .value("Value", number)
                    )
                // We need .opacity(0) or it will
                // overlay your `.symbol`
                .opacity(0)
                .annotation(position: .overlay,
                            alignment: .bottom,
                            spacing: 10) {
                    Text("\(number)")
                        .font(.largeTitle)
                }
            }
        }
    

    enter image description here