Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 210
Maciej Kozieł Avatar asked Oct 21 '25 22:10

Maciej Kozieł


1 Answers

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
}

}

like image 139
Maciej Kozieł Avatar answered Oct 24 '25 11:10

Maciej Kozieł



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!