Formatting numbers in charts ios swift - ios

How do I format the data that is shown in the linechart of Charts? By default the data is shown as double, but I want it to be displayed as int.
let pre = [20.0, 4.0, 6.0, 3.0, 12.0, 16.0]
for i in 0..<pre.count {
let preDataEntry = ChartDataEntry(value: pre[i], xIndex: i)
preEntries.append(preDataEntry)
}
let preChartDataSet = LineChartDataSet(yVals: preEntries, label: "Pre")
lineChartView.data = lineChartData

FIXED FOR SWIFT 3
YAxisValueFormatter.swift
import Foundation
import Charts
class YAxisValueFormatter: NSObject, IAxisValueFormatter {
let numFormatter: NumberFormatter
override init() {
numFormatter = NumberFormatter()
numFormatter.minimumFractionDigits = 1
numFormatter.maximumFractionDigits = 1
// if number is less than 1 add 0 before decimal
numFormatter.minimumIntegerDigits = 1 // how many digits do want before decimal
numFormatter.paddingPosition = .beforePrefix
numFormatter.paddingCharacter = "0"
}
/// Called when a value from an axis is formatted before being drawn.
///
/// For performance reasons, avoid excessive calculations and memory allocations inside this method.
///
/// - returns: The customized label that is drawn on the axis.
/// - parameter value: the value that is currently being drawn
/// - parameter axis: the axis that the value belongs to
///
public func stringForValue(_ value: Double, axis: AxisBase?) -> String {
return numFormatter.string(from: NSNumber(floatLiteral: value))!
}
}
create YAxisValueFormatterclass, and assign object to graph leftAxis
// Number formatting of YAxis
chartView.leftAxis.valueFormatter = YAxisValueFormatter()

There are some formatter properties you could use:
valueFormatter in ChartDataSet
formatters in ChartYAxis:
/// the formatter used to customly format the y-labels
public var valueFormatter: NSNumberFormatter?
/// the formatter used to customly format the y-labels
internal var _defaultValueFormatter = NSNumberFormatter()
should be enough

Related

How to set a custom x axis 0 label in charts ios

I am using Charts by Daniel Gindi
How can I set a String 0 label on x Axis? I want it to show "Now" instead of a Date type.
I've seen the function that helps to format one particular label only, but I can't get how it works. Moreover, I haven't seen any examples of it. So, how does getFormattedLabel(index: Int) work?
This is what I need my xAxis to look like:
Thank you!
You can set your xAxis.valueFormatter to your own custom class and then return now when your x value is 0.
Like:
class ChartValueFormatter: NSObject, IAxisValueFormatter {
func stringForValue(_ value: Double, axis: AxisBase?) -> String {
if value == 0 {
return "Now"
}
let dateFormatter = DateFormatter()
dateFormatter.setLocalizedDateFormatFromTemplate("dd MMM")
dateFormatter.locale = .current
let date = Date(timeIntervalSince1970: value)
return dateFormatter.string(from: date)
}
}
For this to work you need to sort your values based on date and then set then now value timestamp to zero.
It took a while to figure out, but I found a great tutorial on Medium by Spencer Mandrusiak
I needed to make the first label of a String type and all the others should still be Date types.
What I did:
Created an X axis formatting class.
Added an enum with only one label
When returning a value, I return time if there are no custom labels left.
This solution works well for me!
class ChartXAxisFormatter: NSObject, IAxisValueFormatter {
enum CustomLabel: Int {
case firstLabel
var label: String {
switch self {
case .firstLabel: return "Now"
}
}
}
func stringForValue(_ value: Double, axis: AxisBase?) -> String {
let dateFormatterPrint = DateFormatter()
dateFormatterPrint.dateFormat = "hh:mm"
let date = Date(timeIntervalSince1970: value)
let time = dateFormatterPrint.string(from: date)
let intVal = Int(value)
let customLabel = CustomLabel(rawValue: intVal)
return customLabel?.label ?? "\(time)"
}
}
This is a snippet that helps me convert an X axis into timeline and also make the last label on the chart of a String type.
index == count - 2 was used because the label count on X axis was set to 3 by force:
chart.xAxis.setLabelCount(3, force: true)
chart.xAxis.avoidFirstLastClippingEnabled = false
chart.xAxis.forceLabelsEnabled = true
class ChartXAxisFormatter: NSObject, IAxisValueFormatter {
func stringForValue(_ value: Double, axis: AxisBase?) -> String {
if let index = axis?.entries.firstIndex(of: value), let count = axis?.entries.count , index == count - 2 {
return "Now"
}
let dateFormatterPrint = DateFormatter()
dateFormatterPrint.dateFormat = "HH:mm:ss"
let date = Date(timeIntervalSince1970: value)
let time = dateFormatterPrint.string(from: date)
return time
}
}

