折線圖實(shí)踐2018

需求簡(jiǎn)介

類似支付寶那樣的錢(qián)包工具,有很大一個(gè)類別理財(cái),經(jīng)常出現(xiàn)收益率曲線之類的,這個(gè)就是折線圖,大概的樣子如下

image.png

如何實(shí)現(xiàn)

  • 采用貝塞爾曲線自己畫(huà),采用CAShapeLayer進(jìn)行組合,也是可以做的。只是有點(diǎn)復(fù)雜,操心的內(nèi)容有點(diǎn)多。

  • 采用第三方庫(kù),這也是一個(gè)好主意。有現(xiàn)實(shí)需求,自己實(shí)現(xiàn)又煩,好用的第三方庫(kù)就來(lái)了。經(jīng)過(guò)比較,我們可以選擇 Charts

  • 這個(gè)第三方庫(kù)是用swift寫(xiě)的,在目前的object-c工程中使用有點(diǎn)麻煩。不過(guò),為了和Android保持一致,這點(diǎn)麻煩算不了什么。

  • 這些糾結(jié)的過(guò)程,已經(jīng)在另一篇文章里寫(xiě)了:關(guān)于圖表庫(kù)的選擇

包裝Charts

Charts中代表折線圖的是LineChartView,從BarLineChartViewBase,ChartViewBase,NSUIView,UIView一路繼承過(guò)來(lái)。

  • 創(chuàng)建一個(gè)內(nèi)部屬性成員LineChartView,作為我們的自定義view的子view,并且占滿所有空間。我們的自定義view只是一個(gè)容器。
@interface KJTLineChartView ()

@property (strong, nonatomic) LineChartView *lineView;

@end
  • 創(chuàng)建子視圖
#pragma mark - private
- (void)setup {
    self.lineView = [[LineChartView alloc] init];
    [self addSubview:self.lineView];
}
  • 自定義視圖只是作為容器,將剛剛創(chuàng)建的折線圖鋪滿整個(gè)空間。
// 布局
- (void)layoutSubviews {
    [self.lineView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.mas_equalTo(0);
        make.bottom.mas_equalTo(0);
        make.left.mas_equalTo(0);
        make.right.mas_equalTo(0);
    }];
}
  • 由于沒(méi)有數(shù)據(jù),所以現(xiàn)在是空白:
image.png
  • 圖中的文案,在ChartViewBase中可以找到:
image.png

設(shè)置數(shù)據(jù)

  • 圖中數(shù)據(jù)點(diǎn)的包含(x,y)兩個(gè)維度的信息,數(shù)據(jù)類型都是double。在大多數(shù)的介紹文章中,都是把x軸的信息簡(jiǎn)化為序號(hào)index,這當(dāng)然是可以的。

  • 這次的需求是一張“七日年化收益率”折線圖,'y'軸是收益率,數(shù)據(jù)類型是double,沒(méi)問(wèn)題。橫軸是七天的日期,是“月月-日日”的格式。考慮到NSDate類型轉(zhuǎn)化為double和字符串都很方便,所以x軸數(shù)據(jù)可以直接是“時(shí)間戳”,也就是從1970年到現(xiàn)在所過(guò)的秒數(shù)。所以這次的做法是將NSDate類型轉(zhuǎn)化為double,直接作為x軸數(shù)據(jù)來(lái)實(shí)現(xiàn)的。

  • 關(guān)于數(shù)據(jù),設(shè)置過(guò)程主要有以下幾步

  1. 需要封裝成一個(gè)ChartDataEntry結(jié)構(gòu);
  2. 多個(gè)ChartDataEntry然后形成一個(gè)數(shù)組之后;
  3. 將數(shù)組再包裝成一個(gè)LineChartDataSet結(jié)構(gòu),這個(gè)結(jié)構(gòu)代表一條曲線;
  4. 將多個(gè)(包括1個(gè))LineChartDataSet結(jié)構(gòu)包裝成一個(gè)數(shù)組,所以可以同時(shí)顯示多條曲線;
  5. 將這個(gè)數(shù)組包裝成LineChartData結(jié)構(gòu);
  6. 用得到的這個(gè)結(jié)構(gòu),賦值給LineChartView的data成員;
  • 設(shè)計(jì)了一個(gè)對(duì)外接口,提供標(biāo)簽數(shù)組和y值數(shù)組
