Search code examples
iosswiftuicharts

Swift Charts: Draw A Line Chart Where Line Color is Determined by the Y-Value


I am trying to use Swift Charts to draw the below graph. I am having trouble figuring out how to change the color of the line based on it's Y value. In the graph below, different ranges of Y have different colors assigned to them and the line reflects that.

Desired Chart

I tried to set the line color using the foreground of the LineMark, but it seems to only take the first color assignment and ignore any others set on it.

Chart {
    ForEach(plotItemSeries) { plotItem in
        LineMark(x: .value("XVAL", plotItem.xVal),
                 y: .value("YVAL", plotItem.yVal))
        .foregroundStyle(plotItem.zoneColor) // <== color assigned base on y-val
    }
}

Attempt #1

I tried using a foreground scale but it just broke it up into multiple series like you would expect.

Chart {
    ForEach(plotItemSeries) { plotItem in
        LineMark(x: .value("XVAL", plotItem.xVal),
                 y: .value("YVAL", plotItem.yVal))
        .foregroundStyle(by: .value("Zone", plotItem.zoneColorName)) // <== "gray", "blue", etc..
    }
}
.chartForegroundStyleScale(["gray": Color("grayCustom"),
                            "green": Color("greenCustom"),
                            "blue": Color("blueCustom"),
                            "orange": Color("orangeCustom"),
                            "red": Color("redCustom")])

Attempt #2

Any ideas how to set the color of the line depending on it's y value?


Solution

  • It turns out .linearGradient(_ gradient: Gradient, startPoint: UnitPoint, endPoint: UnitPoint) -> LinearGradient is the correct approach for this. The Gradient.Stops are mapped to a UnitPoint which maps to the bounding box of the line drawn so the color boundary y-vals have to be converted to the unit point value which maps to the y-val of the color boundary.

    For a simple example, if we want to draw the line blue below y-val of 5 and green above, and the line is: (0,0) to (10,10) then the unit point value that corresponds to the transition point is 0.5.

    To get a crisp color transition we just define the gradient so that the portion where the color changes is too small to render:

    let stops = [
      Gradient.Stop(color: .red, location: 0.0),
      Gradient.Stop(color: .red, location: 0.5),
      Gradient.Stop(color: .green, location: 0.50001),
      Gradient.Stop(color: .green, location: 1.0)
    ]
    

    Thus the correct approach looks something like:

    Chart {
      ForEach(plotItemSeries) { plotItem in
          LineMark(x: .value("XVAL", plotItem.xVal),
                   y: .value("YVAL", plotItem.yVal))
          .foregroundStyle(.linearGradient(Gradient(stops: stops),
                                           startPoint: .bottom,
                                           endPoint: .top))
      }
    }
    

    The resulting graph line (using the original data) looks close to the original graph we were trying to match: Graph With SwiftCharts