How to add unrelated array to Datapoint labels in an iOS-charts line chart?

I have read How to add Strings on X Axis in iOS-charts? but it does not answer my question.
I have rendered a line chart with iOS-charts with no x/y axis labels, only datapoint labels, like this. This chart shows the temperatureValues in the chart's ChartDataEntry as the Y at intervals of 0..
var temperaturevalues: [Double] = [47.0, 48.0, 49.0, 50.0, 51.0, 50.0, 49.0]
However, I want to add an unrelated array of times to the datapoint labels, with times[i] matching temperatureValues at i.
var times: [String] = ["7 AM", "8 AM", "11 AM", "2 PM", "5 PM", "8 PM", "11 PM"]
I have extended IValueFormatter in a separate class like this:
class ChartsFormatterToDegrees: IValueFormatter {
func stringForValue(_ value: Double, entry: ChartDataEntry, dataSetIndex: Int, viewPortHandler: ViewPortHandler?) -> String {
return "\(Int(value))°"
}
but it only allows me to customize the dataPoint label using the value in the ChartDataEntry which is the temperatureValue being used to map out Y in the lineChart. How can I add another array at indexValue so that the dataPoints labels appear like this instead?
return """
\(Int(value))°
\(timesOfDay)
"""
Here is my charts method:
private func setupChartView(temperatureValues: [Double], timesDataPoints: [String]) {
var tempDataEntries: [ChartDataEntry] = []
let chartsFormatterToDegrees = ChartsFormatterToDegrees(time: timeDataPoints)
for eachTemp in 0..<temperatureValues.count {
let tempEntry = ChartDataEntry(x: Double(eachTemp), y: temperatureValues[eachTemp])
tempDataEntries.append(tempEntry)
}
let lineChartDataSet = LineChartDataSet(entries: tempDataEntries, label: "nil")
lineChartDataSet.valueFormatter = chartsFormatterToDegrees
let chartData = LineChartData(dataSets: [lineChartDataSet])
chartData.addDataSet(lineChartDataSet)
lineChartView.data = chartData
}
As I best understand it, the IValueFormatter extension only lets you modify the values being used to draw the chart, not add additional string arrays at index. When I tried the below, it only prints the timesOfDay at the dataSetIndex, it only prints timesOfDay[0] on all the dataPoint labels.
class ChartsFormatterToDegrees: IValueFormatter {
init(time: [String]) {
timesOfDay = time
}
var timesOfDay = [String]()
func stringForValue(_ value: Double, entry: ChartDataEntry, dataSetIndex: Int, viewPortHandler: ViewPortHandler?) -> String {
return """
\(Int(value))°
\(timesOfDay[dataSetIndex])
"""
}
}
Here's my most updated method with print:
class ChartsFormatterToDegrees: IValueFormatter {
var timesOfDay = [String]()
var values = [Double]()
init(values: [Double], time: [String]) {
timesOfDay = time
//print(values, "are the values") //prints values correctly
}
func stringForValue(_ value: Double, entry: ChartDataEntry, dataSetIndex: Int, viewPortHandler: ViewPortHandler?) -> String {
//print(value, "at", dataSetIndex) //prints as 37.0 at 0, 39.0 at 0, 40.0 at 0 etc
if let index = values.firstIndex(of: value) {
return "\(Int(value))° \(timesOfDay[index])"
}
let i = values.firstIndex(of: value)
//print(i as Any, "is index of where value shows up") //prints as nil is index of where value shows up for each i
return "\(Int(value))°"
}
}
Your assumption is not right. In IValueFormatter, you can construct any string for the corresponding value. The reason you are seeing only value or timeOfDay is because you are constructing String with """ notation that adds multiple lines whereas the label used for entry point may not calculate its size correctly because of this. You should create the String as below and see what you have got
class ChartsFormatterToDegrees: IValueFormatter {
var timesOfDay = [String]()
var values = [Double]()
init(values: [Double], time: [String]) {
timesOfDay = time
values = values
}
func stringForValue(_ value: Double, entry: ChartDataEntry, dataSetIndex: Int, viewPortHandler: ViewPortHandler?) -> String {
if let index = values.firstIndex(of: value) {
return "\(Int(value))° \(timesOfDay[index])"
}
return "\(Int(value))°"
}
}

Force to show min/max on x axis

I am working with library. Anybody know if is it possible to force show min and max label on x-axis.
What I have:
What I want to achieve:
My implementation:
private func prepareXAxis() {
lineChart.xAxis.labelPosition = .bottom
lineChart.xAxis.valueFormatter = ChartXAxisDateValueFormatter()
lineChart.xAxis.labelFont = UIFont.pmd_robotoRegular(ofSize: 13.0)
lineChart.xAxis.labelTextColor = UIColor.pmd_darkGrey
}
And implementation of my own ValueFormatter:
import Foundation
import Charts
class ChartXAxisDateValueFormatter: NSObject, IAxisValueFormatter {
func stringForValue(_ value: Double, axis: AxisBase?) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "hh:mm\ndd MMM"
return dateFormatter.string(from: Date(timeIntervalSince1970: value))
}
}
Edit
I added this code but first and last label on x-axis is still skipped:
lineChart.xAxis.axisMinimum = lineDataSet.xMin
lineChart.xAxis.axisMaximum = lineDataSet.xMax
lineChart.xAxis.avoidFirstLastClippingEnabled = false
lineChart.xAxis.granularity = 1.0
I checked lineDataSet.xMin and lineDataSet.xMax. They have valid values.
You need to set below property for not skip 1st and last value
//Set up XAxis into chart.
lineChart.xAxis.axisMinimum = 0
lineChart.xAxis.avoidFirstLastClippingEnabled = NO
lineChart.xAxis.granularity = 1.0
Hope this will helps.
I found a solution. The lost line:
combinedChart.xAxis.forceLabelsEnabled = true
I also changed type of chart from LineChart to CombinedChartView.

MeasurementFormatter to display feet and inches

I have been playing with MeasurementFormatter to try and display imperial lengths as 10'6" or 10 ft 6 in unsuccessfully. LengthFormatter does this correctly when isForPersonHeightUse is set to true, but it does not adapt to the user's locale well (i.e. countries where length is measured in metric, except for when referring to height). Is there any way to force this behaviour in a formatter?
EDIT
This question is to determine how to choose the units for a measurement. I am able to choose feet or inches, but want to display fractional feet as inches as in: 6'3" instead of 6.25 ft.
public struct LengthFormatters {
public static let imperialLengthFormatter: LengthFormatter = {
let formatter = LengthFormatter()
formatter.isForPersonHeightUse = true
return formatter
}()
}
extension Measurement where UnitType : UnitLength {
var heightOnFeetsAndInches: String? {
guard let measurement = self as? Measurement<UnitLength> else {
return nil
}
let meters = measurement.converted(to: .meters).value
LengthFormatters.imperialLengthFormatter.string(fromMeters: meters)
}
}
Example of using:
let a = Measurement(value: 6.34, unit: UnitLength.feet)
print(a.heightOnFeetsAndInches ?? "")
let b = Measurement(value: 1.5, unit: UnitLength.feet)
print(b.heightOnFeetsAndInches ?? "")
Will print:
6 ft, 4.08 in
1 ft, 6 in
I modified (simplified) #maslovsa's answer to meet my needs. I have a Core Data object called "Patient". It has a height parameter in inches that is an Int64. I want a string that I display to the user, so here's my property on my patient object for doing so:
var heightInFeetString : String {
let measurement = Measurement(value: Double(self.height) / 12.0, unit: UnitLength.feet)
let meters = measurement.converted(to: .meters).value
return LengthFormatter.imperialLengthFormatter.string(fromMeters: meters)
}
Of course, I had to implement the imperialLengthFormatter as well, but I did it as an extension to LengthFormatter itself, like this:
extension LengthFormatter {
public static let imperialLengthFormatter: LengthFormatter = {
let formatter = LengthFormatter()
formatter.isForPersonHeightUse = true
return formatter
}()
}
This actually doesn't kill performance as suggested in the comments for #maslova's answer. Due to the property being static, it only gets initialized once.
// When creating the Patient object
let patient = Patient(...) // Create in maanged object context
patient.height = 71
// Later displays in a collection view cell in a view controller
cell.heightLabel.Text = patient.heightInFeetString
Displays this in my table cell:
5 ft, 11 in
How to display Feet and Inches in SwiftUI
In case anyone arrives here looking for a SwiftUI answer.
struct MeasurementTestView: View {
#State private var height = Measurement(value: 68, unit: UnitLength.inches)
var body: some View {
VStack {
Text(height, format: .measurement(width: .narrow, usage: .personHeight))
Text(height, format: .measurement(width: .abbreviated, usage: .personHeight))
Text(height, format: .measurement(width: .wide, usage: .personHeight))
}
.font(.title)
}
}
Result

How to change Double value to String in barChart/swift?

Instead of showing the Double values in my chart I would like to show a String for each value. I still want the chart to draw from the Double values, but I want the label to show a string with a date instead. How to do this?
I´m using Daniel Cohen Gindis library.
Here is the code for my graph:
import UIKit
import Charts
class ViewController: UIViewController {
#IBOutlet var barChartView: BarChartView!
override func viewDidLoad() {
super.viewDidLoad()
//x line values
let tools = ["Saw","Sissor","Axe"
,"Hammer","Tray"]
//y line values
let values = [1.0,2.0,3.0,4.0,5.0]
setChart(tools, values: values, colors: colors)
}
func setChart(xAxisLabels: [String], values: [Double], colors: [UIColor]){
var dataEntries : [BarChartDataEntry] = []
for i in 0..<xAxisLabels.count {
let dataEntry = BarChartDataEntry(value: values[i], xIndex: i )
dataEntries.append(dataEntry)
}
barChartView.leftAxis.axisMinValue = 0.0
barChartView.leftAxis.axisMaxValue = 14.0
barChartView.data = chartData
}
}
You can use the String() method to convert any data type into a string.
For example:
var myDouble: Double = 1.5
print(String(myDouble)) // Prints "1.5"
I don't know if you still need the answer, but this may help somebody who struggles with the same problem :)
First of all, you need to make a ValueFormatter class for your xAxis.
xAxis.valueFormatter = XAxisValueFormatter()
And then you have to put the following in your new class:
class XAxisValueFormatter: NSObject, IAxisValueFormatter {
func stringForValue(_ value: Double, axis: AxisBase?) -> String {
let dateFormatterPrint = DateFormatter()
dateFormatterPrint.dateFormat = "HH:mm:ss"
let date = Date(timeIntervalSince1970: value)
let time = dateFormatterPrint.string(from: date)
return time
}
}

Resources