// x軸是日期,y軸是數(shù)據(jù)
- (void)updateWithDates:(NSArray<NSDate *> *)dates values:(NSArray<NSNumber *> *)values {
    //
    // 日期和數(shù)據(jù)組成一個(gè)點(diǎn),形成一條折線
    //
    // 1. 將日期和數(shù)據(jù)取出,包裝成ChartDataEntry,組成新的數(shù)組
    // x軸是日期, y軸是數(shù)據(jù)
    NSMutableArray<ChartDataEntry *> *entrys = [NSMutableArray array];
    NSInteger count = dates.count;
    for (NSInteger index = 0; index < count; index++) {
        double x = (double)dates[index].timeIntervalSince1970;
        double y = [values[index] doubleValue];
        ChartDataEntry *entry = [[ChartDataEntry alloc] initWithX:x y:y];
        [entrys addObject:entry];
    }
    // 2. 組裝LineChartDataSet結(jié)構(gòu),并做相應(yīng)的配置
    LineChartDataSet *dataSet = [[LineChartDataSet alloc] initWithValues:entrys label:nil];
    // 配置先不做,看默認(rèn)效果
    // 3. 組裝LineChartDataSet數(shù)組,這里只需要一個(gè)dataSet
    NSArray *dataSets = [NSArray arrayWithObject:dataSet];
    // 4. 組裝LineChartData結(jié)構(gòu)
    LineChartData *data = [[LineChartData alloc] initWithDataSets:dataSets];
    // 5. 賦值
    self.lineView.data = data;
}
  • 在調(diào)用的ViewController中,給幾個(gè)測(cè)試數(shù)據(jù)
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    NSDate *today = [NSDate date];
    NSArray *dates = @[today, [today kjtDateByAddingDays:1], [today kjtDateByAddingDays:2], [today kjtDateByAddingDays:3], [today kjtDateByAddingDays:4], [today kjtDateByAddingDays:5], [today kjtDateByAddingDays:6]];
    NSArray *values = @[@3.729, @3.683, @2.8, @3.1, @4.123, @3.587, @3.7482];
    [self.lineChartView updateWithDates:dates values:values];
}

運(yùn)行起來(lái)之后,效果是這樣的:

image.png

整體設(shè)置

默認(rèn)的可以拖動(dòng),可以放大,雙擊也可以放大,y軸有兩個(gè)。在這里,這些都不需要,可以做如下配置:

    //
    // 設(shè)置chart view 整體
    //
    // 選中處理等
    self.lineView.delegate = self;
    // 隱藏說(shuō)明圖標(biāo)
    self.lineView.legend.form = ChartLegendFormNone;
    // 無(wú)數(shù)據(jù)提示信息
    self.lineView.noDataText = @"暫無(wú)數(shù)據(jù)";
    // 取消Y軸縮放
    self.lineView.scaleYEnabled = NO;
    // 取消X軸縮放
    self.lineView.scaleXEnabled = NO;
    // 取消雙擊縮放
    self.lineView.doubleTapToZoomEnabled = NO;
    // 取消拖拽圖標(biāo)
    self.lineView.dragEnabled = NO;
    // 加載動(dòng)畫(huà)
    [self.lineView animateWithXAxisDuration:1.0];
    // 隱藏右Y軸
    self.lineView.rightAxis.enabled = NO;

加上這些配置之后,右y軸沒(méi)了,也不能放大了,legend也沒(méi)有了,并且還有生成動(dòng)畫(huà)。大致效果如下:

image.png

配置左Y軸

