iOS Charts 3 - Align X Labels (dates) with plots - charts

I have found a similar question on here, but the answer did not help me. I think due to my data structure.
I have an array made up of individual arrays, each for its own line in the chart. this is then made up of multiple structs for the plot points of that line.
My issue is that values/lines are correct, but are not aligning correctly with the dates. In the example below. the dates start at 3rd May and end 8th May. Please help
here is my code
struct chartPoint {
let date:String
var total:Double
let exercise:String
}
var sets:[[chartPoint]] = []
func setupLineChart() {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
var dataSets:[LineChartDataSet] = []
var color:[UIColor] = [UIColor.red,UIColor.blue, UIColor.green,UIColor.red,UIColor.red,UIColor.red,UIColor.red]
for i in sets { //Array of array of structs
let sort = i.sorted { // sort the internal array by date
item1, item2 in
let date1 = dateFormatter.date(from:item1.date)
let date2 = dateFormatter.date(from:item2.date)
return date1!.compare(date2!) == ComparisonResult.orderedAscending
}
var dataEntries: [ChartDataEntry] = []
for stat in 0...(sort.count - 1) {
let date = dateFormatter.date(from:sort[stat].date)
let timeIntervalForDate: TimeInterval = date!.timeIntervalSince1970
let dataEntry = ChartDataEntry(x: Double(timeIntervalForDate), y: sort[stat].total)
dataEntries.append(dataEntry)
if stat == (sort.count - 1){
let chartDataSet = LineChartDataSet(values: dataEntries, label: "\(sort[stat].exercise)")
chartDataSet.setCircleColor(color[stat])
chartDataSet.setColor(color[stat], alpha: 1.0)
chartDataSet.drawValuesEnabled = true
dataSets.append(chartDataSet)
startChart(dataSets: dataSets)
}
}
}
}
func startChart(dataSets:[LineChartDataSet]){
testLineChartView.animate(xAxisDuration: 0.7, yAxisDuration: 0.7)
testLineChartView.dragEnabled = true
testLineChartView.legend.form = .circle
testLineChartView.drawGridBackgroundEnabled = false
let xaxis = testLineChartView.xAxis
xaxis.valueFormatter = axisFormatDelegate
xaxis.labelCount = dataSets.count
xaxis.granularityEnabled = true
xaxis.granularity = 1.0
xaxis.centerAxisLabelsEnabled = true
xaxis.avoidFirstLastClippingEnabled = true
xaxis.drawLimitLinesBehindDataEnabled = true
let rightAxis = testLineChartView.rightAxis
rightAxis.enabled = false
let leftAxis = testLineChartView.leftAxis
leftAxis.drawZeroLineEnabled = true
leftAxis.drawGridLinesEnabled = true
axisFormatDelegate = self
testLineChartView.delegate = self
let chartData = LineChartData(dataSets: dataSets)
testLineChartView.data = chartData
testLineChartView.chartDescription?.text = ""
}
extension ChartViewController: IAxisValueFormatter {
func stringForValue(_ value: Double, axis: AxisBase?) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd MMM"
let date = Date(timeIntervalSince1970: value)
return dateFormatter.string(from: date)
}
}

I am using dates on the x-axis as well in one of my projects and I simply changed each date to a string and passed an array of string values to an IndexAxisValueFormatter.
testLineChartView.xAxis.valueFormatter = IndexAxisValueFormatter(values: xvalues)
If this is not what you are looking for, can you show a sample of what "sets" includes? Then I will be able to run your code.

Here is another thread I started with an issue around a single value not showing. It includes the code that also fixed this.
I think the main point being
xaxis.forceLabelsEnabled = true
iOS Charts - single values not showing Swift

Related

iOS Charts: Where are the values actually coming from?

