Charts icon indicating copy to clipboard operation
Charts copied to clipboard

Recalculate space on label formatting.

Open chuynadamas opened this issue 9 years ago • 18 comments

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

chuynadamas avatar Nov 20 '15 23:11 chuynadamas

I think this is related to https://github.com/danielgindi/ios-charts/pull/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?

liuxuan30 avatar Nov 23 '15 01:11 liuxuan30

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 🤓

chuynadamas avatar Nov 23 '15 17:11 chuynadamas

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 avatar Nov 24 '15 01:11 liuxuan30

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
    }

liuxuan30 avatar Nov 24 '15 01:11 liuxuan30

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
    }

chuynadamas avatar Nov 24 '15 16:11 chuynadamas

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

liuxuan30 avatar Nov 25 '15 05:11 liuxuan30

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

hetpin avatar Dec 11 '15 13:12 hetpin

@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()`

liuxuan30 avatar Dec 14 '15 01:12 liuxuan30

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!

chuynadamas avatar Dec 14 '15 16:12 chuynadamas

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.

hetpin avatar Dec 16 '15 11:12 hetpin

reopen - oops. the original issue is not solved yet

liuxuan30 avatar Dec 16 '15 11:12 liuxuan30

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 avatar Jan 03 '16 19:01 danielgindi

@PhilJay ?

danielgindi avatar Feb 29 '16 16:02 danielgindi

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

PhilJay avatar Feb 29 '16 20:02 PhilJay

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

danielgindi avatar Aug 10 '16 20:08 danielgindi

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

zigdanis avatar Oct 14 '16 13:10 zigdanis

Hey guys,

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

elchris78 avatar Mar 24 '17 03:03 elchris78

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)
    }
}

romrell4 avatar Jan 07 '24 22:01 romrell4