How can I enable decimals in my chart in Swift? - swift

I am using Charts to create my own charts but I am struggling to see the values of my table with decimals instead of just integers.
My chart init is like below, but I cannot find the way to format the points to show the decimal parts since they are doubles. I have tried to set targetChartView.xAxis.decimals and targetChartView.leftAxis.decimals without result.
How can I enable the decimal notation?
init(withGraphView aGraphView: LineChartView, noDataText aNoDataTextString: String, minValue aMinValue: Double, maxValue aMaxValue: Double, numberOfDataSets aDataSetCount: Int, dataSetNames aDataSetNameList: [String], dataSetColors aColorSet: [UIColor], andMaxVisibleEntries maxEntries: Int = 10) {
originalMaxValue = aMaxValue
originalMinValue = aMinValue
dateFormatter = DateFormatter()
targetChartView = aGraphView
lineChartData = LineChartData()
maximumVisiblePoints = maxEntries
timestamps = [Date]()
for i in 0..<aDataSetCount {
let firstEntry = ChartDataEntry(x: 0, y: 0)
var entries = [ChartDataEntry]()
entries.append(firstEntry)
let aDataSet = LineChartDataSet(values: entries, label: aDataSetNameList[i])
aDataSet.setColor(aColorSet[i])
aDataSet.lineWidth = 3
aDataSet.lineCapType = .round
aDataSet.drawCircleHoleEnabled = false
aDataSet.circleRadius = 2
aDataSet.axisDependency = .left
aDataSet.highlightEnabled = true
lineChartData.addDataSet(aDataSet)
}
targetChartView.data = lineChartData
targetChartView.noDataText = aNoDataTextString
targetChartView.chartDescription?.text = ""
targetChartView.rightAxis.drawLabelsEnabled = false
targetChartView.xAxis.drawGridLinesEnabled = false
targetChartView.xAxis.labelPosition = .bottom
targetChartView.leftAxis.drawGridLinesEnabled = false
targetChartView.dragEnabled = true
targetChartView.xAxis.granularityEnabled = true
targetChartView.xAxis.granularity = 1
targetChartView.xAxis.decimals = 0
targetChartView.leftAxis.granularityEnabled = true
targetChartView.leftAxis.granularity = 1
targetChartView.leftAxis.decimals = 0
targetChartView.xAxis.axisMinimum = Double(0)
targetChartView.xAxis.axisMaximum = Double(maximumVisiblePoints)
targetChartView.leftAxis.axisMinimum = aMinValue
targetChartView.leftAxis.axisMaximum = aMaxValue
targetChartView.setScaleEnabled(false)
super.init()
targetChartView.xAxis.valueFormatter = self
targetChartView.delegate = self
// This gesture recognizer will track begin and end of touch/swipe.
// When user presses the graph we don't want it to be moving when new data is received even when the most recent value is visible.
let clickRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress))
clickRecognizer.minimumPressDuration = 0
clickRecognizer.delegate = self
targetChartView.addGestureRecognizer(clickRecognizer)
}

The decimals property is used only if you are using the default formatter. However, I see that you are also setting
targetChartView.xAxis.valueFormatter = self
That means your class is implementing IAxisValueFormatter and its stringForValue(:axis:) method. The value in decimals (which should be nonzero) is then ignored because you have a custom formatter.
You can either remove the assignment and then your decimals should be displayed or, you will have to format your decimals correctly in stringForValue(:axis:).
You have not added this part of your implementation but the problem is probably there.
I see there is also some magic in the AxisRenderer that will probably remove decimals if the interval between values is bigger than 1. Therefore using a custom formatter for both axes is probably the best solution.

