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

Recalculate space on label formatting. #561

Open
chuynadamas opened this issue Nov 20, 2015 · 18 comments
Open

Recalculate space on label formatting. #561

chuynadamas opened this issue Nov 20, 2015 · 18 comments

Comments

@chuynadamas
Copy link
Contributor

Hi. I found a little issue which i have been struggling a lot, it seems that if i apply a format on the x-labels the API doesn't recalculate the space in the wit the new format.

I'm adding a formatter to add an elipsis (...) when the label is more than 7 characters length just like you can see in the image.

screen shot 2015-11-20 at 5 46 00 pm

All the gray area is the barChartView, If i don't use the formatter i got this

screen shot 2015-11-20 at 5 45 28 pm

The code that i'm using to add the formatter is the following:

Container class:

ChartXAxis *xAxis = barView.xAxis;
LargeValueFormatter *formatter = [LargeValueFormatter new];
xAxis.valueFormatter = formatter;

Formatter class:

    public func stringForXValue(index: Int, original: String, viewPortHandler: ChartViewPortHandler) -> String {

        if original.characters.count > 7 {
            return self.addEllipsis(value: original)
        } else {
            return original
        }
    }

As you can see, without the formatter the chart use in a better way the space, i guess that the problem is that API don't recalculate the bottomContent once that the labels are formatted. I'd like to help with this if you guys can give a little explanations if my diagnosis of the issue is ok and where i can find the code that i need to call to recalculate with the new labels. I guess this could be a great feature.

Thanks in advance. 🤓

@liuxuan30
Copy link
Member

I think this is related to #513. This is the x axis label rotation PR. In the PR there is some explainations how it works.

However, I don't see an obvious problem from your images, the first image has 7 chars and 3 ellipsis, seems totally fine with the length. What's the problem? alignment or something else?

@chuynadamas
Copy link
Contributor Author

The alignment is ok, the problem is that i think that the chart needs to be bigger in the first image, this because the labels are smaller than in the second image just to take advantage of the extra space that we gain adding the ellipsis at the labels.

In the following images i added an extra bottom offset just to fix and explain my point.

screen shot 2015-11-23 at 12 08 46 pm

It would be great if we can recalculate the space available after apply the format to the labels.

Thanks in advance 🤓

@liuxuan30
Copy link
Member

I may understand your point, but still It's a little hard to see the difference of sizes. Are you talking about the height?

Could you please check the chartView's frame size and the viewPortHandler.contentRect? There is some different size for the whole UIView and the area to draw the chart itself, excluding the axis and axis labels.

I am guessing the chart view's contentRect is defined once the label size, offset is calculated. Then it won't re-calculate the contentRect based on the new label size after you applied the formatter?

@liuxuan30
Copy link
Member

OK so I took a quick look about this, it seems that it does not take the formatter will change the size into account.
In the code where we compute the x axis label size/rotated size, we don't use formatter first. I guess this can be improved, and we also need to consider y axis, because y axis' requiredSize does not use formatter as well.

We should apply the formater before we calculate label size, like we may add $ after the profit. or cut some chars.

@danielgindi, what do you think?

    public func computeAxis(xValAverageLength xValAverageLength: Double, xValues: [String?])
    {
        var a = ""

        let max = Int(round(xValAverageLength + Double(_xAxis.spaceBetweenLabels)))

        for (var i = 0; i < max; i++)
        {
            a += "h"
        }

        let widthText = a as NSString

        let labelSize = widthText.sizeWithAttributes([NSFontAttributeName: _xAxis.labelFont])

        let labelWidth = labelSize.width
        let labelHeight = labelSize.height

        let labelRotatedSize = ChartUtils.sizeOfRotatedRectangle(labelSize, degrees: _xAxis.labelRotationAngle)

        _xAxis.labelWidth = labelWidth
        _xAxis.labelHeight = labelHeight
        _xAxis.labelRotatedWidth = labelRotatedSize.width
        _xAxis.labelRotatedHeight = labelRotatedSize.height

        _xAxis.values = xValues
    }
    public func requiredSize() -> CGSize
    {
        let label = getLongestLabel() as NSString
        var size = label.sizeWithAttributes([NSFontAttributeName: labelFont])
        size.width += xOffset * 2.0
        size.height += yOffset * 2.0
        size.width = max(minWidth, min(size.width, maxWidth > 0.0 ? maxWidth : size.width))
        return size
    }

@chuynadamas
Copy link
Contributor Author

