Skip to content
This repository was archived by the owner on Dec 2, 2020. It is now read-only.

Commit 78c8cac

Browse files
committed
Fix assorted bugs in BEMSimpleLineGraph, especially null-data related
AverageLine: •Color should be picked up from line if not set (default nil) Typo in encoding macro Line: If null data and interpolation on, then extrapolate for beginning/ending nulls. Bezier curve should be used even when only two points. If interpolation off, then bezier line should be interrupted by gaps (but not top/bottom). Avoid infinity result in midPoint calculation SimpleLineGraphView: Support restoration during startup. NoDataLabel color should not default from Line (which defaults to white, same as background) If null value, ensure corresponding Dot isn't left on chart If label isn't used after initially being created, ensure it's removed from view If neither X nor Y reference lines, set line's enableRefLines to NO (although default, might have been previously YES If Xaxis background is defaulting to colorBottom, then also use alphaBottom to match; same for Yaxis and colorTop. Avoid possible infinite loop if delegate gives a zero incrementIndex for x axis Fix one-pixel gap between yaxis and graph Remove spurious NSLogs
1 parent 7dc9f3a commit 78c8cac

3 files changed

Lines changed: 137 additions & 69 deletions

File tree

Classes/BEMAverageLine.m

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ - (instancetype)init {
1414
self = [super init];
1515
if (self) {
1616
_enableAverageLine = NO;
17-
_color = [UIColor whiteColor];
1817
_alpha = 1.0;
1918
_width = 3.0;
2019
_yValue = NAN;
@@ -49,7 +48,7 @@ - (instancetype) initWithCoder:(NSCoder *)coder {
4948

5049
- (void) encodeWithCoder: (NSCoder *)coder {
5150

52-
#define EncodeProperty(property, type) [coder encode ## type :self.property forKey:@"property" ]
51+
#define EncodeProperty(property, type) [coder encode ## type: self.property forKey:@#property]
5352
EncodeProperty (enableAverageLine, Bool);
5453
EncodeProperty (color, Object);
5554
EncodeProperty (yValue, Float);

Classes/BEMLine.m

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -142,20 +142,56 @@ - (void)drawRect:(CGRect)rect {
142142

143143
self.points = [NSMutableArray arrayWithCapacity:self.arrayOfPoints.count];
144144
for (NSUInteger i = 0; i < self.arrayOfPoints.count; i++) {
145-
CGPoint value = CGPointMake(xIndexScale * i, [self.arrayOfPoints[i] CGFloatValue]);
146-
if (value.y < BEMNullGraphValue || !self.interpolateNullValues) {
147-
[self.points addObject:[NSValue valueWithCGPoint:value]];
148-
}
149-
}
145+
CGFloat value = [self.arrayOfPoints[i] CGFloatValue];;
146+
if (value >= BEMNullGraphValue && self.interpolateNullValues) {
147+
//need to interpolate. For midpoints, just don't add a point
148+
if (i ==0) {
149+
//extrapolate a left edge point from next two actual values
150+
NSUInteger firstPos = 1; //look for first real value
151+
while (firstPos < self.arrayOfPoints.count && [self.arrayOfPoints[firstPos] CGFloatValue] >= BEMNullGraphValue) firstPos++;
152+
if (firstPos >= self.arrayOfPoints.count) break; // all NaNs?? =>don't create any line
153+
154+
CGFloat firstValue = [self.arrayOfPoints[firstPos] CGFloatValue];
155+
NSUInteger secondPos = firstPos+1; //look for second real value
156+
while (secondPos < self.arrayOfPoints.count && [self.arrayOfPoints[secondPos] CGFloatValue] >= BEMNullGraphValue) secondPos++;
157+
if (secondPos >= self.arrayOfPoints.count) {
158+
// only one real number
159+
value = firstValue;
160+
} else {
161+
CGFloat delta = firstValue - [self.arrayOfPoints[secondPos] CGFloatValue];
162+
value = firstValue + firstPos*delta/(secondPos-firstPos);
163+
}
150164

151-
BOOL bezierStatus = self.bezierCurveIsEnabled;
152-
if (self.arrayOfPoints.count <= 2 && self.bezierCurveIsEnabled == YES) bezierStatus = NO;
165+
} else if (i == self.arrayOfPoints.count-1) {
166+
//extrapolate a right edge poit from previous two actual values
167+
NSInteger firstPos = i-1; //look for first real value
168+
while (firstPos >= 0 && [self.arrayOfPoints[firstPos] CGFloatValue] >= BEMNullGraphValue) firstPos--;
169+
if (firstPos < 0 ) continue; // all NaNs?? =>don't create any line; should already be gone
170+
171+
CGFloat firstValue = [self.arrayOfPoints[firstPos] CGFloatValue];
172+
NSInteger secondPos = firstPos-1; //look for second real value
173+
while (secondPos >= 0 && [self.arrayOfPoints[secondPos] CGFloatValue] >= BEMNullGraphValue) secondPos--;
174+
if (secondPos < 0) {
175+
// only one real number
176+
value = firstValue;
177+
} else {
178+
CGFloat delta = firstValue - [self.arrayOfPoints[secondPos] CGFloatValue];
179+
value = firstValue + (self.arrayOfPoints.count - firstPos-1)*delta/(firstPos - secondPos);
180+
}
153181

154-
if (!self.disableMainLine && bezierStatus) {
155-
line = [BEMLine quadCurvedPathWithPoints:self.points];
156-
fillBottom = [BEMLine quadCurvedPathWithPoints:self.bottomPointsArray];
157-
fillTop = [BEMLine quadCurvedPathWithPoints:self.topPointsArray];
158-
} else if (!self.disableMainLine && !bezierStatus) {
182+
} else {
183+
continue; //skip this (middle Null) point, let graphics handle interpolation
184+
}
185+
}
186+
CGPoint newPoint = CGPointMake(xIndexScale * i, value);
187+
[self.points addObject:[NSValue valueWithCGPoint:newPoint]];
188+
}
189+
190+
if (!self.disableMainLine && self.bezierCurveIsEnabled) {
191+
line = [BEMLine quadCurvedPathWithPoints:self.points open:YES];
192+
fillBottom = [BEMLine quadCurvedPathWithPoints:self.bottomPointsArray open:NO];
193+
fillTop = [BEMLine quadCurvedPathWithPoints:self.topPointsArray open:NO];
194+
} else if (!self.disableMainLine && !self.bezierCurveIsEnabled) {
159195
line = [BEMLine linesToPoints:self.points];
160196
fillBottom = [BEMLine linesToPoints:self.bottomPointsArray];
161197
fillTop = [BEMLine linesToPoints:self.topPointsArray];
@@ -317,34 +353,33 @@ + (UIBezierPath *)linesToPoints:(NSArray <NSValue *> *)points {
317353
return path;
318354
}
319355

320-
+ (UIBezierPath *)quadCurvedPathWithPoints:(NSArray <NSValue *> *)points {
356+
+ (UIBezierPath *)quadCurvedPathWithPoints:(NSArray <NSValue *> *)points open:(BOOL) canSkipPoints {
321357
UIBezierPath *path = [UIBezierPath bezierPath];
322358

323359
NSValue *value = points[0];
324360
CGPoint p1 = [value CGPointValue];
325361
[path moveToPoint:p1];
326362

327-
if (points.count == 2) {
328-
value = points[1];
329-
CGPoint p2 = [value CGPointValue];
330-
[path addLineToPoint:p2];
331-
return path;
332-
}
333-
334363
for (NSValue * point in points) {
364+
if (point == value) continue; //already at first point
335365
CGPoint p2 = [point CGPointValue];
336366

337-
CGPoint midPoint = midPointForPoints(p1, p2);
338-
[path addQuadCurveToPoint:midPoint controlPoint:controlPointForPoints(midPoint, p1)];
339-
[path addQuadCurveToPoint:p2 controlPoint:controlPointForPoints(midPoint, p2)];
340-
367+
if (canSkipPoints && (p1.y >= BEMNullGraphValue || p2.y >= BEMNullGraphValue)) {
368+
[path moveToPoint:p2];
369+
} else {
370+
CGPoint midPoint = midPointForPoints(p1, p2);
371+
[path addQuadCurveToPoint:midPoint controlPoint:controlPointForPoints(midPoint, p1)];
372+
[path addQuadCurveToPoint:p2 controlPoint:controlPointForPoints(midPoint, p2)];
373+
}
341374
p1 = p2;
342375
}
343376
return path;
344377
}
345378

346379
static CGPoint midPointForPoints(CGPoint p1, CGPoint p2) {
347-
return CGPointMake((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
380+
CGFloat avgY = (p1.y + p2.y) / 2.0;
381+
if (isinf(avgY)) avgY = BEMNullGraphValue;
382+
return CGPointMake((p1.x + p2.x) / 2, avgY);
348383
}
349384

350385
static CGPoint controlPointForPoints(CGPoint p1, CGPoint p2) {

Classes/BEMSimpleLineGraphView.m

Lines changed: 76 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,16 @@ - (instancetype) initWithFrame:(CGRect)frame {
129129
- (instancetype) initWithCoder:(NSCoder *)coder {
130130
self = [super initWithCoder:coder];
131131
if (self) [self commonInit];
132+
[self restorePropertyWithCoder:coder];
133+
return self;
134+
}
135+
136+
-(void) decodeRestorableStateWithCoder:(NSCoder *)coder {
137+
[super decodeRestorableStateWithCoder:coder];
138+
[self restorePropertyWithCoder:coder];
139+
}
140+
141+
-(void) restorePropertyWithCoder:(NSCoder *) coder {
132142

133143
#define RestoreProperty(property, type) \
134144
if ([coder containsValueForKey:@#property]) { \
@@ -179,23 +189,26 @@ - (instancetype) initWithCoder:(NSCoder *)coder {
179189
RestoreProperty (formatStringForValues, Object);
180190

181191
RestoreProperty (averageLine, Object);
182-
return self;
183192
#pragma clang diagnostic pop
184193
}
185194

186-
- (void) encodeWithEncoder: (NSCoder *)coder {
195+
-(void) encodeRestorableStateWithCoder:(NSCoder *)coder {
196+
[super encodeRestorableStateWithCoder:coder];
197+
[self encodePropertiesWithCoder:coder];
198+
}
187199

188-
#define EncodeProperty(property, type) [coder encode ## type: self.property forKey:@#property]
200+
- (void) encodeWithCoder: (NSCoder *)coder {
201+
[super encodeWithCoder:coder];
202+
[self encodePropertiesWithCoder:coder];
203+
}
189204

205+
-(void) encodePropertiesWithCoder: (NSCoder *) coder {
190206

191-
[super encodeWithCoder:coder];
207+
#define EncodeProperty(property, type) [coder encode ## type: self.property forKey:@#property]
192208

193209
EncodeProperty (labelFont, Object);
194210
EncodeProperty (animationGraphEntranceTime, Float);
195211
EncodeProperty (animationGraphStyle, Integer);
196-
EncodeProperty (enableReferenceAxisFrame, Bool);
197-
EncodeProperty (enableTopReferenceAxisFrameLine, Bool);
198-
EncodeProperty (enableRightReferenceAxisFrameLine, Bool);
199212

200213
EncodeProperty (colorXaxisLabel, Object);
201214
EncodeProperty (colorYaxisLabel, Object);
@@ -383,7 +396,7 @@ - (void)layoutNumberOfPoints {
383396
}
384397
self.noDataLabel.text = noDataText ?: NSLocalizedString(@"No Data", nil);
385398
self.noDataLabel.font = self.noDataLabelFont ?: [UIFont preferredFontForTextStyle:UIFontTextStyleCaption1];
386-
self.noDataLabel.textColor = self.noDataLabelColor ?: (self.colorLine ?: [UIColor blackColor]);
399+
self.noDataLabel.textColor = self.noDataLabelColor ?: (self.colorXaxisLabel ?: [UIColor blackColor]);
387400

388401
[self.viewForFirstBaselineLayout addSubview:self.noDataLabel];
389402

@@ -522,16 +535,20 @@ -(BEMCircle *) circleDotAtIndex:(NSUInteger) index forValue:(CGFloat) dotValue r
522535

523536
[yAxisValues addObject:@(positionOnYAxis)];
524537

538+
BEMCircle *circleDot = nil;
539+
if (reuseNumber < self.circleDots.count) {
540+
circleDot = self.circleDots[reuseNumber];
541+
}
525542
if (dotValue >= BEMNullGraphValue) {
526543
// If we're dealing with an null value, don't draw the dot (but put it in yAxis to interpolate line)
544+
[circleDot removeFromSuperview];
527545
return nil;
528546
}
529547

530-
BEMCircle *circleDot;
531548
CGRect dotFrame = CGRectMake(0, 0, self.sizePoint, self.sizePoint);
532-
if (reuseNumber < self.circleDots.count) {
533-
circleDot = self.circleDots[reuseNumber];
549+
if (circleDot) {
534550
circleDot.frame = dotFrame;
551+
[circleDot setNeedsDisplay];
535552
} else {
536553
circleDot = [[BEMCircle alloc] initWithFrame:dotFrame];
537554
[self.circleDots addObject:circleDot];
@@ -573,34 +590,37 @@ - (void)drawDots {
573590

574591
BEMCircle * circleDot = [self circleDotAtIndex: index forValue: dotValue reuseNumber: index];
575592
UILabel * label = nil;
593+
if (index < self.permanentPopups.count) {
594+
label = self.permanentPopups[index];
595+
} else {
596+
label = [[UILabel alloc] initWithFrame:CGRectZero];
597+
[self.permanentPopups addObject:label ];
598+
}
599+
576600
if (circleDot) {
577601
[self addSubview:circleDot];
578602

579-
if (self.alwaysDisplayPopUpLabels == YES) {
580-
if (![self.delegate respondsToSelector:@selector(lineGraph:alwaysDisplayPopUpAtIndex:)] ||
581-
[self.delegate lineGraph:self alwaysDisplayPopUpAtIndex:index]) {
582-
if (index < self.permanentPopups.count) {
583-
label = self.permanentPopups[index];
584-
} else {
585-
label = [[UILabel alloc] initWithFrame:CGRectZero];
586-
[self.permanentPopups addObject:label ];
587-
}
588-
label = [self configureLabel:label forPoint: circleDot ];
589-
590-
[self adjustXLocForLabel:label avoidingDot:circleDot.frame];
591-
592-
UILabel * leftNeighbor = (index >= 1 && self.permanentPopups[index-1].superview) ? self.permanentPopups[index-1] : nil;
593-
UILabel * secondNeighbor = (index >= 2 && self.permanentPopups[index-2].superview) ? self.permanentPopups[index-2] : nil;
594-
BOOL showLabel = [self adjustYLocForLabel:label
595-
avoidingDot:circleDot.frame
596-
andNeighbors:leftNeighbor.frame
597-
and:secondNeighbor.frame ];
598-
if (showLabel) {
599-
[self addSubview:label];
600-
} else {
601-
[label removeFromSuperview];
602-
}
603+
if ((self.alwaysDisplayPopUpLabels == YES) &&
604+
(![self.delegate respondsToSelector:@selector(lineGraph:alwaysDisplayPopUpAtIndex:)] ||
605+
[self.delegate lineGraph:self alwaysDisplayPopUpAtIndex:index])) {
606+
label = [self configureLabel:label forPoint: circleDot ];
607+
608+
[self adjustXLocForLabel:label avoidingDot:circleDot.frame];
609+
610+
UILabel * leftNeighbor = (index >= 1 && self.permanentPopups[index-1].superview) ? self.permanentPopups[index-1] : nil;
611+
UILabel * secondNeighbor = (index >= 2 && self.permanentPopups[index-2].superview) ? self.permanentPopups[index-2] : nil;
612+
BOOL showLabel = [self adjustYLocForLabel:label
613+
avoidingDot:circleDot.frame
614+
andNeighbors:leftNeighbor.frame
615+
and:secondNeighbor.frame ];
616+
if (showLabel) {
617+
[self addSubview:label];
618+
} else {
619+
[label removeFromSuperview];
603620
}
621+
} else {
622+
//not showing labels this time, so remove if any
623+
[label removeFromSuperview];
604624
}
605625

606626
// Dot and/or label entrance animation
@@ -628,6 +648,8 @@ - (void)drawDots {
628648
} completion:nil];
629649
}
630650

651+
} else {
652+
[label removeFromSuperview];
631653
}
632654
}
633655
for (NSUInteger i = self.circleDots.count -1; i>=numberOfPoints; i--) {
@@ -679,6 +701,8 @@ - (void)drawLine {
679701
line.verticalReferenceHorizontalFringeNegation = xAxisHorizontalFringeNegationValue;
680702
line.arrayOfVerticalReferenceLinePoints = self.enableReferenceXAxisLines ? xAxisLabelPoints : nil;
681703
line.arrayOfHorizontalReferenceLinePoints = self.enableReferenceYAxisLines ? yAxisLabelPoints : nil;
704+
} else {
705+
line.enableReferenceLines = NO;
682706
}
683707

684708
line.color = self.colorLine;
@@ -726,8 +750,13 @@ - (void)drawXAxis {
726750
}
727751
[self addSubview:self.backgroundXAxis];
728752

729-
self.backgroundXAxis.backgroundColor = self.colorBackgroundXaxis ?: self.colorBottom;
730-
self.backgroundXAxis.alpha = self.alphaBackgroundXaxis;
753+
if (self.colorBackgroundXaxis) {
754+
self.backgroundXAxis.backgroundColor = self.colorBackgroundXaxis;
755+
self.backgroundXAxis.alpha = self.alphaBackgroundXaxis;
756+
} else {
757+
self.backgroundXAxis.backgroundColor = self.colorBottom;
758+
self.backgroundXAxis.alpha = self.alphaBottom;
759+
}
731760

732761
NSArray <NSNumber *> *axisIndices = nil;
733762
if ([self.delegate respondsToSelector:@selector(incrementPositionsForXAxisOnLineGraph:)]) {
@@ -751,6 +780,7 @@ - (void)drawXAxis {
751780
baseIndex = increment - 1 - offset;
752781
}
753782
}
783+
if (increment == 0) increment = 1;
754784
NSMutableArray <NSNumber *> *values = [NSMutableArray array ];
755785
NSUInteger index = baseIndex;
756786
while (index < numberOfPoints) {
@@ -933,7 +963,7 @@ - (void)drawYAxis {
933963
self.frame.size.width - self.YAxisLabelXOffset - 1.0f:
934964
0.0),
935965
0,
936-
self.YAxisLabelXOffset - 1.0f,
966+
self.YAxisLabelXOffset,
937967
self.frame.size.height);
938968

939969
if (!self.backgroundYAxis) {
@@ -942,8 +972,13 @@ - (void)drawYAxis {
942972
self.backgroundYAxis.frame = frameForBackgroundYAxis;
943973
}
944974
[self addSubview:self.backgroundYAxis];
945-
self.backgroundYAxis.backgroundColor = self.colorBackgroundYaxis ?: self.colorTop;
946-
self.backgroundYAxis.alpha = self.alphaBackgroundYaxis;
975+
if (self.colorBackgroundYaxis) {
976+
self.backgroundYAxis.backgroundColor = self.colorBackgroundYaxis;
977+
self.backgroundYAxis.alpha = self.alphaBackgroundYaxis;
978+
} else {
979+
self.backgroundYAxis.backgroundColor = self.colorTop;
980+
self.backgroundYAxis.alpha = self.alphaTop;
981+
}
947982

948983
[yAxisLabelPoints removeAllObjects];
949984

@@ -1114,14 +1149,13 @@ - (UILabel *)configureLabel: (UILabel *) oldLabel forPoint: (BEMCircle *)circleD
11141149
NSNumber *value = (index <= dataPoints.count) ? value = dataPoints[index] : @(0); // @((NSInteger) circleDot.absoluteValue)
11151150
#pragma clang diagnostic push
11161151
#pragma clang diagnostic ignored "-Wformat-nonliteral"
1152+
//note this can indeed crash if delegate provides junk for formatString (e.g. %@); try/catch doesn't work
11171153
NSString *formattedValue = [NSString stringWithFormat:self.formatStringForValues, value.doubleValue];
11181154
#pragma clang diagnostic pop
11191155
newPopUpLabel.text = [NSString stringWithFormat:@"%@%@%@", prefix, formattedValue, suffix];
11201156
}
1121-
NSLog(@"%@ before SizeToFit: %@",newPopUpLabel.text, NSStringFromCGRect(newPopUpLabel.frame));
11221157
CGSize requiredSize = [newPopUpLabel sizeThatFits:CGSizeMake(100.0f, CGFLOAT_MAX)];
11231158
newPopUpLabel.frame = CGRectMake(10, 10, requiredSize.width+10.0f, requiredSize.height+10.0f);
1124-
NSLog(@"%@ after SizeToFit: %@",newPopUpLabel.text, NSStringFromCGRect(newPopUpLabel.frame));
11251159
return newPopUpLabel;
11261160
}
11271161

0 commit comments

Comments
 (0)