As I mentioned to #Sulthan, the issue was not in the chart itself but how the data set (aDataSet) had to be formatted, so adding the snippet below enabled three decimals on my data in the graph
init(withGraphView aGraphView: LineChartView, noDataText aNoDataTextString: String, minValue aMinValue: Double, maxValue aMaxValue: Double, numberOfDataSets aDataSetCount: Int, dataSetNames aDataSetNameList: [String], dataSetColors aColorSet: [UIColor], andMaxVisibleEntries maxEntries: Int = 10) {
originalMaxValue = aMaxValue
originalMinValue = aMinValue
dateFormatter = DateFormatter()
targetChartView = aGraphView
lineChartData = LineChartData()
maximumVisiblePoints = maxEntries
timestamps = [Date]()
//Setting 3 decimals for the number that are going to be displayed
let formatter = NumberFormatter()//ADD THIS
formatter.numberStyle = .decimal//ADD THIS
formatter.maximumFractionDigits = 3//ADD THIS
formatter.minimumFractionDigits = 3//ADD THIS
for i in 0..<aDataSetCount {
let firstEntry = ChartDataEntry(x: 0, y: 0)
var entries = [ChartDataEntry]()
entries.append(firstEntry)
let aDataSet = LineChartDataSet(values: entries, label: aDataSetNameList[i])
aDataSet.setColor(aColorSet[i])
aDataSet.lineWidth = 3
aDataSet.lineCapType = .round
aDataSet.drawCircleHoleEnabled = false
aDataSet.circleRadius = 2
aDataSet.axisDependency = .left
aDataSet.highlightEnabled = true
lineChartData.addDataSet(aDataSet)
//Use formater that allows showing decimals
lineChartData.setValueFormatter(DefaultValueFormatter(formatter: formatter))//ADD THIS
}
...}

Related

How to properly reduce scale and format a Float value in Swift?

I'm trying to properly reduce scale, formatting a float value and returning it as a String in Swift.
For example:
let value: Float = 4.8962965
// formattedFalue should be 4.90 or 4,90 based on localization
let formattedValue = value.formatNumber()
Here is what I did:
extension Float {
func reduceScale(to places: Int) -> Float {
let multiplier = pow(10, Float(places))
let newDecimal = multiplier * self // move the decimal right
let truncated = Float(Int(newDecimal)) // drop the fraction
let originalDecimal = truncated / multiplier // move the decimal back return originalDecimal
}
func formatNumber() -> String {
let num = abs(self)
let numberFormatter = NumberFormatter()
numberFormatter.usesGroupingSeparator = true
numberFormatter.minimumFractionDigits = 0
numberFormatter.maximumFractionDigits = 2
numberFormatter.roundingMode = .up
numberFormatter.numberStyle = .decimal
numberFormatter.locale = // we take it from app settings
let formatted = num.reduceScale(to: 2)
let returningString = numberFormatter.string(from: NSNumber(value: formatted))!
return "\(returningString)"
}
}
But when I use this code I get 4.89 (or 4,89 depending on the localization) instead of 4.90 (or 4,90) as I expect.
Thanks in advance.
You get 4.89 because reduceScale(to:) turns the number into 4.89 (actually, probably 4.89000something because 4.89 cannot be expressed exactly as a binary floating point). When the number formatter truncates this to two decimal places, it naturally rounds it down.
In fact, you don't need reduceScale(to:) at all because the rounding function of the number formatter will do it for you.
Also the final string interpolation is unnecessary because the result of NumberFormatter.string(from:) is automatically bridged to a String?
Also (see comments below by Dávid Pásztor and Sulthan) you can use string(for:) to obviate the NSNumber conversion.
This is what you need
import Foundation
extension Float {
func formatNumber() -> String {
let num = abs(self)
let numberFormatter = NumberFormatter()
numberFormatter.usesGroupingSeparator = true
numberFormatter.minimumFractionDigits = 0
numberFormatter.maximumFractionDigits = 2
numberFormatter.roundingMode = .up
numberFormatter.numberStyle = .decimal
numberFormatter.locale = whatever
return numberFormatter.string(for: num)!
}
}
let value: Float = 4.8962965
// formattedFalue should be 4.90 or 4,90 based on localization
let formattedValue = value.formatNumber() // "4.9"
Solved by following Sulthan's comments:
remove that reduceScale method which is not necessary and it will probably work as expected. You are truncating the decimal to 4.89 which cannot be rounded any more (it is already rounded). – Sulthan 6 hours ago
That's because you have specified minimumFractionDigits = 0. If you always want to display two decimal digits, you will have to set minimumFractionDigits = 2. – Sulthan 5 hours ago
Foundation has a better API now.
In-place, you can use .number:
value.formatted(.number
.precision(.fractionLength(2))
.locale(locale)
)
But it's only available on specific types. For an extension for more than one floating-point type, you'll need to use the equivalent initializer instead:
extension BinaryFloatingPoint {
var formattedTo2Places: String {
formatted(FloatingPointFormatStyle()
.precision(.fractionLength(2))
.locale(locale)
)
}
}
let locale = Locale(identifier: "it")
(4.8962965 as Float).formattedTo2Places // 4,90