I have the following Xaxis formatter:
extension ChartXAxisFormatter: IAxisValueFormatter {
func stringForValue(_ value: Double, axis: AxisBase?) -> String {
guard let referenceTimeInterval = referenceTimeInterval,
let selectedFrame = selectedFrame
else {
return ""
}
let date = Date(timeIntervalSince1970: value * 3600 * 24 + referenceTimeInterval)
if(selectedFrame == .week) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "E"
return dateFormatter.string(from: date).capitalized
}
else if(selectedFrame == .month) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd"
return dateFormatter.string(from: date).capitalized
}
else {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM"
return dateFormatter.string(from: date).capitalized
}
}
}
And I call it like so: xAxis.valueFormatter = ChartXAxisFormatter(referenceTimeInterval: referenceTimeInterval, selectedFrame: selectedFrame)
However, this is just formatting the values, it isn't actually deciding what values appear in the xAxis.
When I run it, I get something like: 5 5 6 6 6 7
I can fix this by setting xAxis.granularity = 1 but I get 5 6 without the 7.
What is actually deciding what values appear? How can I get just the unique values?
For example if I had access to what values it uses I can call something like Array(Set(array))

Custom xAxis formatting in iOS Charts library

I'm using the following library to implement charts in my iOS app:
https://github.com/danielgindi/Charts
On the xAxis I would like to have a formatting similar to the one on the screenshot.
I would like to display the name of the month once, followed by days of that month (without month name), and when the months changes, display the name again.
Can someone point to the right direction?
Current code for xAxis configuration, which shows month and day for each entry:
xAxis.drawAxisLineEnabled = true
xAxis.drawGridLinesEnabled = false
xAxis.centerAxisLabelsEnabled = true
xAxis.drawLabelsEnabled = true
xAxis.axisLineColor = R.Color.Chart.Alpha.separator
xAxis.labelTextColor = R.Color.Chart.Alpha.content
xAxis.labelPosition = .bottom
xAxis.labelCount = 5
xAxis.valueFormatter = XAxisValueFormatter()
class XAxisValueFormatter: AxisValueFormatter {
func stringForValue(_ value: Double, axis: AxisBase?) -> String {
let date = Date(timeIntervalSince1970: value)
return date.to(DateFormat.monthday)
}
}
You need to update your existing code as below.
Update your data binding for XAxis:
let chartFormatter = LineChartFormatter(labels: xValues)
let xAxis = XAxis()
xAxis.valueFormatter = chartFormatter
self.xAxis.valueFormatter = xAxis.valueFormatter
Update Value Formatter code:
private class LineChartFormatter: NSObject, IAxisValueFormatter {
var labels: [String] = []
let dateFormatter = DateFormatter()
let dateShortFormatter = DateFormatter()
func stringForValue(_ value: Double, axis: AxisBase?) -> String {
if let date = dateFormatter.date(from:labels[Int(value)]) {
if value == 0 {
dateShortFormatter.dateFormat = "MMM dd"
return dateShortFormatter.string(from: date)
} else {
let prevDate = dateFormatter.date(from:labels[Int(value - 1)])
dateShortFormatter.dateFormat = "MMM"
if dateShortFormatter.string(from: date) != dateShortFormatter.string(from: prevDate!) {
dateShortFormatter.dateFormat = "MMM dd"
return dateShortFormatter.string(from: date)
} else {
dateShortFormatter.dateFormat = "dd"
return dateShortFormatter.string(from: date)
}
}
} else {
return labels[Int(value)]
}
}
init(labels: [String]) {
super.init()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
self.labels = labels
}}
By Above code change, you will achieve your Date formatting in XAxis.

Charts' line chart doesn't render line chart properly