Exactly! Thats the problem, I fixed in a dirty way because i need to implement my custom formatter in the ChartData object and in the x-Axis, I implemented the same logic for the y-Axis too. let me explain.

The functions which is in charge to calculate the space is the xValsAverageLength, this one needs to apply the format to the labels that why i added the format to the data object.

    // calculates the average length (in characters) across all x-value strings
    internal func calcXValAverageLength()
    {
        if (_xVals.count == 0)
        {
            _xValAverageLength = 1
            return
        }

        var sum = 1

        for (var i = 0; i < _xVals.count; i++)
        {
            let label = _xVals[i]! as String
            //Apply the format
            let formattedLabel = _xValsValueFormatter.stringForXValue(i, original: label) ?? label

            sum += _xVals[i] == nil ? 0 : (formattedLabel).characters.count
        }   
        _xValAverageLength = Double(sum) / Double(_xVals.count)
    }

I'm using the same formatter that i pass to the x-Axis because this is the one which display the labels in the chart, If you have any idea or help how to implement this i'd love to contribute with you guys.

Fort the y-Axis i did the same, i guess that this is a clean implementation.

    /// formatter for the x-Values
    public var yValsValueFormatter: ChartXAxisValueFormatter?{
        get{
            return _yValsValueFormatter
        }
        set{
            _yValsValueFormatter = newValue ?? ChartDefaultXAxisValueFormatter()
        }
    }
    /// - returns: the formatted y-label at the specified index. This will either use the auto-formatter or the custom formatter (if one is set).
    public func getFormattedLabel(index: Int) -> String
    {
        if (index < 0 || index >= entries.count)
        {
            return ""
        }

        let formattedValue = _yValsValueFormatter.stringForXValue(index, original: (valueFormatter ?? _defaultValueFormatter).stringFromNumber(entries[index])!)

        return formattedValue
    }

In this case i'm still using your number formatter the problem was that i needed a non standard format for the y-Axis ( 1000 = 1K, 1000000 = 1M ) that why i sued a custom formatter. I think that this feature also need to be in the iOS api not just un the Android one.

Thanks in advance 🤓

Updated

Also i have to update the following methods to add the feature to the horizontal bar chart

    public override func computeAxis(xValAverageLength xValAverageLength: Double, xValues: [String?])
    {
        _xAxis.values = xValues

        let longest = _xAxis.getLongestLabel() as NSString

        let labelSize = longest.sizeWithAttributes([NSFontAttributeName: _xAxis.labelFont])

        let labelWidth = floor(labelSize.width + _xAxis.xOffset * 3.5)
        let labelHeight = labelSize.height

        let labelRotatedSize = ChartUtils.sizeOfRotatedRectangle(rectangleWidth: labelSize.width, rectangleHeight:  labelHeight, degrees: _xAxis.labelRotationAngle)

        _xAxis.labelWidth = labelWidth
        _xAxis.labelHeight = labelHeight
        _xAxis.labelRotatedWidth = round(labelRotatedSize.width + _xAxis.xOffset * 3.5)
        _xAxis.labelRotatedHeight = round(labelRotatedSize.height)
    }
    public override func getLongestLabel() -> String
    {
        var longest = ""

        for (var i = 0; i < values.count; i++)
        {
            var label = values[i]
            //Apply the format
            label = valueFormatter?.stringForXValue(i, original: label!) ?? label

            if (label != nil && longest.characters.count < (label!).characters.count)
            {
                longest = label!
            }
        }

        return longest
    }

@liuxuan30
Copy link
Member

if possible, follow the code style and not the 'dirty' way to create a PR :)

@hetpin
Copy link

hetpin commented Dec 11, 2015

@lidgardo Can you explain in details how to customize Y-axis. Honestly, I tried to read your comments above, but still can not implement.

I worked with ChartYAxisValueFormatter Android version. I'm porting my App to IOS, but I can not find ChartYAxisValueFormatter IOS version. I'm not senior on ios dev, can you help. Pls.

@liuxuan30
Copy link
Member

