Maciej Kozieł Maciej Kozieł - 1 month ago 22
iOS Question

How to render only chosen axis labels in ios Charts

Library I'm using: charts

I have line chart with values at specific days in year. I don't want to draw each day as label on x axis (there may be more than 300 days range) so I'm looking to draw months only. Right now, my xVals looks like: [nil, nil, nil, "07/2016", nil nil nil (..)]. Unfortunately, no label are displayed.

Is there any other way to achieve desired behaviour?




Code (x axis values):


while date.compare(to) != NSComparisonResult.OrderedDescending { // iterate over all days in range
let components = currentCalendar.components([.Day, .Month , .Year], fromDate: date)
if components.day != 1 {
vals.append(nil)
} else { // first day of the month
vals.append(String(components.month) + " / " + String(components.year))
}

let incrementComponents = NSDateComponents()
incrementComponents.day = 1
date = currentCalendar.dateByAddingComponents(incrementComponents, toDate: date, options: [])!
}


And entries:

for point in base!.points {
if let index = chartValuesFormatter?.indexFromDate(point.date) {
entries.append(ChartDataEntry(value: point.value, xIndex: index))
}
}


Generally it works, when I specify all values some of them are displayed and entries are rendered correctly. My only problem is to display only month labels on the beginning of month periods.

Answer

Ok, so I've found solution. It's not as simple as is should (for such basic problem), but is very customizable. LineChartView (via BarLineChartViewBase) has property xAxisRenderer. You can subclass ChartXAxisRenderer which is default one, and override drawLabels function. In my case, I've copied original code and modified logic behind calculation what label and where should be drawn.

Edit: My custom renderer with overlap logic. Probably overcomplicated, but it works so far.

class ChartXAxisDateRenderer: ChartXAxisRenderer {
internal static let FDEG2RAD = CGFloat(M_PI / 180.0)

/// draws the x-labels on the specified y-position
override func drawLabels(context context: CGContext, pos: CGFloat, anchor: CGPoint) {

    guard let xAxis = xAxis else { return }

    let paraStyle = NSParagraphStyle.defaultParagraphStyle().mutableCopy() as! NSMutableParagraphStyle
    paraStyle.alignment = .Center

    let labelAttrs = [NSFontAttributeName: xAxis.labelFont,
                      NSForegroundColorAttributeName: xAxis.labelTextColor,
                      NSParagraphStyleAttributeName: paraStyle]
    let labelRotationAngleRadians = xAxis.labelRotationAngle * ChartXAxisDateRenderer.FDEG2RAD

    let valueToPixelMatrix = transformer.valueToPixelMatrix

    let minLabelsMargin: CGFloat = 4.0

    var position = CGPoint(x: 0.0, y: 0.0)

    var labelMaxSize = CGSize()

    if (xAxis.isWordWrapEnabled) {
        labelMaxSize.width = xAxis.wordWrapWidthPercent * valueToPixelMatrix.a
    }

    var positions = [CGPoint]()
    var widths = [CGFloat]()
    var labels = [String]()
    var originalIndices = [Int]()

    for i in 0...xAxis.values.count-1 {
        let label = xAxis.values[i]
        if (label == nil || label == "")
        {
            continue
        }

        originalIndices.append(i)
        labels.append(label!)

        position.x = CGFloat(i)
        position.y = 0.0
        position = CGPointApplyAffineTransform(position, valueToPixelMatrix)
        positions.append(position)

        let labelns = label! as NSString
        let width = labelns.boundingRectWithSize(labelMaxSize, options: .UsesLineFragmentOrigin, attributes: labelAttrs, context: nil).size.width
        widths.append(width)
    }

    let newIndices = findBestPositions(positions, widths: widths, margin: minLabelsMargin)

    for index in newIndices {
        let label = labels[index]
        let position = positions[index]
        let i = originalIndices[index]

        if (viewPortHandler.isInBoundsX(position.x)) {
            drawLabel(context: context, label: label, xIndex: i, x: position.x, y: pos, attributes: labelAttrs, constrainedToSize: labelMaxSize, anchor: anchor, angleRadians: labelRotationAngleRadians)
        }
    }
}

// Best position indices - minimum "n" without overlapping
private func findBestPositions(positions: [CGPoint], widths: [CGFloat], margin: CGFloat) -> [Int] {
    var n = 1
    var overlap = true

    // finding "n"
    while n < widths.count && overlap {
        overlap = doesOverlap(n, positions: positions, widths: widths, margin: margin)
        if overlap {
            n += 1
        }
    }

    var newPositions = [Int]()
    var i = 0
    // create result indices
    while i < positions.count {
        newPositions.append(i)
        i += n
    }

    return newPositions
}

// returns whether drawing only n-th labels will casue overlapping
private func doesOverlap(n: Int, positions: [CGPoint], widths: [CGFloat], margin: CGFloat) -> Bool {
    var i = 0
    var newPositions = [CGPoint]()
    var newWidths = [CGFloat]()

    // getting only n-th records
    while i < positions.count {
        newPositions.append(positions[i])
        newWidths.append(widths[i])
        i += n
    }

    // overlap with next label checking
    for j in 0...newPositions.count - 2 {
        if newPositions[j].x + newWidths[j] + margin > newPositions[j+1].x {
            return true
        }
    }

    return false
}

}