Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XAxis labels overlap in Charts 3.0 #1969

Open
debriennefrancoisjean opened this issue Dec 16, 2016 · 13 comments
Open

XAxis labels overlap in Charts 3.0 #1969

debriennefrancoisjean opened this issue Dec 16, 2016 · 13 comments

Comments

@debriennefrancoisjean
Copy link

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
Copy link
Author

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.

@liuxuan30 liuxuan30 added the bug label Dec 19, 2016
@ligerjohn
Copy link

ligerjohn commented Dec 21, 2016

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.

@hellstorm
Copy link

Hello

same issue :(

@kaszap82
Copy link

hi!

I have the same issue, is there any workaround?

@4np
Copy link
Contributor

4np commented Jan 16, 2017

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
Copy link
Contributor

4np commented Jan 20, 2017

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...

@AnitaAtyi
Copy link

Hi!
I have the same issue.

@4np
Copy link
Contributor

4np commented Feb 20, 2017

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

@AnitaAtyi
Copy link

AnitaAtyi commented Feb 24, 2017

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

@ondrejhanslik
Copy link

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.

@bansi116
Copy link

bansi116 commented Jun 21, 2019

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.

@Rajneesh071
Copy link

Is this issue fixed ?

@SujalGondaliya1
Copy link

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests