Like the Health app on iOS 8 where null/empty data points are not displayed while X axis labels are still there. Using iOS-Charts as the chart library for my project is it possible to achieve the same?
You shouldn't pass a nil, it does not a accept a nil and Xcode will warn about it. What you should do, is just not pass it at all.
You do not have to pass a y value for every x index. You just have to make sure that the y values are ordered according to the x indices.
It can be achieved using iOS-Charts. I am currently making a project which requires various multiple line charts, multiple bar charts and line charts. Even if I have null values X axis labels are still there. You can ask me if you have any doubts.
Here's a function I wrote based on daniel.gindi's answer (I pass NaNs for the entries in the values array that are empty) :
func populateLineChartView(lineChartView: LineChartView, labels: [String], values: [Float]) {
var dataEntries: [ChartDataEntry] = []
for i in 0..<labels.count {
if !values[i].isNaN {
let dataEntry = ChartDataEntry(value: Double(values[i]), xIndex: i)
dataEntries.append(dataEntry)
}
}
let lineChartDataSet = LineChartDataSet(yVals: dataEntries, label: "Label")
let lineChartData = LineChartData(xVals: labels, dataSet: lineChartDataSet)
lineChartView.data = lineChartData
}
You can use the axis Maximum, so even if you don't have ChartDataEntry for that events it will still show
chartView.xAxis.axisMaximum = 60
chartView.xAxis.axisMaximum = 90
Simplest answer as Daniel mentioned is to just not pass the y value. Initially I was also confused as all functions needed x and y. This approach is how I did it
var dataEntries = [ChartDataEntry]()
for (iterating a source array){
let entry = ChartDataEntry()
if (some condition to check yValue exists) {
entry.x = xValue
entry.y = yValue
}
else
{
entry.x = xValue
}
dataEntries.append(entry)
}
Related
I have a Chart with WeatherKit.HourWeather objects spanning over multiple days on the x axis. However, I want to exclude the nighttime hours. It looks like I can do this with the chartXScale modifier like this:
let myDataSeperatedByHours = arrayWithAllDates.filter { ... }.sorted(...) // Array of WeatherKit.HourWeather objects filtered by isDaylight = true and sorted by date
let allDaytimeDates = myDataSeperatedByHours.map { $0.date } //only the Date objects
Chart {
ForEach(myDataSeperatedByHours, id: \.date) { hourData in
LineMark(
x: .value("hour", hourData.date),
y: .value("value", hourData.value)
)
}
}
.chartXAxis {
AxisMarks(position: .bottom, values: allDaytimeDates) { axisValue in
if let date = axisValue.as(Date.self) {
AxisValueLabel(
"\(Self.shortTimeFormatter.calendar.component(.hour, from: date))"
)
}
}
}
.chartXScale(domain: allDaytimeDates, type: .category)
However the Chart still displays part where there is no value. (the nighttime)
I want everything removed when there is night. I've marked it green on the image below. Maybe I have to use two Charts next to each other. One for every day, but I can't believe that there's no way to do it with one Chart only.
I've created an example app that you can download and test here: https://github.com/Iomegan/DateChartIssue
As per chart scale modifier documentation for domain parameter:
The possible data values along the x axis in the chart. You can define the domain with a ClosedRange for number or Date values (e.g., 0 ... 500), and with an array for categorical values (e.g., ["A", "B", "C"])
It seems for date type values this function is expecting a range but since you are specifying an array the method invocation traps.
Instead of providing the domain directly, you can provide an automatic scale domain modifying the inferred domain. To set the domain to your calculated allDaytimeDates use:
.chartXScale(domain: .automatic(dataType: Date.self) { dates in
dates = allDaytimeDates
})
Update 1
There are multiple approaches you can try to ignore night time date scale on X-axis. The simpler and not recommended approach is to provided X-axis value in your line mark as a String instead of a Date.
The issue with specifying X-axis value as Date is you can only supply a range for the axis scale and you can't just pick multiple ranges as scale for your axis as of now and similarly you can't specify your scale to ignore certain range or values (i.e. night time). With specifying X-axis value as string you will be able to just ignore night time values:
LineMark(
x: .value("hour", "\(hourData.date)"),
y: .value("value", hourData.value)
)
The demerit with this approach is temprature variations as obtained from this graph is wrong as all your data points will be just separated equally regardless of their date value.
The preferred approach is to manually adjust the X-axis position for next day's data points. For your scenario you can create a DayHourWeather type with custom X-position value:
struct DayHourWeather: Plottable {
let position: TimeInterval // Manually calculated X-axis position
let date: Date
let temp: Double
let series: String // The day this data belongs to
var primitivePlottable: TimeInterval { position }
init?(primitivePlottable: TimeInterval) { nil }
init(position: TimeInterval, date: Date, temp: Double, series: String) {
self.position = position
self.date = date
self.temp = temp
self.series = series
}
}
You can customize the position data to move daytime plots closer together ignoring night time values. Then you can create DayHourWeathers from your HourWeathers:
/// assumes `hourWeathers` are filtered containing only day time data and already sorted
func getDayHourWeathers(from hourWeathers: [HourWeather]) -> [DayHourWeather] {
let padding: TimeInterval = 10000 // distance between lat day's last data point and next day's first data point
var translation: TimeInterval = 0 // The negetive translation required on X-axis for certain day
var series: Int = 0 // Current day series
var result: [DayHourWeather] = []
result.reserveCapacity(hourWeathers.count)
for (index, hourWeather) in hourWeathers.enumerated() {
defer {
result.append(
.init(
position: hourWeather.date.timeIntervalSince1970 - translation,
date: hourWeather.date,
temp: hourWeather.temp,
series: "Day \(series + 1)"
)
)
}
guard
index > 0,
case let lastWeather = hourWeathers[index - 1],
!Calendar.current.isDate(lastWeather.date, inSameDayAs: hourWeather.date)
else { continue }
// move next day graph to left occupying previous day's night scale
translation = hourWeather.date.timeIntervalSince1970 - (result.last!.position + padding)
series += 1
}
return result
}
Now to plot your chart you can use the newly created DayHourWeather values:
var body: some View {
let dayWeathers = getDayHourWeathers(from: myDataSeperatedByHours)
Chart {
ForEach(dayWeathers, id: \.date) { hourData in
LineMark(
x: .value("hour", hourData.position), // custom X-axis position calculated
y: .value("value", hourData.temp)
)
.foregroundStyle(by: .value("Day", hourData.series))
}
}
.chartXScale(domain: dayWeathers.first!.position...dayWeathers.last!.position) // provide scale range for calculated custom X-axis positions
}
Note that with above changes your X-axis marker will display your custom X-axis positions. To change it back to the actual date label you want to display you can specify custom X-axis label:
.chartXAxis {
AxisMarks(position: .bottom, values: dayWeathers) {
AxisValueLabel(
"\(Self.shortTimeFormatter.calendar.component(.hour, from: dayWeathers[$0.index].date))"
)
}
}
The values argument for AxisMarks only accepts an array of Plottable items, this is why confirming DayHourWeather to Plottable is needed. After above changes the chart obtained will look similar to this:
Note that I have created a different series for each day data. Although you can combine them into a single series, I will advise against doing so as the resulting chart is misleading to viewer since you are removing part of the X-axis scale.
When creating a line chart from more than one data sets, the line chart only shows one of the data sets and when zooming or panning the chart it crashes with Fatal error: Can't form Range with upperBound < lowerBound.
If I create the line chart from one data set it works as expected.
This problem only occurs when the two datasets have completely different ranges of X values.
The code below should draw a chart with x ranging from 0 to 19 (i.e. 2 datasets). But it only draws the second dataset. The chart crashes if you pan or zoom it.
If I edit the code, replacing for x in (10..<20) with for x in (0..<10), both datasets are correctly drawn and the chart does not crash.
To summarise: when adding two dataSets that have entries with different ranges of X coordinates the chart draws incorrectly and will crash.
Is there an iOS_charts API call needed to prevent this? How can I draw two datasets that do not have overlapping X-coordinates?
I've been able to produce the same crash when running code using this demo code if I modify it to create multiple datasets that have non-overlapping x-coordinates.
class ElevationChartViewController: UIViewController {
#IBOutlet var chartView: LineChartView!
override func viewDidLoad() {
super.viewDidLoad()
chartView.backgroundColor = .white
chartView.legend.enabled = false
chartView.maxVisibleCount = 20000
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let dataSets = createChartDataSets()
chartView.data = LineChartData(dataSets: dataSets)
}
}
func createChartDataSets() -> [LineChartDataSet] {
var dataSets = [LineChartDataSet]()
var entriesOne = [ChartDataEntry]()
var entriesTwo = [ChartDataEntry]()
var y = 0.0
for x in (0..<10) {
entriesOne.append( ChartDataEntry(x: Double(x), y: y))
y = y + 10
if y > 60 {y = 0.0}
}
dataSets.append(LineChartDataSet(entriesOne))
for x in (10..<20) {
entriesTwo.append( ChartDataEntry(x: Double(x), y: y))
y = y + 10
if y > 50 {y = 0.0}
}
dataSets.append(LineChartDataSet(entriesTwo))
return dataSets
}
Swift version: 5.4
Xcode 12.4
Observed running on a real iPhone 12 sw version 14.4
Charts v4.0.1
I have been facing a similar issue and this solution worked for me so far. Not sure about potential side effects that could arise from this. I have not tested with panning or zooming.
Subclass LineChartDataSet and override entryIndex(x xValue:closestToY yValue:rounding) copy and paste the super implementation, but remove the guard statement at the top of the function
var closest = partitioningIndex { $0.x >= xValue }
guard closest < endIndex else { return -1 }
and replace with
var closest = partitioningIndex { $0.x >= xValue }
if closest >= endIndex {
closest = endIndex - 1
}
I have built a small application to measure Heart Rate (HR) and currently trying to implement a chart using iOS Charts. It is working as intended (getting HR from watch and displaying it), but I have a small problem with the design of the chart.
On the image below you can see that I have overlapping numbers above the data line (70, and the later 90). I do not know how to remove them.
Chart Image
Here is my setup for the chart and its' update counter function:
//chart set up
self.chtChart.delegate = self as? ChartViewDelegate
let set_a: LineChartDataSet = LineChartDataSet(entries:[ChartDataEntry(x: Double(0), y: self.valueHR)], label: "HR")
set_a.drawCirclesEnabled = false
set_a.setColor(UIColor.systemPink)
self.chtChart.xAxis.drawGridLinesEnabled = false
self.chtChart.rightAxis.drawLabelsEnabled = false
self.chtChart.xAxis.drawLabelsEnabled = false
self.chtChart.data = LineChartData(dataSets: [set_a])
// update counter
var i = 1
#objc func updateCounter() {
self.chtChart.data?.addEntry(ChartDataEntry(x: Double(i), y: valueHR), dataSetIndex: 0)
self.chtChart.setVisibleXRange(minXRange: Double(0), maxXRange: Double(1000))
self.chtChart.notifyDataSetChanged()
self.chtChart.moveViewToX(Double(i))
i = i + 1
}
I know that there is a duplicate question, but the solution with the formatter did not help me: nothing was changed after the implementation of the solution.
formatter solution
I am trying to plot points on lineChartView class. I then plotted data on it, but I see a very funny thing. The second plot is labeled, but not the first one.
I am setting up LineChartView instance and named it lineChart:
var lineChart: LineChartView = {
var l = LineChartView()
l.translatesAutoresizingMaskIntoConstraints = false
l.backgroundColor = .white
return l
}()
lineChartDataPoints holds ChartDataEntry classes, which holds x and y values:
var lineChartDataPoints: [ChartDataEntry] = []
I loop over xData and append ChartDataEntry class to lineChartDataPoints. xData and yData variable hold x and y values: (Which is generated in other function and not really a point of this question)
for i in 0..<xData.count {
let data = ChartDataEntry(x: Double(i + 1), y: Double(yData[i])!)
lineChartDataPoints.append(data)
}
Then I add lineChartDataPoints to LineChartDataSet:
let lineDataSet = LineChartDataSet(values: lineChartDataPoints, label: "Values")
Then lineDataSet is added to lineData after setting parameters.
lineDataSet.colors = [UIColor.red]
lineDataSet.lineWidth = 5
lineDataSet.circleColors = [UIColor.blue]
lineDataSet.circleRadius = 5
lineData.addDataSet(lineDataSet)
And this is obviously not because there is only one point. Because I tried this:
print("data \(lineChartDataPoints)")
// data [ChartDataEntry, x: 1.0, y 1.0, ChartDataEntry, x: 6.0, y 1.0]
There definitely are two points supplied for the graph to display, but I am pretty stumped on why first point won't be labeled with blue dot like second point.
Before installing the new Charts framework (Version 3.2) for Swift 5, I did not have this issue. There were some tweaks to be made in Charts framework. Installing newest patch (3.3) have solved this problem. So update to the latest version of this framework to avoid this problem.
Hi i need a graph as attached for ios. I am using ios-Chart library(swift alternative of MPAndroidChart) for swift .
I have managed to get these points on the graph using the scatter graph. But i couldn't figure out how will i connect the two vertical points. Any help or early response will be appreciate able.
my current code is :
func drawChart(dataPoints:[String] , value1 :[Double] , value2:[Double])
{
var dataEntries1:[ChartDataEntry] = []
for i in 0..<dataPoints.count {
let dataEntry = ChartDataEntry(value:value1[i] , xIndex : i)
dataEntries1.append(dataEntry)
}
var dataEntries2:[ChartDataEntry] = []
for i in 0..<dataPoints.count {
let dataEntry = ChartDataEntry(value:value2[i] , xIndex : i)
dataEntries2.append(dataEntry)
}
let dataSet1 = ScatterChartDataSet(yVals: dataEntries1, label: "Value1" )
dataSet1 .setColor(UIColor.blueColor())
let dataSet2 = ScatterChartDataSet(yVals: dataEntries2 ,label: "Value2")
dataSet2.setColor(UIColor.greenColor())
var bloodPressureDataSets = [ScatterChartDataSet]()
bloodPressureDataSets.append(dataSet1)
bloodPressureDataSets.append(dataSet2)
let barChartData = ScatterChartData(xVals: dataPoints, dataSets: bloodPressureDataSets)
bpChart.xAxis.labelPosition = .Bottom
bpChart.rightAxis.enabled=false
//barChart.legend.enabled=false
bpChart.descriptionText=""
bpChart.data = barChartData
}
Currently i can see this type of graph using the above code.
I want to join these two vertical points like the graph below,
take a look at scatter chart renderer, drawDataSet func. You can connect the dots there
UPDATE towards your comments:
first, go to ScatterChartRenderer and locate to
internal func drawDataSet(context context: CGContext, dataSet: ScatterChartDataSet)
This is where we calculate the position and draw the shape here
There is a main loop:
for (var j = 0, count = Int(min(ceil(CGFloat(entries.count) * _animator.phaseX), CGFloat(entries.count))); j < count; j++)
{
let e = entries[j]
point.x = CGFloat(e.xIndex)
point.y = CGFloat(e.value) * phaseY
point = CGPointApplyAffineTransform(point, valueToPixelMatrix);
...
}
Here's the iteration of the data entries your provide in the dataSet, we just get the xIndex and y value here and convert it to the coordinate on the screen.
So what you can do is to sub class this renderer, override this function to get what you want.
e.g. You want to connect the data entries(the dot) for the same xIndex, you should first iterate each data set to collect all the entries on same xIndex, use CGPointApplyAffineTransform(point, valueToPixelMatrix) to convert and use CoreGraphics APIs to draw the line. You don't need to worry about the math, the library already gives you the API to convert between data value and the screen coordinate value. You just focus on how to draw the chart.