Search code examples
swiftios-charts

IOS Charts positioning of gridlines


I am struggling to find out how to set gridlines on my chart to specific points on the x axis and to centre labels on the lines corresponding to the time of day. I have a simple chart, the data represents a day with about 280 data points (one for every 5 minute period). I want to show gridlines at times of the day equal to 03:00, 06:00, 12:00, 15:00, 18:00 and 21:00 and show the corresponding labels centred below the gridlines. I have have also tried LimitLines, which works for placing the vertical lines at the correct points, but there does not seem to be the ability to align the time labels with the centre of the limit lines.

I know this can be done because I have seen screenshots on-line showing time labels centred on gridlines (or limitlines) for specific periods on the day, but I've spent hours looking for how to do this an I'm drawing a blank.

My code below (note the commented out failed attempt to get limitlines to work).

let tideHeights = LineChartDataSet(entries: dataEntries)
    tideHeights.drawCirclesEnabled = false
    tideHeights.colors = [NSUIColor.black]

    //let timings = ["00:00", "03:00", "06:00", "12:00", "15:00", "18:00", "21:00"]

    let data = LineChartData(dataSet: tideHeights)

    lineChartView.xAxis.valueFormatter = IndexAxisValueFormatter(values: dataPoints)

    //lineChartView.xAxis.drawGridLinesEnabled = false
    //let gridLine = values.count / timings.count
    //var xPos = 0

    //for label in timings {
        //let limitLine = ChartLimitLine(limit: Double(xPos), label: label)
        //limitLine.labelPosition = .bottomLeft
        //limitLine.lineWidth = 0.5
        //limitLine.lineColor = .black
        //limitLine.valueTextColor = .black
        //lineChartView.xAxis.addLimitLine(limitLine)
        //xPos += gridLine

    //}

    lineChartView.xAxis.labelCount = 7
    lineChartView.xAxis.labelPosition = .bottom

    lineChartView.xAxis.granularity = 1
    lineChartView.xAxis.avoidFirstLastClippingEnabled = true
    lineChartView.legend.enabled = false
    lineChartView.data = data

The output is below, the gridlines are at system set positions, how can I control where they are placed? iPhone screenshot

Update:

After applying the very helpful suggestion by Stephan Schlecht, I am still getting strange results. With lineChartView.xAxis.setLabelCount(9, force: true) I get:

enter image description here

and with lineChartView.xAxis.setLabelCount(4, force: true) I get:

enter image description here

It seems for some reason that the 1st gridline after the Axis is ignored.

Code is:

lineChartView.xAxis.valueFormatter = IndexAxisValueFormatter(values: dataPoints)

    let marker = ChartMarker()
    marker.setMinuteInterval(interval: 5)
    marker.chartView = lineChartView
    lineChartView.marker = marker

    lineChartView.xAxis.setLabelCount(4, force: true)
    lineChartView.xAxis.avoidFirstLastClippingEnabled = true
    lineChartView.xAxis.labelPosition = .bottom
    lineChartView.leftAxis.labelPosition = .insideChart
    lineChartView.rightAxis.drawLabelsEnabled = false
    lineChartView.legend.enabled = false

    lineChartView.data = data
    lineChartView.data?.highlightEnabled = true

Update 2

To reproduce, I have the following class which creates the data (btw, it is a stub class, eventually it will contain a paid api, so for now I generate the data for test purposes:

class TideHeights {
var tideHeights = [Double]()
var tideTimes = [Date]()
var strTimes = [String]()
var intervalMins = 5

init (intervalMins: Int) {
    self.intervalMins = intervalMins
    let slots = 24 * 60.0 / Double(intervalMins)
    let seconds = 24 * 60 * 60.0 / slots
    let radians = 2 * Double.pi / slots
    let endDate = Date().endOfDay

    var date = Date().startOfDay
    var radianOffset = 0.0


    repeat {
        tideHeights.append(sin(radianOffset) + 1)
        tideTimes.append(date)

        let timeFormatter = DateFormatter()
        timeFormatter.dateFormat = "HH:mm"
        strTimes.append(timeFormatter.string(from: date))

        date = date.advanced(by: seconds)
        radianOffset += radians

    } while date <= endDate   
}  

}

this class is instantiated and used to draw the graph in the following two functions in ViewController.

func drawChart() -> Void {

    let tideData = TideHeights(intervalMins: 5)

    lineChartView.dragEnabled = true
    lineChartView.setScaleEnabled(false)
    lineChartView.pinchZoomEnabled = false

    setChartValues(dataPoints: tideData.strTimes, values: tideData.tideHeights)

}

func setChartValues(dataPoints: [String], values: [Double]) -> Void {

    //initialise and load array of chart data entries for drawing
    var dataEntries: [ChartDataEntry] = []
    for i in 0..<values.count {
        let dataEntry = ChartDataEntry(x: Double(i), y: values[i])
        dataEntries.append(dataEntry)
    }

    let tideHeights = LineChartDataSet(entries: dataEntries)
    tideHeights.drawCirclesEnabled = false
    tideHeights.colors = [NSUIColor.black]

    let gradientColors = [ChartColorTemplates.colorFromString("#72E700").cgColor,
                              ChartColorTemplates.colorFromString("#724BEB").cgColor]

    let gradient = CGGradient(colorsSpace: nil, colors: gradientColors as CFArray, locations: nil)!

    tideHeights.fillAlpha = 0.7
    tideHeights.fill = Fill(linearGradient: gradient, angle: 90)
    tideHeights.drawFilledEnabled = true

    let data = LineChartData(dataSet: tideHeights)

    lineChartView.xAxis.valueFormatter = IndexAxisValueFormatter(values: dataPoints)

    let marker = ChartMarker()
    marker.setMinuteInterval(interval: 5)
    marker.chartView = lineChartView
    lineChartView.marker = marker

    lineChartView.xAxis.setLabelCount(4, force: true)
    lineChartView.xAxis.avoidFirstLastClippingEnabled = true
    lineChartView.xAxis.labelPosition = .bottom
    lineChartView.leftAxis.labelPosition = .insideChart
    lineChartView.rightAxis.drawLabelsEnabled = false
    lineChartView.legend.enabled = false

    lineChartView.data = data
    lineChartView.data?.highlightEnabled = true

}

Solution

  • The documentation says:

    setLabelCount(int count, boolean force): Sets the number of labels for the y-axis. Be aware that this number is not fixed (if force == false) and can only be approximated. If force is enabled (true), then the exact specified label-count is drawn – this can lead to uneven numbers on the axis.

    see https://weeklycoding.com/mpandroidchart-documentation/axis-general/

    Remark: although the y-axis is mentioned in the documentation of the function, it can actually be applied to both axes.

    So instead of

    lineChartView.xAxis.labelCount = 7
    

    use

    lineChartView.xAxis.setLabelCount(9, force: true)
    

    Note: you need 9 label counts not 7.

    Test

    If one combines a sine wave of 288 points with your code above

    for i in 0...288 {
        let val = sin(Double(i) * 2.0 * Double.pi / 288.0) + 1
        dataEntries.append(ChartDataEntry(x: Double(i), y: val))
    }
    

    and forces the label count = 9 as shown, then it looks like this:

    forced label count screenshot