How to sort WBS format string in Realm?

I want to sort my Realm object, using one of it properties. It has WBS like format (1.1.3 ,1.1.11, etc) using String as it type.
I'm using RealmSwift 3.11.2 and I've already tried using sorted(by:) and it works! But the 1.1.10 and 1.1.11 will be sorted ahead of 1.1.2
This is the code that I'm using
tasks = tasks.sorted(byKeyPath: "wbs", ascending: true)
I expect the output will be ordered correctly, like [1.1.2, 1.1.10, 1.1.11].
Any help is appreciated, I'm drawing a blank here, is there any way I can do it in Realm ?
You may want to take a minute and do an Internet search on sorting strings.
The issue is that if you have a list of strings like this 1, 2, 3, 10 and they are sorted, they will sort to 1, 10, 2, 3.
This is because how strings are sorted; for this example they are they are generally sorted by the first letter/object in the string (1's) and then by the second letter (0) and so on, so then the strings that start with 1 get grouped up. Then 2's etc.
So you need to either pad them so the formats are the same like
01.01.02, 01.01.10, 01.01.11 etc
which will sort correctly or store the three sections in separate properties as numbers (int's)
class TriadClass {
var first = 0
var second = 0
var third = 0
init(x: Int, y: Int, z: Int) {
self.first = x
self.second = y
self.third = z
}
func getPaddedString() -> String {
let formatter = NumberFormatter()
formatter.format = "00"
let firstNum = NSNumber(value: self.first)
let secondNum = NSNumber(value: self.second)
let thirdNum = NSNumber(value: self.third)
let firstString = formatter.string(from: firstNum)
let secondString = formatter.string(from: secondNum)
let thirdString = formatter.string(from: thirdNum)
let finalString = "\(firstString!).\(secondString!).\(thirdString!)"
return finalString
}
func getPureString() -> String {
let formatter = NumberFormatter()
formatter.format = "0"
let firstNum = NSNumber(value: self.first)
let secondNum = NSNumber(value: self.second)
let thirdNum = NSNumber(value: self.third)
let firstString = formatter.string(from: firstNum)
let secondString = formatter.string(from: secondNum)
let thirdString = formatter.string(from: thirdNum)
let finalString = "\(firstString!).\(secondString!).\(thirdString!)"
return finalString
}
}
Then sort by first, second and third. To get the number for display in the UI I included to functions to format them. Note both functions could be greatly improved and shortened by I left them verbose so you could step through.
The usage would be
let myNum = TriadClass(x: 1, y: 1, z: 10)
let paddedString = myNum.getPaddedString()
print(paddedString)
let nonPaddedString = myNum.getPureString()
print(nonPaddedString)
and an output of
01.01.10
1.1.10
There are a number of other solutions as well but these two are a place to start.

Candlestick Chart with dates and time on x-axis

I am trying to create an candlestick chart using Charts Framework using with Codable as JSON passing myclass can be shown as :
struct ChartDataPair: Codable {
var DateTime: String = ""
var Open: Double = 0.0
var High: Double = 0.0
var Low: Double = 0.0
var Close: Double = 0.0
}
Which creates an array of chartDataPairs as shown :
struct ChartData: Codable {
var chartDataPairs: [ChartDataPair]
}
The value that I am fetching will be shows below a bit as example :
{"chartDataPairs":
[{
"DateTime": "2018/10/1 10:00:01",
"Open": 50.05,
"High": 50.05,
"Low": 49.00,
"Close":49.00
},
{
"DateTime": "2018/10/1 10:05:02",
"Open": 51.05,
"High": 54.06,
"Low": 40.00,
"Close":45.06
},
{
"DateTime": "2018/10/1 10:10:02",
"Open": 50.05,
"High": 64.06,
"Low": 40.00,
"Close":58.06
}]
}
The data is just a sample so just wrote 3 values. Now I have to fetch only time and convert the DateTime String to Double to plot it in x-axis of the charts. For with I m using :
var dataEntries: [ChartDataEntry] = []
guard let financialData = dataChart.self else {
return
}
for chartData in financialData{
let open = chartData.Open
let close = chartData.Close
let high = chartData.High
let low = chartData.Low
let datetime = chartData.DateTime
let formatter = DateFormatter()
formatter.dateFormat = "YYYY/MM/dd HH:mm:ss"
let yourDate = formatter.date(from: datetime)
formatter.dateFormat = "HH:mm"
let myStringafd = formatter.string(from: yourDate!)
let time = myStringafd
let components = time.characters.split { $0 == ":" } .map { (x) -> Int in return Int(String(x))! }
let hours = components[0]
let minutes = components[1]
let double1 = Double("\(hours).\(minutes)")
let dataEntry = CandleChartDataEntry(x: double1! , shadowH: high, shadowL: low, open: open, close: close)
dataEntries.append(dataEntry)
}
let chartDataSet = CandleChartDataSet(values: dataEntries, label: "")
chartDataSet.axisDependency = .left
chartDataSet.drawIconsEnabled = false
chartDataSet.shadowColor = .darkGray
chartDataSet.shadowWidth = 0.7
chartDataSet.decreasingColor = .red
chartDataSet.decreasingFilled = true // fill up the decreasing field color
chartDataSet.increasingColor = UIColor(red: 122/255, green: 242/255, blue: 84/255, alpha: 1)
chartDataSet.increasingFilled = true // fill up the increasing field color
chartDataSet.neutralColor = .blue
chartDataSet.barSpace = 1.0
chartDataSet.drawValuesEnabled = false
let chartData = CandleChartData(dataSet: chartDataSet)
candlestickView.data = chartData
I know that the conversion of the time to double ins't correct as per it is needed, Here I need some help on converting the datetime to double value.
The second issue is the bar width of the candlestick, I am unable to decrease the width of the candlestick.
And I want to fill up the x-axis with the time value like HH:MM with certain intervals like 15 mins, 50 mins, 4 hrs etc.
For which I followed few questions and suggestions here in given link below :
iOS-Charts Library: x-axis labels without backing data not showing
On this issue: candlestickView.xAxisRenderer = XAxisWeekRenderer()
isn't working. It is calling for viewporthandler, x-axis and transformation.
Though I can get the custom labels from the custom IAxisValueFormatter. The interval between the two values in the x-axis is not what I wanted it to be like in 15 mins or 50 mins or 4 hrs etc.
ios Charts 3.0 - Align x labels (dates) with plots
On the above mentioned link I am unable to get the minTimeInterval
and referenceTimeInterval
Basically What I want to do here is plot the hour and minute form the string that I am fetching from the JSON in x-axis and create a custom interval in between the values of x-axis while creating the custom x-axis labels.
My chart is currently shown as :
Candlestick Chart
I have same thing to display so I used this way to display the data
let xAxis = chartView.xAxis
var dataPoints = [String]()
for i in 0 ..< arrData.count
{
let timeStampFrom = arrData[i].time
dataPoints.append(self.stringFromTimestamp(timeStampFrom, strDateFormat: "h a"))
}
xAxis.valueFormatter = IndexAxisValueFormatter(values:dataPoints)
xAxis.setLabelCount(5, force: false)
Some needed function
func stringFromTimestamp(_ timeInterval : Double ,strDateFormat : String)->String
{
let dateFormatter = DateFormatter()
self.setDateFormat(dateFormatter,dateFormat: strDateFormat)
let date = Date(timeIntervalSince1970: TimeInterval(timeInterval))
return dateFormatter.string(from: date)
}
here is the output

NumberFormatter RoundingMode with unusual round

Making my own formatter like this:
enum Formatters {
enum Number {
static let moneyFormatter: NumberFormatter = {
let mFormatter = NumberFormatter()
mFormatter.numberStyle = NumberFormatter.Style.currency
mFormatter.currencyGroupingSeparator = " "
mFormatter.roundingMode = NumberFormatter.RoundingMode.halfUp
mFormatter.maximumFractionDigits = 0
return mFormatter
}()
}
}
And want to example: if 11 400 then round to 11 000, if 11 500 then 12 000
And etc. But it RoundMode works only with Digits correctly, how it setup for groups?
NumberFormatter has a roundingIncrement property for this purpose:
enum Formatters {
enum Number {
static let moneyFormatter: NumberFormatter = {
let mFormatter = NumberFormatter()
mFormatter.numberStyle = .currency
mFormatter.roundingMode = .halfUp
mFormatter.roundingIncrement = 1_000
mFormatter.maximumFractionDigits = 0
return mFormatter
}()
}
}
let fmt = Formatters.Number.moneyFormatter
print(fmt.string(from: 10_499.99)!) // 10.000 €
print(fmt.string(from: 10_500.00)!) // 11.000 €
However, for some reason unknown to me, this does not work if the
groupingSeparator or currencyGroupingSeparator property is set. Therefore, if you need a non-default grouping separator, you would
have to replace it "manually" in the formatted string.
Of course an alternative is to round the value to the nearest
multiple of 1,000 before formatting it. Example:
let value = 10_499.99
let roundedToThousands = (value / 1000).rounded() * 1000

iOS Charts - single values not showing Swift

When I have multiple points in an array for a line on a line graph, everything shows perfectly.
But when there is only one point, the dot does not show. I dont know why?
the delegate is being set elsewhere, but this doesnt seem to be the issue.
The below examples shows Test 2 and Test exercise. The first image is where each has one value, the second they each have 2.
heres my code
func startChart(){
chart.dragEnabled = true
chart.legend.form = .circle
chart.drawGridBackgroundEnabled = false
let xaxis = chart.xAxis
xaxis.valueFormatter = axisFormatDelegate
xaxis.labelCount = dataSets.count
xaxis.labelPosition = .bottom
xaxis.granularityEnabled = true
xaxis.granularity = 1.0
xaxis.avoidFirstLastClippingEnabled = true
xaxis.forceLabelsEnabled = true
let rightAxis = chart.rightAxis
rightAxis.enabled = false
rightAxis.axisMinimum = 0
let leftAxis = chart.leftAxis
leftAxis.drawGridLinesEnabled = true
leftAxis.axisMinimum = 0
let chartData = LineChartData(dataSets: dataSets)
chart.data = chartData
}
If I add
chart.setVisibleXRangeMinimum(myMinDate)
the value will show correctly. however it squashes the value to the left and overlaps 2 x value dates
The only way I could get around this was to add an additional invisible line.
I created a clear line that started the day before and ended the day after my single values.
As long as there is a line on the chart that goes from one point to another, the other single values show.
var singleValue = false
for i in 0...(dataSets.count - 1) {
if dataSets[i].values.count > 1{
singleValue = true
}
}
var data = dataSets
if singleValue == false {
let minNS = Calendar.current.date(byAdding: .day, value: -1, to: minNSDate as! Date)
let maxNS = Calendar.current.date(byAdding: .day, value: 1, to: maxNSDate as! Date)
var dataEntries: [ChartDataEntry] = []
let dataEntry1 = ChartDataEntry(x:Double(String(format: "%.2f",Double((minNS?.timeIntervalSince1970)!)))!,y:00.00)
let dataEntry2 = ChartDataEntry(x:Double(String(format: "%.2f",Double((maxNS?.timeIntervalSince1970)!)))!,y:00.00)
dataEntries.append(dataEntry1)
dataEntries.append(dataEntry2)
let set = LineChartDataSet(values: dataEntries, label: "")
set.setCircleColor(UIColor.clear)
set.circleHoleColor = UIColor.clear
set.setColor(UIColor.white, alpha: 0.0)
set.drawValuesEnabled = false
data.append(set)
}
chart.chartDescription?.text = ""
let chartData = LineChartData(dataSets: data)
chart.data = chartData
I think I found a better solution. Single point is not enough to draw a line (you need at least two points) so LineChartView can't render your data. You can fix that by replace LineChartView with CombinedChartView. CombinedChartView give a possibility to mix different types of data on one chart. You can check how many data entires do you have and decide which type of DataSet will be proper.
Code example:
if dataEntry.count == 1 {
let scatterDataSet = ScatterChartDataSet(values: dataEntry, label: title)
scatterDataSet.setColor(UIColor.pmd_darkBlue)
scatterDataSet.setScatterShape(.circle)
scatterDataSet.drawValuesEnabled = false
combinedChartData.scatterData = ScatterChartData(dataSets: [scatterDataSet])
}
else {
let lineDataSet = LineChartDataSet(values: dataEntry, label: title)
lineDataSet.setColor(UIColor.pmd_darkBlue)
lineDataSet.lineWidth = 3.0
lineDataSet.drawCirclesEnabled = false
lineDataSet.drawValuesEnabled = false
combinedChartData.lineData = LineChartData(dataSets: [lineDataSet])
}
combinedChart.data = combinedChartData
You can also combine two and more types DataSets in one chart.
Important
Don't forget to add this line:
combinedChart.drawOrder = [DrawOrder.line.rawValue, DrawOrder.scatter.rawValue]
You must to write types of data types you use otherwise data will not render.