I am using the Swift Charts library by Daniel Gindi. I am trying to show individual values for each point in a scatter plot.
I have set drawValuesEnabled to true for the set and created a custom IValueFormatter. However, it appears that the method in the formatter class is never even being called, and no labels are shown. What else could be the cause of this?
EDIT: Code to create scatter plot is as follows:
let graphView: ScatterChartView = {
let scatter = ScatterChartView()
scatter.translatesAutoresizingMaskIntoConstraints = false
scatter.chartDescription?.enabled = true
scatter.dragEnabled = true
scatter.setScaleEnabled(true)
//scatter.maxVisibleCount = 200
scatter.pinchZoomEnabled = false
scatter.backgroundColor = .clear
scatter.leftAxis.enabled = false
scatter.rightAxis.enabled = true
scatter.rightAxis.drawGridLinesEnabled = false
scatter.rightAxis.valueFormatter = TimeValueFormatter()
scatter.xAxis.labelPosition = XAxis.LabelPosition.bottom
scatter.xAxis.axisMaxLabels = 8
scatter.xAxis.granularity = 1
scatter.xAxis.drawLabelsEnabled = true
scatter.xAxis.drawGridLinesEnabled = false
//date formatter should be provided by controller
scatter.legend.enabled = false
return scatter
}()
And the data set (with x-axis as integers and y-axis as time):
self.dataSet = {
let set = ScatterChartDataSet(values: entries)
set.setScatterShape(.circle)
set.setColor(UIColor.PRColors.prGreen)
set.valueFormatter = TimeValueFormatter()
return set
}()
Expected result is labels with the y-value shown by each point, as in the example project.
Figured it out. Turns out, when you create a data set using an array of ChartDataEntry objects, the array must be immutable (let rather than var), or labels will not be shown.
Related
I am trying to set a value for "background color" as seen below but when I print the value after setting it, it simply shows nil? This makes absolutely no sense. Please help, I have exhausted all resources already.
Thank you.
let request = GTLRSheets_Request.init()
request.repeatCell = GTLRSheets_RepeatCellRequest.init()
let colbox = GTLRSheets_GridRange.init()
colbox.startRowIndex = rowstart
colbox.endRowIndex = rowend
colbox.startColumnIndex = columnstart
colbox.endColumnIndex = columnend
request.repeatCell?.range = colbox
let color = GTLRSheets_Color.init()
color.blue = 60
color.red = 47
color.green = 77
request.repeatCell?.cell?.userEnteredFormat?.backgroundColor = color
request.repeatCell?.cell?.userEnteredFormat?.textFormat?.bold = true
request.repeatCell?.cell?.userEnteredFormat?.horizontalAlignment = "CENTER"
request.repeatCell?.fields = "userEnteredFormat(backgroundColor,textFormat,horizontalAlignment)"
print(request.repeatCell?.cell?.userEnteredFormat?.backgroundColor)
let batchUpdate = GTLRSheets_BatchUpdateSpreadsheetRequest.init()
batchUpdate.requests = [request]
let createQuery = GTLRSheetsQuery_SpreadsheetsBatchUpdate.query(withObject: batchUpdate, spreadsheetId: spreadsheetId)
service.executeQuery(createQuery) { (ticket, result, NSError) in
}
Issue:
You should initialize cell and userEnteredFormat.
Solution:
cell refers to an instance of GTLRSheets_CellData, and should be initialized the following way:
request.repeatCell?.cell = GTLRSheets_CellData.init()
userEnteredFormat refers to an instance of GTLRSheets_CellFormat:
request.repeatCell?.cell?.userEnteredFormat = GTLRSheets_CellFormat.init()
Reference:
GTLRSheets_CellData
GTLRSheets_CellFormat
Swift: Initialization
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
}
...}
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.
I don't know if there is a better way of doing this, my solution works but just seems to repetitive.
I have a life indicator which is represented using an icon within SpriteBuilder, these are set to not visible, and then within source I have implemented the following code:
lives = 3 // this is reduced or increased within game
if (lives == 5) {
lifeIcon1.visible = true
lifeIcon2.visible = true
lifeIcon3.visible = true
lifeIcon4.visible = true
lifeIcon5.visible = true
} else if (lives == 4) {
lifeIcon1.visible = true
lifeIcon2.visible = true
lifeIcon3.visible = true
lifeIcon4.visible = true
} else if (lives == 3) {
lifeIcon1.visible = true
lifeIcon2.visible = true
lifeIcon3.visible = true
} else if (lives == 2) {
lifeIcon1.visible = true
lifeIcon2.visible = true
} else if (lives == 1) {
lifeIcon1.visible = true
}
This works fine but just seems repetitive, and is difficult to scale if I wanted to increase the lives to more than 5 at a later date.
Any suggestions?
Assuming you want to set your visible to false when you decrease your lives, you can use something like this:
let lifeIcons = [lifeIcon1, lifeIcon2, lifeIcon3, lifeIcon4, lifeIcon5]
...
for (index, lifeIcon) in lifeIcons.enumerate() {
lifeIcon.visible = index < lives
}
Or if you like using closures:
lifeIcons.enumerate().forEach{$1.visible = $0 < lives}
(Here $1 is the current life icon in the iteration, and $0 is the index of that icon)
The key point here is to use an array, instead of multiple variables to keep track of your life icons. You can then iterate through this array, using the indices of the elements in order to determine which icon they are. enumerate() is used in order to create a lazy sequence of pairs of elements with their indices.
In this case we can compare the indices of the elements with the current number of lives. Because indices are 0 based, we can check whether a given index is less than the current number of lives in order to determine whether it should be visible.
How about this:
let icons = [lifeIcon1, lifeIcon2, lifeIcon3, lifeIcon4, lifeIcon5]
icons[0..<lives].forEach { $0.visible = true }
It is similar to #Adam Heeg's answer, but the c-style loops are in the process of being deprecated. I would slice the array by subscript and use forEach instead.
It seems like you have some other code which you don't wnat to change. I would suggest the following.
Load all your lifeIcon's into an array then do the following. (adjust for if you are 0 based or not).
for (int i = 0; i < lives; i++)
{
lifeIconArray[i].visible = true;
}
I use IOS-charts for drawing of downloading speed. I give to chart function array of double which has byte values. It build chart. But I want to show in chart labels value in megabytes, kilobytes or byte. I can transform byte in these value with help NSByteFormatter. But there is one problem, a function that builds charts takes a single array of values (such as an array of bytes), and outputs the same values in the labels on the chart, I tried to find a solution to do this, but I found none. How can i do this?
Function which draw a chart
func setSendChart(description: [String], value: [Double]) {
var chartDataEntryArray = [ChartDataEntry]()
for item in 0..<description.count {
let chartEntry = ChartDataEntry(value: value[item], xIndex: item)
chartDataEntryArray.append(chartEntry)
}
let dataSet = LineChartDataSet(yVals: chartDataEntryArray, label: "")
dataSet.drawCircleHoleEnabled = false
dataSet.drawCirclesEnabled = false
dataSet.drawCubicEnabled = true
dataSet.drawFilledEnabled = true
dataSet.drawValuesEnabled = false
dataSet.fillColor = UIColor(red: 47/255, green: 206/255, blue: 255/255, alpha: 1.0)
let chartData = LineChartData(xVals: description, dataSet: dataSet)
sendChartView.data = chartData
}
You can't change the value the chart is using to draw; however, you could provide your own NSNumberFormatter implementation to get the formatted value you want. yAxis has valueFormatter, so does dataSet
Another way is, you subclass dataSet to pass your value text array, and override renderAxisLabels or drawValues to draw the text you want.
I would prefer the first one since it does not touch the library code