根據(jù)設(shè)計(jì)UI圖,配置左Y軸的相關(guān)屬性

    //
    // 設(shè)置左Y軸
    //
    // y軸展示多少個(gè)
    self.lineView.leftAxis.labelCount = 6;
    // 設(shè)置Y軸的最小值
    self.lineView.leftAxis.axisMinimum = 0;
    // Y軸顏色
    self.lineView.leftAxis.axisLineColor = kColorWithHexA(0x000000,  0.24);
    // 文字顏色
    self.lineView.leftAxis.labelTextColor = kColorWithHexA(0x000000,  0.24);
    // 文字字體
    self.lineView.leftAxis.labelFont = kFontSystem10;
    // 網(wǎng)格線顏色
    self.lineView.leftAxis.gridColor = kColorWithHex(0xECECF1);

這時(shí)的效果如下:

image.png

配置X軸

    //
    // 設(shè)置X軸
    //
    // 設(shè)置x軸數(shù)據(jù)在底部
    self.lineView.xAxis.labelPosition = XAxisLabelPositionBottom;
    // x軸不畫(huà)網(wǎng)格線
    self.lineView.xAxis.drawGridLinesEnabled = NO;
    // X軸顏色
    self.lineView.xAxis.axisLineColor = kColorWithHexA(0x000000,  0.24);
    // 文字顏色
    self.lineView.xAxis.labelTextColor = kColorWithHexA(0x000000,  0.24);
    // 文字字體
    self.lineView.xAxis.labelFont = kFontSystem10;
image.png

配置折線

    // 是否在拐點(diǎn)處顯示數(shù)據(jù)
    dataSet.drawValuesEnabled = NO;
    // 折線寬度
    dataSet.lineWidth = 2;
    // 折線顏色
    [dataSet setColor:kRedColor];
    // 是否繪制拐點(diǎn)
    dataSet.drawCirclesEnabled = NO;
    // 是否填充顏色
    dataSet.drawFilledEnabled = YES;
    // 填充顏色
    dataSet.fillColor = kRedColor;
    // 填充顏色透明度
    dataSet.fillAlpha = 0.20;
    // 去掉左下角“DataSet”字符串
    dataSet.label = nil;
image.png

格式化坐標(biāo)軸標(biāo)簽

標(biāo)簽格式的設(shè)置比較特殊,需要一個(gè)單獨(dú)的類,實(shí)現(xiàn)IChartAxisValueFormatter協(xié)議。只有一個(gè)函數(shù)

image.png
  • Y軸格式化,將double類型的數(shù)據(jù),轉(zhuǎn)化為3位小數(shù)的字符串。并且為了避免擁擠,0不顯示
@implementation KJTLabelFormatterY

#pragma mark - IChartAxisValueFormatter
- (NSString *)stringForValue:(double)value axis:(ChartAxisBase *)axis {
    // 第一個(gè)Y軸坐標(biāo)不顯示,避免和第一個(gè)X坐標(biāo)擁擠
    if (value == 0) {
        return @"";
    }
    return [NSString stringWithFormat:@"%0.3f", value];
}

@end

配置的地方,指定這個(gè)自定義的類

    // 格式化y標(biāo)簽
    self.lineView.leftAxis.valueFormatter = [[KJTLabelFormatterY alloc] init];
image.png
  • X軸格式化,這里就很簡(jiǎn)單,將時(shí)間戳轉(zhuǎn)換為“月月-日日”格式的日期字符串就可以了。
@implementation KJTLabelFormatterX

#pragma mark - IChartAxisValueFormatter
- (NSString *)stringForValue:(double)value axis:(ChartAxisBase *)axis {
    NSDate *date = [NSDate dateWithTimeIntervalSince1970:(NSTimeInterval)value];
    return [date kjtStringWithFormat:@"MM-dd"];
}

@end

配置的地方:

    // 格式化x軸標(biāo)簽
    self.lineView.xAxis.valueFormatter = [[KJTLabelFormatterX alloc] init];
