Charts icon indicating copy to clipboard operation
Charts copied to clipboard

XAxis labels overlap in Charts 3.0

Open debriennefrancoisjean opened this issue 8 years ago • 13 comments

In charts 2.x, the xAxis adjusted it's labelCount based on the width of the widest label. Here is an example from the 2.x demo:

charts 2

However, in Charts 3, the labelWidth seems to be disregarded (computeAxis does not even consider it) when calculating the number of entries to show on the axis. Anything above 10 (about) characters has a chance to eventually get overlapped. Here's the same example, from Charts 3 demo:

charts 3

This is a major regression for us from 2x. I've looked at the code and here is what I found:

XAxisRender.computeAxisValues calls super.computeAxisValues first, then calls computeSize. In my opinion, the result of computeSize should be an input to computeAxisValues and would need to be called first.

However modifying the AxisRendererBase.computeAxisValues to consider the labelWidth is beyond my comprehension level at this time.

debriennefrancoisjean avatar Dec 16 '16 14:12 debriennefrancoisjean

Another regression that is linked to this is that labelCount seems to be used, regardless of available space. On iPad, on Charts 2.x, the framework was showing as many xAxis labels as could possibly fit on screen. Now, with Charts 3.x, the framework will show 6 xAxis labels, always.

I understand that I could calculate an optimal amount of xAxis labels and set the labelCount myself but Charts 2.x handled this for me automatically. It's also not that simple of a calculation.

debriennefrancoisjean avatar Dec 16 '16 15:12 debriennefrancoisjean

I had the exact same issue and it turns out that adding axisMinimum = 0.0 on the x axis fixed it for me. None of my labels overlap anymore.

edit: Spoke to soon, this is still an issue for us too. I was able to alleviate the situation somewhat using xAxis.avoidFirstLastClippingEnabled = false.

ligerjohn avatar Dec 21 '16 20:12 ligerjohn

Hello

