Charts
Charts copied to clipboard
Recalculate space on label formatting.
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.

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

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. 🤓
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?
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.
It would be great if we can recalculate the space available after apply the format to the labels.
Thanks in advance 🤓
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?
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
}
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
}
if possible, follow the code style and not the 'dirty' way to create a PR :)
@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 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()`
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!
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.
reopen - oops. the original issue is not solved yet
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?
@PhilJay ?
Sorry, didn't see this. Looks like a big issue. I will look into it tomorrow!
I'm not sure, but I think that this one is also solved by Charts 3.0, am I right?
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😀
Hey guys,
I didn't get what the solution was! I'm still having the same spacing problem, any solution?
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.
However, when I come back and it redraws, it does...
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)
}
}