Background:
The graphs that I've added to my app plot data in realtime. Data is sent to the app over a BLE connection and I plot the data on the graph as it comes in. The graphs start off with no data in them. All of the graphs plot similar sets of data, just for different components, so if I can get one working they should all work.
A graph has two line series that plot readings from two different sensors. The are two y-axes on the graph, each plotting the reading from one of the sensors. The x-axis is a time series, it's values should correspond with when the readings come in over BLE.
Currently, I can't seem to get the graph to plot new points when I add a new data. If I pre-populate my data array with datapoints before i create the datasource helper object, those points are plotted on the graph correctly. Here is the relevant code for one of the line series:
@objcMembers class DataModel: NSObject { var date: Date! var value: NSNumber! init(date: Date, value: NSNumber) { self.date = date self.value = value } }
When data comes in for one of the plots, it's stored as a DataModel object and added to an array like:
var oilTempReadings: [DataModel] = []
These are the class types I'm using for my axes and data series:
var xAxisTemp: IGCategoryDateTimeXAxis! var yAxisTemp: IGNumericYAxis! var temperatureLineSeries: IGLineSeries! var temperatureDataSource: IGCategoryDateSeriesDataSourceHelper!
And this is how I'm setting up my datasource helper:
temperatureDataSource = IGCategoryDateSeriesDataSourceHelper.init(data: bleManager.coolantTempReadings, valuePath: "value", andDatePath: "date") temperatureLineSeries.yAxis = yAxisTemp temperatureLineSeries.xAxis = xAxisTemp temperatureLineSeries.dataSource = temperatureDataSource graph.add(xAxisTemp) graph.add(yAxisTemp) graph.addSeries(temperatureLineSeries)
If I have test data in the coolantTempReadings array at this point, they are immediately populated in the graph when I add the graph to the subview. However, when new data comes in and I try to insert it into the graph, it doesn't show up. This is how I'm trying to insert the new data:
graph.insertItem(at: bleManager.coolantTempReadings.count - 1, with: temperatureDataSource)
Any ideas why the graphs aren't updating with the new data?
When I was first playing around with the graphing package, before reading up on how to update data in realtime, I was doing this each time a new data point came in (I was only plotting data values in a number array at that point, not DataModel objects):
temperatureDataSource.values = bleManager.coolantTempReadings temperatureLineSeries.dataSource = temperatureDataSource doorDataSource.values = bleManager.coolantDoorReadings doorLineSeries.dataSource = doorDataSource graph.refresh()
This worked well until the app had been running a while. New data points come in about every second, so this would slowly bog down the CPU and then eventually crash the app.
When you update your data with the new readings, are you only updating one of the two series? I think the reason you're not seeing any updates might be because of this. When the chart shows multiple series it expects them to have the same number of data points and will fall back on the lowest point count. Try adding readings to both series, or if there isn't a valid value to add, add a nan to the other series.
Using refresh forces the chart to update everything, which is more suitable when the entire datasource changes, but should still work here. The slowdown could indicate a memory leak, but this will require some profiling.
I switched back to my branch that was using the refresh() function after each new data point came in and did some profiling. There don't seem to be any memory leaks; CoreGraphics allocates a lot of memory over time, but it's being consistently released and the amount persistent memory being used is staying fairly static.
I had to disable the "Record reference counts" option in the profiler because I'm running in to this issue https://forums.developer.apple.com/thread/97592 so hopefully that's not skewing my tests. But looking back on previous crash logs and watching the performance while profiling, the issue seems to be the CPU usage. All crash logs on this branch list "CPU Usage" as the crash reason, and I can see CPU usage slowly increase over time while refreshing the graphs. Commenting out the graph.refresh() function stops it from crashing.
I suspect that forcing a graph refresh every second on multiple graphs just ends up being too much to render as the amount of data increases in the graphs.
I've considered using the refresh() method, but at the same time dropping the oldest data point off of my array every time a new one comes in, once there's a decent amount of data in the graph. This should give it the effect of a "moving" graph, which I eventually want anyway. But if possible, I'd like to get it working the recommended way using graph.insertItem(at:), so that I don't have to worry how older devices will handle the refresh() method.
OK thanks, glad I'm not crazy. I'll try it out using the refresh() method with fewer data points.
Could've sworn this worked in earlier versions of Swift, but you're right, the data in the DSH and chartData arrays are not the same. You might have to resort to calling refresh on the chart instead of insert.
I've rewritten my code from your example, and now the graph is adding new points. However, all of the values are 0.
Graph creation:
graph = IGChartView.init(frame: CGRect.init(x: 0, y: 0, width: container.frame.width, height: container.frame.height)) graph.autoresizingMask = [UIViewAutoresizing.flexibleHeight, UIViewAutoresizing.flexibleWidth] graph.theme = IGChartDefaultThemes.defaultTheme() graph.gridMode = IGGridMode.behindSeries graph.zoomDisplayType = IGChartZoom.horizontal temperatureDataSource = IGCategoryDateSeriesDataSourceHelper.init(data: bleManager.coolantTempReadings, valuePath: "value", andDatePath: "date") doorDataSource = IGCategoryDateSeriesDataSourceHelper.init(data: bleManager.coolantDoorReadings, valuePath: "value", andDatePath: "date") graph.delegate = self graph.addSeries(forType: IGLineSeries.self, usingKey: "tempSeries", with: temperatureDataSource, firstAxisKey: "x", secondAxisKey: "tempY") graph.addSeries(forType: IGLineSeries.self, usingKey: "doorSeries", with: doorDataSource, firstAxisKey: "x", secondAxisKey: "doorY") let tempY: IGNumericYAxis = graph.findAxis(byKey: "tempY") as! IGNumericYAxis tempY.labelsLocation = IGAxisLabelsLocation.outsideLeft tempY.minimum = 0 tempY.maximum = 140 let doorY: IGNumericYAxis = graph.findAxis(byKey: "doorY") as! IGNumericYAxis doorY.labelsLocation = IGAxisLabelsLocation.outsideRight doorY.minimum = 0 doorY.maximum = 100 doorY.majorStrokeThickness = 0 graphContainer.addSubview(graph)
When new values come in:
graph.insertItem(at: bleManager.coolantDoorReadings.count - 1, with: doorDataSource)
I'm logging the values to the console, they aren't 0. It seems that the datasources values aren't getting updated when their associated data object is. Maybe it's an objective-c to swift translation issue?
Separate Y axes shouldn't make a difference. This new code will put 2 separate Y axes in the chart, each with a different range.
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. _chartData = [[NSMutableArray alloc]init]; _dsh1 = [[IGCategoryDateSeriesDataSourceHelper alloc]initWithData:_chartData valuePath:@"value1" andDatePath:@"date1"]; _dsh2 = [[IGCategoryDateSeriesDataSourceHelper alloc]initWithData:_chartData valuePath:@"value2" andDatePath:@"date2"]; _chart = [[IGChartView alloc]initWithFrame:self.view.bounds]; _chart.delegate = self; [_chart addSeriesForType:[IGLineSeries class] usingKey:@"series1" withDataSource:_dsh1 firstAxisKey:@"x" secondAxisKey:@"y1"]; [_chart addSeriesForType:[IGLineSeries class] usingKey:@"series2" withDataSource:_dsh2 firstAxisKey:@"x" secondAxisKey:@"y2"]; IGNumericYAxis *y1 = (IGNumericYAxis*)[_chart findAxisByKey:@"y1"]; y1.labelsLocation = IGAxisLabelsLocationOutsideLeft; y1.minimum = 0; y1.maximum = 10; IGNumericYAxis *y2 = (IGNumericYAxis*)[_chart findAxisByKey:@"y2"]; y2.labelsLocation = IGAxisLabelsLocationOutsideRight; y2.minimum = 10; y2.maximum = 100; y2.majorStrokeThickness = 0; [self.view addSubview:_chart]; _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTick) userInfo:nil repeats:YES]; } -(void)timerTick { ChartData *obj = [[ChartData alloc]init]; obj.date1 = obj.date2 = [NSDate new]; obj.value1 = arc4random()%10; obj.value2 = arc4random()%90 + 10; [_chartData addObject:obj]; NSInteger index = _chartData.count-1; [_chart insertItemAtIndex:index withSource:_dsh1]; [_chart insertItemAtIndex:index withSource:_dsh2]; }
It looks like your example is using the same x and y axes for both series, whereas my chart is plotting the values for each series on its own y-axis (one is a value and one is a percentage, so they don't use the same scale). So maybe that's why yours works and mine doesn't?
Is there something special I need to do when using different axes? I tried creating them with separate y-axes but sharing the same x-axis, but then one of the line series just didn't show up at all, and the one that did get drawn still didn't update with new values.