same issue :(

hellstorm avatar Jan 06 '17 22:01 hellstorm

hi!

I have the same issue, is there any workaround?

kaszap82 avatar Jan 10 '17 13:01 kaszap82

I am experiencing the same issue where the x-axis labels overlap (using a custom IAxisValueFormatter):

screen shot 2017-01-16 at 15 14 41

4np avatar Jan 16 '17 14:01 4np

As mentioned before, the number of labels is fixed to six, which in itself is a bit odd. You would expect the library to dynamically determine how many labels could fit on the axis and make it fit. Perhaps having an optional configuration property to specify the maximum number of labels to render.

Rendering the rects clearly shows the overlapping labels:

    internal class func drawMultilineText(context: CGContext, text: String, knownTextSize: CGSize, point: CGPoint, attributes: [String : AnyObject]?, constrainedToSize: CGSize, anchor: CGPoint, angleRadians: CGFloat)
    {
        ...
        NSUIGraphicsPopContext()

        // draw rect
        #if !os(OSX)
            NSUIGraphicsPushContext(context)
            context.setStrokeColor(UIColor.red.cgColor)
            context.setLineWidth(0.5)
            context.addRect(rect)
            context.drawPath(using: .stroke)
            NSUIGraphicsPopContext()
        #endif
    }

screen shot 2017-01-20 at 12 49 40

When changing the labelCount for the xAxis from the default value of 6 to 5, it will actually result in just 4 labels to be rendered instead of the expected 5 (see screenshot below). Debugging the number of xAxis.entries also shows just 4 values: "x-axis entries: [30.0, 60.0, 90.0, 120.0]". When you change it to 4 it will actually only display 3 labels, etcetera.

        let xAxis = lineChartView.xAxis
        ...
        xAxis.labelCount = 5
screen shot 2017-01-20 at 17 42 34

UPDATE: I just noticed that you need to set the labelCount using xAxis.setLabelCount(5, force: true) which does work as expected. Makes me wonder though why labelCount is writable if you need to use this method (related to #2085)?

One of the pieces of logic I found that is error prone is getLongestLabel that returns the String value of the axis label with the most characters. When using non-monospaced fonts this is actually a wrong assumption. A label with less characters might be rendered wider than the one that has the most characters and debugging the label widths actually shows that this is really happening: the label with the highest number of characters had a width that was 5 pixels less than a label that had less characters but renders wider. In all occurences where this code is being called it is only being used to afterwards determine the width of the string. Hence, a more accurate (although possibly more resource intensive) solution would be to replace that logic with something like this:

    // The length of a label depends on its font (if it is not
    // a monospaced font) and rendering, not on the number or 
    // characters in a string.
    open func getLongestLabelSize() -> CGSize {
        var longestSize = CGSize()
        
        // iterate over all labels
        for i in 0 ..< entries.count {
            let text = getFormattedLabel(i)
            let size = text.size(attributes: [NSFontAttributeName: labelFont])
            
            if size.width > longestSize.width {
                longestSize = size
            }
        }
        
        return longestSize
    }

Or even more Swifty like this:

    open func getLongestLabelSize() -> CGSize {
        return entries.enumerated().map { index, _ in
            return getFormattedLabel(index).size(attributes: [NSFontAttributeName: labelFont])
        }
        .sorted(by: { s1, s2 in return s1.width < s2.width })
        .last!
    }

Unfortunately I have not yet found the root cause of the overlapping labels but it is clear the logic that renders the labels is faulty...

4np avatar Jan 20 '17 16:01 4np

Hi! I have the same issue.

AnitaAtyi avatar Feb 15 '17 15:02 AnitaAtyi

I have a workaround for this issue. It basically skips rendering of overlapping labels but it could be improved as there is now more whitespace where labels could have been rendered. Still I think it is acceptable for now until this issue is solved...

Create a new custom XAxisRenderer:

import Foundation
import Charts
#if !os(OSX)
import UIKit
#endif

// The original XAxisRender can result in overlapping labels on the
// x-axis (see https://github.com/danielgindi/Charts/issues/1969 ).
// This x-axis renderer will check if labels overlap and ignore drawing
// labels that overlap the previously drawn label.
class NoOverlappingLabelsXAxisRenderer: XAxisRenderer {
    public var shouldDrawBoundingBoxes = false
    public var labelSpacing = CGFloat(4.0)
    
    // Keep track of the previous label's rect
    private var previousLabelRect: CGRect?
    
    override func renderAxisLabels(context: CGContext) {
        previousLabelRect = nil
        super.renderAxisLabels(context: context)
    }
    
    //swiftlint:disable function_parameter_count
    override func drawLabel(context: CGContext, formattedLabel: String, x: CGFloat, y: CGFloat, attributes: [String : NSObject], constrainedToSize: CGSize, anchor: CGPoint, angleRadians: CGFloat) {
        guard let axis = self.axis as? XAxis else { return }

        // determine label rect
        let labelRect = CGRect(x: x - (axis.labelWidth / 2), y: y, width: axis.labelWidth, height: axis.labelHeight)
        
        // check if this label overlaps the previous label
        if let previousLabelRect = previousLabelRect, labelRect.origin.x <= previousLabelRect.origin.x + previousLabelRect.size.width + labelSpacing {
            // yes, skip drawing this label
            self.previousLabelRect = nil
            return
        }
        
        // remember this label's rect
        self.previousLabelRect = labelRect
        
        // draw label
        super.drawLabel(context: context, formattedLabel: formattedLabel, x: x, y: y, attributes: attributes, constrainedToSize: constrainedToSize, anchor: anchor, angleRadians: angleRadians)
        
        // draw label rect for debugging purposes
        if shouldDrawBoundingBoxes {
            #if !os(OSX)
            // draw rect
            UIGraphicsPushContext(context)
            context.setStrokeColor(UIColor.red.cgColor)
            context.setLineWidth(0.5)
            context.addRect(labelRect)
            context.drawPath(using: .stroke)
            UIGraphicsPopContext()
            
            // draw line
            UIGraphicsPushContext(context)
            context.move(to: CGPoint(x: x, y: y))
            context.addLine(to: CGPoint(x: x, y: y - 4))
            context.setLineWidth(0.5)
            context.strokePath()
            UIGraphicsPopContext()
            #endif
        }
    }
    //swiftlint:disable function_parameter_count
}

And configure it when setting up your chart:

        // instantiate the chart
        let lineChartView = PALineChartView(frame: ...)

        ...
        
        // configure the x-axis
        let xAxis = lineChartView.xAxis
        ...
        
        // configure custom renderer
        let xAxisRenderer = NoOverlappingLabelsXAxisRenderer(viewPortHandler: lineChartView.viewPortHandler, xAxis: lineChartView.xAxis, transformer: lineChartView.xAxisRenderer.transformer)
        xAxisRenderer.shouldDrawBoundingBoxes = true // enable to debug label rects
        lineChartView.xAxisRenderer = xAxisRenderer

4np avatar Feb 20 '17 17:02 4np

Dear 4np,

It works fine for me. Thank you very much!

I deleted this line, because the further neighbors were overlapped too in some cases: self.previousLabelRect = nil

AnitaAtyi avatar Feb 24 '17 11:02 AnitaAtyi

Unfortunately, this solution is not perfect because most of the times you want to always display the first and last items on the axis. One possible solution is to add labels from left and right (keeping two frames) alternately.

ondrejhanslik avatar Jul 01 '17 07:07 ondrejhanslik

How to make multiline label in Line chart using Chart lib. of x aixis label (objective c ) i 'm having date and time value. i have to display both value in one label.

bansi116 avatar Jun 21 '19 07:06 bansi116

Is this issue fixed ?

Rajneesh071 avatar Dec 10 '21 16:12 Rajneesh071

I want to scroll the chart on only x-axis without any zoom out or anything. Can you help me out from this? And can you advise me to how to leave some space between label on x-axis?

SujalGondaliya1 avatar Jun 29 '24 11:06 SujalGondaliya1