最近,遇到這樣一個問題,將一組日期和數字爲數據源的數據畫成折線圖。
折線圖可以左右滑動,可以縮放,同時點擊視圖的時候可以定位到最近的一個數據點
代碼demo:https://github.com/xiujiePei/Line
處理這個我覺得有如下三個難點
一、數據源數據不連續,需要自己計算生成連續數據源
二、如何實現可縮放的折線圖
三、如何定位最近的數據源
所以,這裏我只針對這三個問題進行分析
一、首先我們看一下數據源
{
"data":[
{
"data":"3478.78",
"date":"0105"
},
{
"data":"3539.81",
"date":"0106"
},
{
"data":"3294.38",
"date":"0107"
},
{
"data":"3361.56",
"date":"0108"
},
{
"data":"3192.45",
"date":"0111"
},
{
"data":"3215.71",
"date":"0112"
},
{
"data":"3155.88",
"date":"0113"
},
{
"data":"3221.57",
"date":"0114"
},
{
"data":"3118.73",
"date":"0115"
},
{
"data":"3130.73",
"date":"0118"
},
{
"data":"3223.13",
"date":"0119"
},
{
"data":"3174.38",
"date":"0120"
},
{
"data":"3081.35",
"date":"0121"
},
{
"data":"3113.46",
"date":"0122"
},
{
"data":"3128.89",
"date":"0125"
},
{
"data":"2940.51",
"date":"0126"
},
{
"data":"2930.35",
"date":"0127"
},
{
"data":"2853.76",
"date":"0128"
},
{
"data":"2946.09",
"date":"0129"
},
{
"data":"2901.05",
"date":"0201"
},
{
"data":"2961.33",
"date":"0202"
},
{
"data":"2948.64",
"date":"0203"
},
{
"data":"2984.76",
"date":"0204"
},
{
"data":"2963.79",
"date":"0205"
},
{
"data":"2946.71",
"date":"0215"
}
]
}
我把數據製成了JSON字符串的形式
在仔細觀察數據後,我發現,其實數據並不連續,如果以日期爲X軸的話,直接引用現有數據,所畫的折線圖肯定是不對的,所以我們要自己把數據轉變成連續數據。下面是直接上代碼
{
NSMutableArray *tmpArr = [NSMutableArray array];
StockCodeLineSingleDataEntity *pre = _SCLData.mDDatas.firstObject;
[tmpArr addObject:pre];
for (int i = 1; i < _SCLData.mDDatas.count; i++) {
StockCodeLineSingleDataEntity *SCLS = [_SCLData.mDDatas objectAtIndex:i];
int spaceDate = [pre.mDate ToDateStringSpace:SCLS.mDate];
float spaceData = [SCLS.mData floatValue] - [pre.mData floatValue];
float spaceData2 ;
if (spaceDate == -1 || spaceDate == 0) {
//continue;
}else{
spaceData2 = spaceData/spaceDate;
for (int i = 0; i < spaceDate -1 ; i++) {
NSString *date = [pre.mDate addOneDay];
NSString *data = [NSString stringWithFormat:@"%.2f",[pre.mData floatValue]+ spaceData2];
NSDictionary *dic = [NSDictionary dictionaryWithObjectsAndKeys:data,@"data",date,@"date", nil];
StockCodeLineSingleDataEntity *s = [StockCodeLineSingleDataEntity StockCodeLineSingleDataWithInfos:dic];
[tmpArr addObject:s];
pre = s;
}
}
[tmpArr addObject:SCLS];
pre = SCLS;
}
xyValues = [[SCLXYValues alloc] init];
xyValues.mXYValue = tmpArr;
}
其中有個重點方法爲ToDateStringSpace:,雖然名字很詭異,但是他所實現的功能是NSString形式的日期轉換成真正的日期,並且計算日期差的一個方法,是NSString中的分類方法,具體可以代碼可以參考NSString+PXJString、NSDate+PXJDate分類。
其中數據源中的數值可以直接算,不用轉換
然後重新計算得到的數據源存儲起來,備用
二、如何實現可縮放的折線圖
在這裏,折線圖的superView爲一個UIScrollView類型的視圖,我通過改變scrollView的contentSzie中width來實現縮放視圖。
通過添加UIPinchGestureRecognizer手勢,獲取在縮放時得到的scale(pin對象的屬性),但是我沒有像處理縮放圖片那樣直接改變視圖的transform,而是重新計算一下contentSize的寬度
- (void)drawXYValue{
[_scrollView clearAllPoint];
CGFloat c = 20 / _scale;//根據縮放因子計算可顯示的最大天數
CGFloat width = (xyValues.mXYValue.count/c)*(_scrollView.frame.size.width-ScrollViewLeftMargin);
_scrollView.contentSize = CGSizeMake(width+ScrollViewLeftMargin+ScrollViewRightMargin, _scrollView.frame.size.height);
//根據縮放因子計算點中間的間隔
widthX = width / (xyValues.mXYValue.count-1);
_scrollView.PointSpaceX = widthX;
if (_lineChartLine == nil) {
_lineChartLine = [CAShapeLayer layer];
_lineChartLine.lineCap = kCALineCapButt;
_lineChartLine.fillColor = [[UIColor clearColor] CGColor];
_lineChartLine.lineWidth = xyValues.lineWidth;
_lineChartLine.strokeEnd = 0.0f;
[_scrollView.layer addSublayer:_lineChartLine];
}
_lineChartLine.strokeColor = [xyValues.lineColor CGColor];
UIBezierPath *_lineChartPath = [UIBezierPath bezierPath];
[_lineChartPath setLineWidth:_scrollView.frame.size.width];
[_lineChartPath setLineCapStyle:kCGLineCapSquare];
CGFloat value = yAxis.mLineLongth / (yAxis.maxNumber - yAxis.minNumber);
StockCodeLineSingleDataEntity *s = [xyValues.mXYValue objectAtIndex:0];
CGFloat StartY = topMargin + value * (yAxis.maxNumber - s.mData.floatValue);
CGFloat StartX = ScrollViewLeftMargin;
[_lineChartPath moveToPoint:CGPointMake(StartX, StartY)];
PXJLinePoint *point = [[PXJLinePoint alloc] initWithFrame:CGRectMake(2, 2, 6, 6)];
point.center = CGPointMake(StartX, StartY);
point.isShowNumber = YES;
point.pointColor = xyValues.lineColor;
point.number = [self getShowString:s];
[_scrollView addSubview:point];
for (int i = 1; i < xyValues.mXYValue.count; i++) {
StockCodeLineSingleDataEntity *s = [xyValues.mXYValue objectAtIndex:i];
CGFloat X = ScrollViewLeftMargin + widthX*i;
CGFloat Y = topMargin + value * (yAxis.maxNumber - s.mData.floatValue);
[_lineChartPath addLineToPoint:CGPointMake(X, Y)];
PXJLinePoint *point = [[PXJLinePoint alloc] initWithFrame:CGRectMake(2, 2, 6, 6)];
point.isShowNumber = YES;
point.center = CGPointMake(X,Y);
point.number = [self getShowString:s];
point.pointColor = xyValues.lineColor;
[_scrollView addSubview:point];
}
_lineChartLine.strokeEnd = 1.0;
_lineChartLine.path = _lineChartPath.CGPath;
}
所以怎麼重新計算cotentSize呢!
這裏,視圖上默認顯示的點數爲20,那麼20個點所佔的width爲scrollView的frame中的width,總的數據佔content size中的_scrollView.contentSize的寬度 width = _scrollView.frame.size.width*(PointNumer/20)。這裏面是默認寬度,那麼在放大縮小的過程中我們其實只要改變20即可以。
CGFloat c = 20 / _scale;這就是最重要的控制顯示多少個點的方法,然後依次畫圖,並且重繪視圖即可以
三、如何定位最近的數據源
定位數據源的話,其實我這裏就是一個search一個,其實就是一個遍歷,這是剛開始的想法,講真,我覺得不是很好。所以沒什麼好講的
- (CGPoint)searchTouchNearPoint:(CGPoint)touchPoint{
NSArray *arr = [self subviews];
CGPoint nearPoint ;
CGFloat nearSpace = MAXFLOAT;
for (UIView *v in arr) {
if ([v isKindOfClass:[PXJLinePoint class]]) {
//當然只在
CGPoint point = v.center;
CGFloat space = (touchPoint.x-point.x)*(touchPoint.x-point.x)+(touchPoint.y-point.y)*(touchPoint.y-point.y);
if (nearSpace > space) {
nearSpace = space;
nearPoint = point;
searchPoint = (PXJLinePoint *)v;
if (nearSpace <= (self.PointSpaceX/2)*(self.PointSpaceX/2)) {
break;
}
}
}
}
return nearPoint;
}
其中有一個
if (nearSpace <= (self.PointSpaceX/2)*(self.PointSpaceX/2)) {
break;
}
我覺得其實是可以縮短遍歷時間的小方法
但是我想象着,如果只獲取顯示的point的話,並且只在這些points中遍歷,應該是一個一個更好的方法,畢竟你的點擊不會超出視圖
最後,我覺得根據數據源算出其它的數據,其實並不是很好,如果數據很大呢,所以這是改進的點;然後,畫圖時也應該需要顯示什麼就畫什麼,而不是畫出所有的部分。
第三個問題就是,縮放時,應該以縮放點進行縮放,所以實際上我只是改變了縮放的視圖的width,好吧,還需改進