Custom xAxis formatting in iOS Charts library - ios-charts

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.

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))

_startDate Date <unavailable; try printing with "vo" or "po">

I want to create a date variable but get the following response:
_startDate Date <unavailable; try printing with "vo" or "po">
The code i have is:
func createMileagesPDF() {
var _someDate: NSDate = NSDate()
self.someDate = Date()
guard (!exportYear.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && exportYear.count == 4) && cars.count > 0 else {
showAlert.toggle()
return
}
let _dateFormatter = DateFormatter()
//_dateFormatter.dateFormat = "dd-MM-yyyy"
let _startDateString = String(format: "01-01-%#", exportYear)
let _startDate = createdate() // = _dateFormatter.date(from: "01-12-2020 00:00:01")
let _x = createdate()
print("Create PDF")
}
func createdate() -> Date {
return Date()
}
What i want is an startdate based on an given year.
A string converted to an date.
When i do this in PlayGround it all works fine.
Thanks.

Cannot convert value of type 'String' to expected argument type 'TimeInterval' (aka 'Double')

I am formatting time that is being passed in. I am getting this error, and I'm trying to figure out what's happening here with it. I'm using the most up to date version of Xcode (v. 8) and am programming in Swift 3. What this error means and how to fix it?
I'm piecing in parts of the code that are relevant to the question so it's not a overload of information here.
var date: String {
if _date == nil {
_date = ""
}
return _date
}
init(weatherDict: Dictionary<String, AnyObject>){
if let temp = weatherDict["temp"] as? Dictionary<String, AnyObject>
{
if let min = temp["min"] as? Double{
self._lowTemp = ktof(kelvins: min)
}
if let max = temp["max"] as? Double{
self._highTemp = ktof(kelvins: max)
}
}
let unixConvertedDate = Date(timeIntervalSince1970: date)
if let date = weatherDict["dt"] as? Double {
let unixConvertedDate = Date(timeIntervalSince1970: date)
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .full
//dateFormatter.dateFormat = "EEEE"
dateFormatter.dateStyle = .none
self._date = unixConvertedDate.dayoftheWeek()
}
}
and then outside of the class, which all the above code is inside of, I have this.
extension Date {
func dayoftheWeek() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "EEEE"
return dateFormatter.string(from: self)
}
}
if let date = weatherDict["dt"] as? Double {
let unixConvertedDate = Date(timeIntervalSince1970: date)
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .full
dateFormatter.dateFormat = "EEEE"
dateFormatter.timeStyle = .long
self._date = "\(unixConvertedDate)"
}
}
}
extension Date {
func dayOfTheWeek() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "EEEE"
return dateFormatter.string(from: self)
}
}

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

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

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
}