@hetpin if you are looking for y axis formater, it is here:

    /// the formatter used to customly format the y-labels
    public var valueFormatter: NSNumberFormatter?

    /// the formatter used to customly format the y-labels
    internal var _defaultValueFormatter = NSNumberFormatter()`

@chuynadamas
Copy link
Contributor Author

Hi @hetpin as @liuxuan30 said those are the properties to format the Y-Axis, you need to take in consideration that this are only functionals to standard number format, if this is your case that property will be useful. if not You i'll need to implement your formatter extends from the class

ChartXAxisValueFormatter

Now according my comments above all of them were to fix the process of calculate the space. I'm working on a pull request to add this enhancement. If you have doubts about the custom formatter I can explain you better, in other case you can use the valueFormatter property which is already there.

Sorry for the late response.
Cheers!

@hetpin
Copy link

hetpin commented Dec 16, 2015

Thanks for help, I ended up with a simple customization of NSNumberFormatter. In short, I subclassed NSNumberFormatter and override two methods: - (id)init; and - (NSString *)stringForObjectValue:(id const)aObj; Simple and effective.
Thanks.

@liuxuan30 liuxuan30 reopened this Dec 16, 2015
@liuxuan30
Copy link
Member

reopen - oops. the original issue is not solved yet

@danielgindi
Copy link
Collaborator

This is a problematic one, requiring some changes to the api.

@PhilJay what do you think about passing the viewPortHandler to computeAxis so we can pass it to getLongestLabel so we can pass it to the X axis formatter?

@danielgindi
Copy link
Collaborator

@PhilJay ?

@PhilJay
Copy link
Collaborator

PhilJay commented Feb 29, 2016

Sorry, didn't see this.
Looks like a big issue. I will look into it tomorrow!

@danielgindi
Copy link
Collaborator

I'm not sure, but I think that this one is also solved by Charts 3.0, am I right?

@liuxuan30 liuxuan30 added the task label Aug 12, 2016
@zigdanis
Copy link

open func getLongestLabel() -> String called for only visible chart area.
So open func computeSize() doesn't fit all xAxis values when user scrolling bar chart.
I would be happy to have some way to calculate bottom xAxis offset that will handle all dataSet values😀
example

@elchris78
Copy link

Hey guys,

I didn't get what the solution was! I'm still having the same spacing problem, any solution?

@romrell4
Copy link

romrell4 commented Jan 7, 2024

I believe I'm having the same issue, but with some additional details:
I've got a LineChart with rotated bottom X values. When it first draws, it seems to not take into account the extra rotated value height necessary.
Simulator Screenshot - iPhone SE (3rd generation) - 2024-01-07 at 15 30 47

However, when I come back and it redraws, it does...
Simulator Screenshot - iPhone SE (3rd generation) - 2024-01-07 at 15 30 52

Full video:
https://github.com/danielgindi/Charts/assets/15692420/e6eb3b58-b637-4b44-b417-a4413d5f78fd

Any idea what race case could be causing the initial draw to not have the correct height? Here's the chart code:

struct ValuePerDateChart: UIViewRepresentable {
    typealias UIViewType = LineChartView
    
    init(data: [ValuePerDate], legendLabel: String) {
        self.data = data.reversed()
        self.legendLabel = legendLabel
    }
    
    let data: [ValuePerDate]
    let legendLabel: String
    
    func makeUIView(context: Context) -> LineChartView {
        let chart = LineChartView()
        chart.rightAxis.enabled = false
        chart.leftAxis.drawGridLinesEnabled = false
        chart.xAxis.drawGridLinesEnabled = false
        
        chart.xAxis.axisMinimum = -0.5
        chart.xAxis.granularity = 1
        chart.xAxis.labelPosition = .bottom
        chart.xAxis.labelRotationAngle = -50
        chart.xAxis.setLabelCount(20, force: false)
        chart.xAxis.labelFont = .systemFont(ofSize: 9)
        chart.legend.verticalAlignment = .top
        chart.extraTopOffset = 8
        return chart
    }
    
    func updateUIView(_ uiView: LineChartView, context: Context) {
        let dataSet = LineChartDataSet(
            // Show the latest entries at the end
            entries: data
                .enumerated()
                .compactMap { index, match in
                    ChartDataEntry(x: Double(index), y: match.value)
                },
            label: legendLabel
        )
        dataSet.setColor(UIColor(.chartWinnerColor))
        dataSet.setCircleColor(UIColor(.chartWinnerColor))
        dataSet.circleHoleColor = UIColor(.chartWinnerColor)
        dataSet.circleRadius = 4
        dataSet.lineWidth = 2
        dataSet.axisDependency = .left
        dataSet.valueFormatter = DataValueFormatter()
        dataSet.mode = .horizontalBezier
        let data = LineChartData(dataSet: dataSet)
        uiView.data = data
        uiView.xAxis.valueFormatter = XAxisValueFormatter(data: self.data)
        uiView.xAxis.axisMaximum = data.xMax + 0.5
        uiView.animate(yAxisDuration: 1, easingOption: .easeInOutQuad)
    }
}

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

8 participants