I've implemented something to solve the problem with positioning labels. My goal was to exclude overlapping labels and graph lines. The main idea is to add newlines and spaces to the value string to position it correctly.
Source code is in Objective-C unfortunately, but it can be easily adopted for Swift
LineChartValueFormatter.h file:
#import <Foundation/Foundation.h>
@interface LineChartValueFormatter : NSObject
- (instancetype)initWithChartData:(NSArray<NSNumber *> *)chartData;
- (NSString *)formattedValueStringAtIndex:(NSUInteger)index;
@end
LineChartValueFormatter.m file:
#import "LineChartValueFormatter.h"
typedef enum : NSUInteger {
ChartValueDirectionTop,
ChartValueDirectionTopLeft,
ChartValueDirectionTopRight,
ChartValueDirectionBottom,
ChartValueDirectionBottomLeft,
ChartValueDirectionBottomRight,
} ChartValueDirection;
BOOL chartValueDirectionIsBottom(ChartValueDirection direction) {
return (direction == ChartValueDirectionBottom || direction == ChartValueDirectionBottomLeft || direction == ChartValueDirectionBottomRight);
}
BOOL chartValueDirectionIsTop(ChartValueDirection direction) {
return (direction == ChartValueDirectionTop|| direction == ChartValueDirectionTopLeft || direction == ChartValueDirectionTopRight);
}
@interface LineChartValueFormatter ()
@property (nonatomic, strong) NSArray<NSNumber *> * chartData;
@property (nonatomic, strong) NSMutableArray<NSNumber *> * directions;
@end
@implementation LineChartValueFormatter
- (instancetype)initWithChartData:(NSArray<NSNumber *> *)chartData {
self = [super init];
if (self) {
self.chartData = chartData;
self.directions = [NSMutableArray arrayWithCapacity:chartData.count];
[self.directions addObject:@([self firstValueDirection])];
for (NSInteger i = 1; i < self.chartData.count - 1; ++i) {
[self.directions addObject:@([self commonValueDirectionAtIndex:i])];
}
[self.directions addObject:@([self lastValueDirection])];
}
return self;
}
- (NSString *)formattedValueStringAtIndex:(NSUInteger)index {
if (!self.directions || !self.directions.count) {
return valueString;
}
ChartValueDirection direction = [self.directions[index] integerValue];
NSString * newlineString = @"\n";
NSString * valueString = [self.chartData[index] stringValue];
NSUInteger numberOfSpaces = valueString.length;
// There is U+2007 space used (“Tabular width”, the width of digits), not usual space
NSString * spaceString = [@"" stringByPaddingToLength:numberOfSpaces
withString:@" "
startingAtIndex:0];
switch (direction) {
case ChartValueDirectionTopLeft:
return [NSString stringWithFormat:@"%@%@", valueString, spaceString];
case ChartValueDirectionTopRight:
return [NSString stringWithFormat:@"%@%@", spaceString, valueString];
case ChartValueDirectionBottom:
return [NSString stringWithFormat:@"%@%@", newlineString, valueString];
case ChartValueDirectionBottomLeft:
return [NSString stringWithFormat:@"%@%@%@", newlineString, valueString, spaceString];
case ChartValueDirectionBottomRight:
return [NSString stringWithFormat:@"%@%@%@", newlineString, spaceString, valueString];
default:
return valueString;
}
}
- (ChartValueDirection)firstValueDirection {
double rate = [self.chartData[0] doubleValue];
double nextRate = [self.chartData[1] doubleValue];
if (nextRate > rate) {
return ChartValueDirectionBottomRight;
}
return ChartValueDirectionTopRight;
}
- (ChartValueDirection)lastValueDirection {
NSUInteger count = self.chartData.count;
double rate = [self.chartData[count - 1] doubleValue];
double previousRate = [self.chartData[count - 2] doubleValue];
if (previousRate > rate) {
return ChartValueDirectionBottomLeft;
}
return ChartValueDirectionTopLeft;
}
- (ChartValueDirection)commonValueDirectionAtIndex:(NSUInteger)index {
double rate = [self.chartData[index] doubleValue];
double previousRate = [self.chartData[index - 1] doubleValue];
double nextRate = [self.chartData[index + 1] doubleValue];
if (previousRate > rate && rate > nextRate) {
return ChartValueDirectionBottomLeft;
}
if (previousRate >= rate && rate <= nextRate) {
return ChartValueDirectionBottom;
}
if (previousRate < rate && rate < nextRate) {
return ChartValueDirectionTopLeft;
}
if (previousRate <= rate && rate >= nextRate) {
return ChartValueDirectionTop;
}
return ChartValueDirectionTop;
}
@end
Usage example:
// Here is your code for configuring charts ...
// chartData is your array of y values of type NSArray<NSNumber *> * defined and filled somewhere above
// lineChartDataSet is your dataset for graph drawing defined and initialized somewhere above
LineChartValueFormatter * formatter = [[LineChartValueFormatter alloc] initWithChartData:chartData];
lineChartDataSet.valueFormatter = [ChartDefaultValueFormatter withBlock:^NSString * _Nonnull(double value, ChartDataEntry * _Nonnull entry, NSInteger dataSetIndex, ChartViewPortHandler * _Nullable viewPortHandler) {
return [formatter formattedValueStringAtIndex:entry.x];
}];

This is not really great solution but it allows to fix problem without changing Charts
source code, that was critical for me. Hope it helps someone else