I'm using the Charts framework and I'm experiencing some very weird behavior in my line chart.
When I segue to the ChartViewContoller and the default selection has data, the chart renders normally:
but if I segue to this view controller when the default selection doesn't have any data and then select an item that has data, it looks like this:
1) segue to this:
2) then select an item that has data:
Of course viewDidLoad is called when I segue to the view controller and as long as the default selection has data when I segue to it, I can select another item that has data or doesn't and the chart will continue to render properly. So the difference appears to be in viewDidLoad but I've tried everything I can think of but nothing fixes the problem. Here's my viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(hexString: "232B35")
self.title = "1RM"
chartView.delegate = self
chartView.chartDescription?.enabled = false
let leftAxis = chartView.leftAxis
leftAxis.axisMinimum = 190
leftAxis.labelTextColor = NSUIColor.white
let xAxis = chartView.xAxis
xAxis.labelPosition = .bottom
xAxis.axisMinimum = 0
xAxis.granularity = 1
xAxis.axisLineWidth = 5
xAxis.valueFormatter = self
xAxis.labelTextColor = NSUIColor.white
chartView.configureDefaults()
chartView.rightAxis.enabled = false // this fixed the extra xAxis grid lines
chartView.backgroundColor = NSUIColor(red: 35/255.0, green: 43/255.0, blue: 53/255.0, alpha: 1.0)
fetchData()
chartView.setVisibleXRangeMaximum(7)
chartView.animate(yAxisDuration: 1.0)
}
here's what's happening in fetchData():
func fetchData() {
chartView.data = nil
let liftName = UserDefaults.selectedLiftForChart()
let liftEvents = dataManager.fetchLiftsEventsOfTypeByName(liftName)
guard liftEvents.count > 0 else {
chartView.noDataText = "There's no \(liftName) data to display"
shouldHideData = true
return }
// put them into a Dictionary grouped by each unique day
let groupedEvents = Dictionary(grouping: liftEvents, by: { floor($0.date.timeIntervalSince1970 / 86400) })
// grab the maximum 1RM from each day
let dailyMaximums = groupedEvents.map { $1.max(by: { $0.oneRepMax < $1.oneRepMax }) }
// MARK: - TODO: Fix the silly unwrapping
sortedLiftEvents = dailyMaximums.sorted(by: { $0?.date.compare(($1?.date)!) == .orderedAscending }) as! [LiftEvent]
let intervalBetweenDates: TimeInterval = 3600 * 24 // 3600 = 1 hour
let startDate = (sortedLiftEvents.first?.date)! - intervalBetweenDates
let lastDate = sortedLiftEvents.last?.date
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM d"
let dates:[Date] = intervalDates(from: startDate, to: lastDate!, with: intervalBetweenDates)
days = dates.map {dateFormatter.string(from: $0)}
generateLineData()
}
and finally, this is the generateLineData method:
func fetchData() {
chartView.data = nil
let liftName = UserDefaults.selectedLiftForChart()
let liftEvents = dataManager.fetchLiftsEventsOfTypeByName(liftName)
guard liftEvents.count > 0 else {
chartView.noDataText = "There's no \(liftName) data to display"
shouldHideData = true
return }
// put them into a Dictionary grouped by each unique day
let groupedEvents = Dictionary(grouping: liftEvents, by: { floor($0.date.timeIntervalSince1970 / 86400) })
// grab the maximum 1RM from each day
let dailyMaximums = groupedEvents.map { $1.max(by: { $0.oneRepMax < $1.oneRepMax }) }
// MARK: - TODO: Fix the silly unwrapping
sortedLiftEvents = dailyMaximums.sorted(by: { $0?.date.compare(($1?.date)!) == .orderedAscending }) as! [LiftEvent]
let intervalBetweenDates: TimeInterval = 3600 * 24 // 3600 = 1 hour
let startDate = (sortedLiftEvents.first?.date)! - intervalBetweenDates
let lastDate = sortedLiftEvents.last?.date
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM d"
let dates:[Date] = intervalDates(from: startDate, to: lastDate!, with: intervalBetweenDates)
days = dates.map {dateFormatter.string(from: $0)}
generateLineData()
}
I've tried putting chartView.setVisibleXRangeMaximum(7) in the method that sets the chart data and verified that chartView.visibleXRange is 7 each time the chart is rendered but it doesn't make a difference. I've also made sure that the max XRange is being set after the data is set for the chart.
Is there anything else I can try or is this perhaps a bug that hasn't been fixed yet?
Thanks
Well I finally figured it out. I knew from reading the documentation that some properties must be set after the chart data is handed to the chart. It was not entirely clear which properties but through lots of debugging and process of elimination I determined it was the xAxis properties that needed to be reset whenever the data changed.
Now, when the data is changed I call my new function:
func resetxAxis() {
let xAxis = chartView.xAxis
xAxis.labelPosition = .bottom
xAxis.axisMinimum = 0
xAxis.granularity = 1
xAxis.axisLineWidth = 5
xAxis.valueFormatter = self
}
This had been in my viewDidLoad method so I made the above method out of it and can call it any time it's needed.