image.png

設(shè)置選中標(biāo)簽

提供了一個(gè)ChartMarkerView視圖,作為選中之后彈出的“氣泡視圖”。這個(gè)可以看做一個(gè)容器,具體的樣子,完全可以自定義。這里,就用了一個(gè)UILabel和一個(gè)UIImageView來(lái)實(shí)現(xiàn)。
很多文章中提到的BalloonMarker并不存在,這是官方demo中自己寫(xiě)的一個(gè)自定義視圖,并且是swift的,不能直接用。
“氣泡視圖”ChartMarkerView的原點(diǎn),默認(rèn)在選中數(shù)據(jù)點(diǎn)上,效果會(huì)出現(xiàn)在下方。如果要做到UI上那樣在選中點(diǎn)上方,需要通過(guò)offset屬性來(lái)調(diào)節(jié)位置。這個(gè)地方相對(duì)布局不方便,直接用frame比較好。這個(gè)過(guò)程相對(duì)比較繁瑣,需要耐心調(diào)整。

    //
    // 設(shè)置選中時(shí)候的"氣泡視圖"
    //
    // 這個(gè)標(biāo)記視圖將以數(shù)據(jù)點(diǎn)為起點(diǎn)布局
    ChartMarkerView *markerView = [[ChartMarkerView alloc] init];
    // 顯示y軸數(shù)據(jù)的標(biāo)簽
    self.markerLabel = [[UILabel alloc] init];
    self.markerLabel.layer.cornerRadius = 9;
    self.markerLabel.layer.masksToBounds = YES;
    self.markerLabel.font = kFontSystem12;
    self.markerLabel.textAlignment = NSTextAlignmentCenter;
    self.markerLabel.textColor = [UIColor whiteColor];
    self.markerLabel.backgroundColor = kRedColor;
    [markerView addSubview:self.markerLabel];
    // 選中小圖標(biāo)
    UIImageView *imageView = [[UIImageView alloc] init];
    imageView.image = [UIImage imageNamed:@"折線圖選中"];
    [markerView addSubview:imageView];
    // 布局,標(biāo)簽和圖標(biāo)右對(duì)齊,間隔為4
    self.markerLabel.frame = CGRectMake(0, 0, 49, 18);
    imageView.frame = CGRectMake(49-14, 18+4, 14, 14);
    // 設(shè)置偏移量,使數(shù)據(jù)點(diǎn)和小圖標(biāo)的中心點(diǎn)重合
    CGFloat x = imageView.center.x;
    CGFloat y = imageView.center.y;
    markerView.offset = CGPointMake(-x, -(y+7));
    // 設(shè)置折線圖的標(biāo)簽視圖
    markerView.chartView = self.lineView;
    self.lineView.marker = markerView;

UILabel上要顯示y的值,需要實(shí)現(xiàn)ChartViewDelegate協(xié)議

#pragma mark - ChartViewDelegate
- (void)chartValueSelected:(ChartViewBase *)chartView entry:(ChartDataEntry *)entry highlight:(ChartHighlight *)highlight {
    self.markerLabel.text = [NSString stringWithFormat:@"%0.4f", entry.y];
}

效果圖如下,和UI設(shè)計(jì)圖比較接近了。

image.png
  • 在頂部,如果空間不夠,由于受到邊界影響,氣泡視圖會(huì)出現(xiàn)移位現(xiàn)象,導(dǎo)致圖片對(duì)不準(zhǔn)。
image.png
  • 1個(gè)點(diǎn),由于有坐標(biāo)軸的影響,位置也會(huì)偏移一些
image.png
  • UI圖上是"山頂",效果相對(duì)較好,如果位置出現(xiàn)在"山谷",效果就差一點(diǎn)了
image.png

小結(jié)

相對(duì)于自己來(lái)寫(xiě),省力很多,整體效果也好很多。雖然配置比較麻煩,思路有點(diǎn)奇特。推薦使用。

參考文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容