iOS Charts remove decimal from yValues

How to remove decimals from y values in iOS Charts?
Im using the latest iOS Charts release with Swift3
Thanks for every one who tried to help, here was the fix, adding the below code
let formatter = NumberFormatter()
formatter.numberStyle = .none
formatter.maximumFractionDigits = 0
formatter.multiplier = 1.0
chartData.valueFormatter = DefaultValueFormatter(formatter: formatter)
to the setBarChartData func
func setBarChartData(xValues: [String], yValues: [Double], label: String) {
var dataEntries: [BarChartDataEntry] = []
for i in 0..<yValues.count {
let dataEntry = BarChartDataEntry(x: Double(i), y: yValues[i])
dataEntries.append(dataEntry)
}
let chartDataSet = BarChartDataSet(values: dataEntries, label: label)
let chartData = BarChartData(dataSet: chartDataSet)
let formatter = NumberFormatter()
formatter.numberStyle = .none
formatter.maximumFractionDigits = 0
formatter.multiplier = 1.0
chartData.valueFormatter = DefaultValueFormatter(formatter: formatter)
let chartFormatter = BarChartFormatter(labels: xValues)
let xAxis = XAxis()
xAxis.valueFormatter = chartFormatter
self.xAxis.valueFormatter = xAxis.valueFormatter
self.data = chartData
}
Could also use:
let format = NumberFormatter()
format.numberStyle = .none
let formatter = DefaultValueFormatter(formatter: format)
data.setValueFormatter(formatter)
You need to set delegate for value formatter in DataSet like below
Obj-C :
//DataSet 1
LineChartDataSet *set1 = [[LineChartDataSet alloc] initWithValues:values label:#"outstanding"];
set1.valueFormatter = self;
Use below delegate method for formatting your value :
#pragma mark - IChartValueFormatter
- (NSString * _Nonnull)stringForValue:(double)value entry:(ChartDataEntry * _Nonnull)entry dataSetIndex:(NSInteger)dataSetIndex viewPortHandler:(ChartViewPortHandler * _Nullable)viewPortHandler{
//Format your value what you want here
return [NSString stringWithFormat:#"%0.f",value];
}
Confirm Protocol :
#interface YourViewController ()<ChartViewDelegate,IChartValueFormatter>
For Swift you need to create Extension of BarChart and use below methods in it
Swift :
extension BarChartView {
private class BarChartFormatter: NSObject, IAxisValueFormatter {
var labels: [String] = []
func stringForValue(_ value: Double, axis: AxisBase?) -> String {
return labels[Int(value)]
}
init(labels: [String]) {
super.init()
self.labels = labels
}
}
func setBarChartData(xValues: [String], yValues: [Double], label: String) {
var dataEntries: [BarChartDataEntry] = []
for i in 0..<yValues.count {
let dataEntry = BarChartDataEntry(x: Double(i), y: yValues[i])
dataEntries.append(dataEntry)
}
let chartDataSet = BarChartDataSet(values: dataEntries, label: label)
let chartData = BarChartData(dataSet: chartDataSet)
let chartFormatter = BarChartFormatter(labels: xValues)
let xAxis = XAxis()
xAxis.valueFormatter = chartFormatter
self.xAxis.valueFormatter = xAxis.valueFormatter
self.data = chartData
}
}
Call Above Extension Method like this :
func setChart(){
let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
let unitsSold = [20.0, 4.0, 3.0, 6.0, 12.0, 16.0, 4.0, 18.0, 2.0, 4.0, 5.0, 4.0]
barChartView.setBarChartData(xValues: months, yValues: unitsSold, label: "Monthly Sales")
}
hope you will get your formatted value on line chart.
After lots of research and test. i was able to remove the decimal.
First unlock the pod chartViewBase
Remove two lines from the { } i.e 'digits' from chartViewBase pod.
Before:
if let formatter = defaultValueFormatter as? DefaultValueFormatter
{
// setup the formatter with a new number of digits
let digits = reference.decimalPlaces
formatter.decimals = digits
}
After:
if let formatter = defaultValueFormatter as? DefaultValueFormatter
{
}
It will look like following::

ios Charts 3.0 - Align x labels (dates) with plots

I am having a hard time to migrate library Charts (from Daniel Gindi) from version 2 (Swift 2.3) to 3 (Swift 3).
Basically, I can't have the x labels (dates) aligned correctly with the corresponding plots.
This is what I had before in version 2:
In version 2, I had values for days 7, 8, 10 and 11.
So I was missing a day in the middle, but the labels were correctly alined with the plots.
And here is what I have in version 3:
In version 3, the "labels" in the x axis have now been replaced by double (for dates, it's a timeInterval since 1970), and formatted via a formatter.
So, indeniably, the graph is more "correct" now, since the chart correctly extrapolates the value for the 9th, but I can't find how to put the labels under the corresponding plots.
This is my code for the x axis:
let chartView = LineChartView()
...
let xAxis = chartView.xAxis
xAxis.labelPosition = .bottom
xAxis.labelCount = entries.count
xAxis.drawLabelsEnabled = true
xAxis.drawLimitLinesBehindDataEnabled = true
xAxis.avoidFirstLastClippingEnabled = true
// Set the x values date formatter
let xValuesNumberFormatter = ChartXAxisFormatter()
xValuesNumberFormatter.dateFormatter = dayNumberAndShortNameFormatter // e.g. "wed 26"
xAxis.valueFormatter = xValuesNumberFormatter
Here is the ChartXAxisFormatter class I created:
import Foundation
import Charts
class ChartXAxisFormatter: NSObject {
var dateFormatter: DateFormatter?
}
extension ChartXAxisFormatter: IAxisValueFormatter {
func stringForValue(_ value: Double, axis: AxisBase?) -> String {
if let dateFormatter = dateFormatter {
let date = Date(timeIntervalSince1970: value)
return dateFormatter.string(from: date)
}
return ""
}
}
So, the values here are correct, the formatting is correct, the shape of the chart is correct, but the alignment of the labels with the corresponding plots is not good.
Thanks for your help
OK, got it!
You've got to define a reference time Interval (the "0" for the x axis). And then calculate the additional time interval for each x value.
The ChartXAxisFormatter becomes:
import Foundation
import Charts
class ChartXAxisFormatter: NSObject {
fileprivate var dateFormatter: DateFormatter?
fileprivate var referenceTimeInterval: TimeInterval?
convenience init(referenceTimeInterval: TimeInterval, dateFormatter: DateFormatter) {
self.init()
self.referenceTimeInterval = referenceTimeInterval
self.dateFormatter = dateFormatter
}
}
extension ChartXAxisFormatter: IAxisValueFormatter {
func stringForValue(_ value: Double, axis: AxisBase?) -> String {
guard let dateFormatter = dateFormatter,
let referenceTimeInterval = referenceTimeInterval
else {
return ""
}
let date = Date(timeIntervalSince1970: value * 3600 * 24 + referenceTimeInterval)
return dateFormatter.string(from: date)
}
}
And, then, when I create my data entries, it works like so:
// (objects is defined as an array of struct with date and value)
// Define the reference time interval
var referenceTimeInterval: TimeInterval = 0
if let minTimeInterval = (objects.map { $0.date.timeIntervalSince1970 }).min() {
referenceTimeInterval = minTimeInterval
}
// Define chart xValues formatter
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .none
formatter.locale = Locale.current
let xValuesNumberFormatter = ChartXAxisFormatter(referenceTimeInterval: referenceTimeInterval, dateFormatter: formatter)
// Define chart entries
var entries = [ChartDataEntry]()
for object in objects {
let timeInterval = object.date.timeIntervalSince1970
let xValue = (timeInterval - referenceTimeInterval) / (3600 * 24)
let yValue = object.value
let entry = ChartDataEntry(x: xValue, y: yValue)
entries.append(entry)
}
// Pass these entries and the formatter to the Chart ...
The result is much nicer (I removed cubic by the way):
If you exactly know how many labels you need in the x-axis,you can write this code to solve it.For example,If I need seven labels to appear on the x-axis,Then this should be enough.The reason is the chart library is not properly calculating the intervals between the two x-axis points and hence the plot-label mismatch.When we force the library to plot against the given number of labels,The issue seems to be gone.
let xAxis = chart.xAxis
xAxis.centerAxisLabelsEnabled = false
xAxis.setLabelCount(7, force: true) //enter the number of labels here
#IBOutlet weak var tView:UIView!
#IBOutlet weak var lineChartView:LineChartView!{
didSet{
lineChartView.xAxis.labelPosition = .bottom
lineChartView.xAxis.granularityEnabled = true
lineChartView.xAxis.granularity = 1.0
let xAxis = lineChartView.xAxis
// xAxis.axisMinimum = 0.0
// xAxis.granularity = 1.0
// xaAxis.setLabelCount(6, force: true)
}
}
#IBOutlet weak var back: UIButton?
#IBAction func back(_ sender: Any) {
self.navigationController?.popViewController(animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
self.lineChartView.delegate = self
self.lineChartView.chartDescription?.textColor = UIColor.white
let months = ["Jan" , "Feb", "Mar"]
let dollars1 = [1453.0,2352,5431]
setChart(months, values: dollars1)
}
func setChart(_ dataPoints: [String], values: [Double]) {
var dataEntries: [ChartDataEntry] = []
for i in 0 ..< dataPoints.count {
dataEntries.append(ChartDataEntry(x: Double(i), y: values[i]))
}
let lineChartDataSet = LineChartDataSet(entries: dataEntries, label: nil)
lineChartDataSet.axisDependency = .left
lineChartDataSet.setColor(UIColor.black)
lineChartDataSet.setCircleColor(UIColor.black) // our circle will be dark red
lineChartDataSet.lineWidth = 1.0
lineChartDataSet.circleRadius = 3.0 // the radius of the node circle
lineChartDataSet.fillAlpha = 1
lineChartDataSet.fillColor = UIColor.black
lineChartDataSet.highlightColor = UIColor.white
lineChartDataSet.drawCircleHoleEnabled = true
var dataSets = [LineChartDataSet]()
dataSets.append(lineChartDataSet)
let lineChartData = LineChartData(dataSets: dataSets)
lineChartView.data = lineChartData
lineChartView.rightAxis.enabled = false
lineChartView.xAxis.drawGridLinesEnabled = false
lineChartView.xAxis.labelPosition = .bottom
lineChartView.xAxis.valueFormatter = IndexAxisValueFormatter(values: dataPoints)
}
func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) {
print(entry)
}
I solved this issue using this answer
https://stackoverflow.com/a/44554613/2087608
I suspected that these offsets come from adjusting X axis value to a specific time of the day in my case
Here is my code
for i in 0..<valuesViewModel.entries.count {
let dataEntry = ChartDataEntry(x: roundDate(date: valuesViewModel.entries[i].date).timeIntervalSince1970, y: valuesViewModel.entries[i].value)
dataEntries.append(dataEntry)
}
func roundDate(date: Date) -> Date {
var comp: DateComponents = Calendar.current.dateComponents([.year, .month, .day], from: date)
comp.timeZone = TimeZone(abbreviation: "UTC")!
let truncated = Calendar.current.date(from: comp)!
return truncated
}