Change yValue label vertical spacing in combined chart (ios-charts) - swift

I have a combined chart that uses a line and bar chart to show yValues.
In some instances the line and bar chart values will overlap, is there a way to set the vertical spacing of the labels for the yValues so that they're not on top of each other (like Jan to Oct in the image)?
Combined Chart Image
I'm using the Charts framework (formerly ios-charts), here is the code to setup the CombineChartView:
let xValues = getXAxisLabelsForYear(year)
let runningTotalsByMonth = getRunningTotalByMonthForYear(year)!
var yValsBar = [BarChartDataEntry]()
var yValsLine = [ChartDataEntry]()
for i in 0 ..< xValues.count {
let yBarDataEntry = BarChartDataEntry(value: monthlyWinnings[i], xIndex: i)
yValsBar.append(yBarDataEntry)
let yLineDataEntry = ChartDataEntry(value: runningTotalsByMonth[i], xIndex: i)
yValsLine.append(yLineDataEntry)
}
let barChartDataSet = BarChartDataSet(yVals: yValsBar, label: "Monthly Winnings")
//setup bar chart
var barChartColors = [UIColor]()
for i in monthlyWinnings {
if i >= 0.0 {
barChartColors.append(myGreen)
} else {
barChartColors.append(UIColor.redColor())
}
}
barChartDataSet.colors = barChartColors
barChartDataSet.barShadowColor = UIColor.clearColor()
barChartDataSet.valueFont = UIFont.systemFontOfSize(10.0)
//setup line chart
let lineChartDataSet = LineChartDataSet(yVals: yValsLine, label: "Cumulative Winnings")
var lineChartColors = [UIColor]()
for i in runningTotalsByMonth {
if i >= 0.0 {
lineChartColors.append(myGreen)
} else {
lineChartColors.append(UIColor.redColor())
}
}
lineChartDataSet.colors = lineChartColors
lineChartDataSet.circleColors = [UIColor.blueColor()]
lineChartDataSet.drawCircleHoleEnabled = false
lineChartDataSet.circleRadius = 5
lineChartDataSet.lineWidth = 2
lineChartDataSet.valueFont = UIFont.systemFontOfSize(10.0)
//combine data
let data = CombinedChartData(xVals: xValues)
data.barData = BarChartData(xVals: xValues, dataSet: barChartDataSet)
data.lineData = LineChartData(xVals: xValues, dataSet: lineChartDataSet)
combinedChartView.data = data
//format the chart
combinedChartView.xAxis.setLabelsToSkip(0)
combinedChartView.xAxis.labelPosition = .Bottom
combinedChartView.descriptionText = ""
combinedChartView.rightAxis.drawLabelsEnabled = false
combinedChartView.rightAxis.drawGridLinesEnabled = false
combinedChartView.drawGridBackgroundEnabled = false
combinedChartView.leftAxis.drawZeroLineEnabled = true
combinedChartView.xAxis.drawGridLinesEnabled = false
combinedChartView.xAxis.wordWrapEnabled = true

You can draw bar chart values below the top of the bar using
chartView.drawValueAboveBarEnabled = false
and setting some color
barChartDataSet.valueTextColor = UIColor.someColor()
Will look like this:

See my comment above, but something like this may work if you're not using auto layout:
let labelA = UILabel()
let labelB = UILabel()
let padding: CGFloat = 5.0 // or whatever
if CGRectIntersectsRect(labelA.frame, labelB.frame) {
// If the minY of labelA is <= labelB's that means labelA is ABOVE labelB
if labelA.frame.minY <= labelB.frame.minY {
// Set it above, with some (optional) padding
labelA.frame.origin.y = labelB.frame.origin.y - padding - labelA.frame.height
} else {
labelB.frame.origin.y = labelA.frame.origin.y - padding - labelB.frame.height
}
}
Of course you'll need additional code for checking if it's too high and other edge cases.

Related

Why moveViewToX() won't move my display to the last value in my bar chart? - Swift

I'm new to Swift and I'm trying to create bar chart. I manage to create a chart but for some reason moveViewToX() does not work as expected - it does not scroll to the last x values. Here is my relevant function to create a chart:
func createBarView() {
// Set basic parameters
barChartView.xAxis.labelTextColor = .black
barChartView.leftAxis.labelTextColor = .black
barChartView.leftAxis.axisMinimum = 0
barChartView.xAxis.labelPosition = XAxis.LabelPosition.bottom
barChartView.xAxis.labelRotationAngle = 0
barChartView.xAxis.granularity = 1
barChartView.xAxis.labelFont = UIFont.systemFont(ofSize: 20.0, weight: .regular)
barChartView.xAxis.drawGridLinesEnabled = false
barChartView.rightAxis.drawGridLinesEnabled = false
barChartView.leftAxis.drawGridLinesEnabled = false
barChartView.rightAxis.enabled = false
barChartView.leftAxis.enabled = false
barChartView.xAxis.labelPosition = .bottom
barChartView.clipsToBounds = true
barChartView.layer.cornerRadius = 10
barChartView.legend.enabled = false
barChartView.animate(yAxisDuration: 1.3, easingOption: .easeInOutQuart)
barChartView.scaleYEnabled = false
barChartView.scaleXEnabled = false
barChartView.pinchZoomEnabled = false
barChartView.doubleTapToZoomEnabled = false
//Get X (days) and Y data (calories)
let count = realm.objects(WorkoutData.self).count
for i in 0 ..< count {
let graphDates = realm.objects(WorkoutData.self)[i].date
allDates.append(graphDates)
let calendarDate = Calendar.current.dateComponents([.day, .year, .month], from: graphDates)
graphDays.append(String(calendarDate.day!)) // Get days array for xAxis
let graphCalories = realm.objects(WorkoutData.self)[i].calories
visosKalorijos.append(graphCalories) // Get calories array for yAxis
}
//Supply data for BarChartDataEntry
var entries = [BarChartDataEntry]()
for i in 0 ..< count { // dataPoints
let dataEntry = BarChartDataEntry(x: Double(i), y: Double(visosKalorijos[i]))
entries.append(dataEntry) //// Get x:y data
}
//Set labels of xAxis (to show days of the month)
barChartView.xAxis.valueFormatter = IndexAxisValueFormatter(values: graphDays)
// Create dataset
let set = BarChartDataSet(entries: entries, label: "-")
set.colors = ChartColorTemplates.solid()
set.valueFont = UIFont.systemFont(ofSize: 16)
let data = BarChartData(dataSet: set)
data.barWidth = Double(0.4)
barChartView.data = data
// Set visibility
barChartView.setVisibleXRangeMaximum(7)
barChartView.moveViewToX(Double(count))
}
When function is executed I can see only up to 17th day, but there are more days on the xAxis if I scroll.
After view is loaded I need to scroll to the end.
The last part - // Set visibility is where the problem is - it does not scroll automatically to the last x value. What Am I doing wrong? Thanks for your help.
After some time I figured this out. Last step //Set visibility needs delay of 0.1s:
barChartView.setVisibleXRangeMaximum(7)
let when = DispatchTime.now() + 0.1
DispatchQueue.main.asyncAfter(deadline: when) {
self.barChartView.moveViewToX(Double(self.count))
}
Hopefully this will help someone in the future :)

Line chart lines with same value are overlapping

I am using danielgindi
/
Charts for creating line chart. When Line chart data has same values, only the last drawn line showing. Other lines are hidden behind the last drawn line. Is there any way to show all lines.
image: 'fff' and 'User B' is hidden behind 'User C', they all have number of activities equal to zero
Line chart initialisation
func commonInit() {
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
self.addSubview(view)
lblXAxis.text = "Days"
lblYAxis.text = "Number of Activities"
lblYAxis.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
self.roundAllCorners(radius: 6)
let valFormatter = NumberFormatter()
valFormatter.numberStyle = .none
valFormatter.maximumFractionDigits = 0
viewProgressLineChart.leftAxis.valueFormatter = DefaultAxisValueFormatter(formatter: valFormatter)
viewProgressLineChart.leftAxis.granularity = 1
viewProgressLineChart.xAxis.granularity = 1
viewProgressLineChart.xAxis.labelCount = 11
viewProgressLineChart.xAxis.avoidFirstLastClippingEnabled = false
viewProgressLineChart.xAxis.labelPosition = .bottom
viewProgressLineChart.chartDescription?.text = ""
viewProgressLineChart.xAxis.labelTextColor = UIColor.onPrimary
viewProgressLineChart.leftAxis.labelTextColor = UIColor.onPrimary
viewProgressLineChart.rightAxis.labelTextColor = UIColor.onPrimary
viewProgressLineChart.clipValuesToContentEnabled = true
viewProgressLineChart.legend.enabled = false
viewProgressLineChart.rightAxis.enabled = false
viewProgressLineChart.animate(xAxisDuration: 0.8)
}
This is how data is given to line chart,
func updateGraph(users: [User]){
let data = LineChartData()
for (index,user) in users.enumerated(){
let userColor = UIColor.selectedColors[index]
userAndStatusColorArray.append(UserAndStatusColor(name: user.name ?? "user", color: userColor))
var lineChartEntry = [ChartDataEntry]()
if user.progress != nil{
for progress in user.progress!{
let chartData = ChartDataEntry(x: Double(progress.day), y: Double(progress.activitiesCompleted!))
lineChartEntry.append(chartData)
}
let chartDataSet = LineChartDataSet(entries: lineChartEntry)
chartDataSet.colors = [userColor]
chartDataSet.circleColors = [userColor]
chartDataSet.circleRadius = 3
chartDataSet.drawValuesEnabled = false
data.addDataSet(chartDataSet)
}
}
viewPODProgressLineChart.viewProgressLineChart.data = data
}

iOS Charts Radar Chart size

I'm using the Charts library and am trying to replicate this design:
I'm sort of getting there, but the chart is rendering itself way too small:
I'm expecting the chart to fill the entire width of the screen, and use all the vertical space. To be clear: the RadarChartView is the width of the entire black area, and the entire vertical space right up to the legend (which is not part of the chart view itself).
Any ideas?
This is the table cell code that shows the chart:
import Charts
import UIKit
final class ReportSpiderChart: UITableViewCell {
private let labels = ["ARTISTS", "TRACKS", "ALBUMS"]
#IBOutlet private var chartView: RadarChartView!
override func awakeFromNib() {
super.awakeFromNib()
chartView.webLineWidth = 1
chartView.innerWebLineWidth = 1
chartView.webColor = .init(hex: "28282A")
chartView.innerWebColor = .init(hex: "28282A")
chartView.legend.enabled = false
let xAxis = chartView.xAxis
xAxis.labelFont = .systemFont(ofSize: 11, weight: .semibold)
xAxis.xOffset = 0
xAxis.yOffset = 0
xAxis.labelTextColor = .init(hex: "919198")
xAxis.valueFormatter = self
let yAxis = chartView.yAxis
yAxis.labelCount = 3
yAxis.labelFont = .systemFont(ofSize: 11, weight: .semibold)
yAxis.labelTextColor = .init(hex: "919198")
yAxis.axisMinimum = 0
yAxis.drawLabelsEnabled = false
}
func configure(data: ReportData) {
let entries: [RadarChartDataEntry] = [
.init(value: Double(data.artists)),
.init(value: Double(data.tracks)),
.init(value: Double(data.albums)),
]
chartView.yAxis.axisMaximum = Double(max(max(data.artists, data.tracks), data.albums))
let dataSet = RadarChartDataSet(entries: entries)
dataSet.fillColor = UIColor(hex: "FA4B4B").withAlphaComponent(0.75)
dataSet.fillAlpha = 0.75
dataSet.drawFilledEnabled = true
dataSet.lineWidth = 0
dataSet.drawHighlightCircleEnabled = false
dataSet.setDrawHighlightIndicators(false)
let data = RadarChartData(dataSets: [dataSet])
data.setDrawValues(false)
chartView.data = data
}
}
extension ReportSpiderChart: IAxisValueFormatter {
func stringForValue(_ value: Double, axis: AxisBase?) -> String {
return labels[Int(value) % labels.count]
}
}
It seems that their is a spaceTop and spaceBottom property on axis, did you try to set them to 0 on both axis ?
https://github.com/danielgindi/Charts/blob/1bbec78109c7842d130d53ff8811bb6dbe865ba4/Source/Charts/Components/YAxis.swift#L72

Chart viewport height increase when chart has no data plot on chart

I am using https://github.com/danielgindi/Charts library. When the chart has no data then chart height increased it should be the same as when the chart has data. Chart xAxis labels overlapped with char title. Any help would be appreciated. Code for chart setup
var dataEntries: [ChartDataEntry] = []
axisFormatDelegate = self
chartView.legend.form = .none
let rightAxis = chartView.rightAxis
rightAxis.enabled = false
let yAxis = chartView.leftAxis
let xAxisValue = chartView.xAxis
xAxisValue.valueFormatter = axisFormatDelegate
xAxisValue.axisMinimum = -1
xAxisValue.axisMaximum = Double(forX.count)
xAxisValue.granularity = 1
for i in 0..<forX.count {
if forY[i] != 0 {
let dataEntry = ChartDataEntry(x: Double(i), y: forY[i])
dataEntries.append(dataEntry)
}
}
let lineChartDataSet = LineChartDataSet(entries: dataEntries, label: "")
let lineChartData = LineChartData(dataSet: lineChartDataSet)
print("Line chart data: \(lineChartData.dataSets)")
chartView.data = lineChartData
The chart with data:
The chart with no data:
I think it because you set legend.form to .none, it not showing legend but still had space for it
I usually use this to hide legend and to give extra margin in bottom of xAxis label use
chartView.legend.enabled = false
chartView.extraBottomOffset = 10

Multiple scatterplots using Core Plot and Swift

I'm trying to find a way to add two different scatterplots to a single graph but i wasn't able so far. I've found some examples in Objective-C but nothing in Swift, just the Scatterplot example in the CorePlot 2.1 release, but it plots the same data in two different line colors.
This is what i have so far (only one scatter plot is plotted):
import UIKit
import CorePlot
class ViewController : UIViewController, CPTScatterPlotDataSource {
private var scatterGraph : CPTXYGraph? = nil
typealias plotDataType = [CPTScatterPlotField : Double]
private var dataForPlot = [plotDataType]()
#IBOutlet var graphView: UIView!
// MARK: Initialization
override func viewDidAppear(animated : Bool)
{
super.viewDidAppear(animated)
// Create graph from theme
let newGraph = CPTXYGraph(frame: CGRectZero)
newGraph.applyTheme(CPTTheme(named: kCPTDarkGradientTheme))
let hostingView = graphView as! CPTGraphHostingView
hostingView.hostedGraph = newGraph
// Paddings
newGraph.paddingLeft = 10.0
newGraph.paddingRight = 10.0
newGraph.paddingTop = 10.0
newGraph.paddingBottom = 10.0
// Plot space
let plotSpace = newGraph.defaultPlotSpace as! CPTXYPlotSpace
//plotSpace.allowsUserInteraction = true
plotSpace.yRange = CPTPlotRange(location:0, length:10)
plotSpace.xRange = CPTPlotRange(location:0, length:10)
// Axes
let axisSet = newGraph.axisSet as! CPTXYAxisSet
if let x = axisSet.xAxis {
x.majorIntervalLength = 2
x.orthogonalPosition = 2.0
x.minorTicksPerInterval = 2
}
if let y = axisSet.xAxis {
y.majorIntervalLength = 2
y.minorTicksPerInterval = 5
y.orthogonalPosition = 2.0
y.delegate = self
}
// Create a blue plot area
let boundLinePlot = CPTScatterPlot(frame: CGRectZero)
let blueLineStyle = CPTMutableLineStyle()
blueLineStyle.miterLimit = 1.0
blueLineStyle.lineWidth = 3.0
blueLineStyle.lineColor = CPTColor.blueColor()
boundLinePlot.dataLineStyle = blueLineStyle
boundLinePlot.identifier = "Blue Plot"
boundLinePlot.dataSource = self
newGraph.addPlot(boundLinePlot)
// Add plot symbols
let symbolLineStyle = CPTMutableLineStyle()
symbolLineStyle.lineColor = CPTColor.blackColor()
let plotSymbol = CPTPlotSymbol.ellipsePlotSymbol()
plotSymbol.fill = CPTFill(color: CPTColor.blueColor())
plotSymbol.lineStyle = symbolLineStyle
plotSymbol.size = CGSize(width: 10.0, height: 10.0)
// Put an area gradient under the plot above
let areaColor = CPTColor(componentRed: 0.3, green: 1.0, blue: 0.3, alpha: 0.8)
let areaGradient = CPTGradient(beginningColor: areaColor, endingColor: CPTColor.clearColor())
areaGradient.angle = -90.0
let areaGradientFill = CPTFill(gradient: areaGradient)
// Add some initial data
var contentArray = [plotDataType]()
let plotData1: plotDataType = [.X: 0, .Y: 5]
let plotData2: plotDataType = [.X: 5, .Y: 0]
contentArray.append(plotData1)
contentArray.append(plotData2)
self.dataForPlot = contentArray
self.scatterGraph = newGraph
}
// MARK: - Plot Data Source Methods
func numberOfRecordsForPlot(plot: CPTPlot) -> UInt
{
return UInt(self.dataForPlot.count)
}
func numberForPlot(plot: CPTPlot, field: UInt, recordIndex: UInt) -> AnyObject?
{
let plotField = CPTScatterPlotField(rawValue: Int(field))
if let num = self.dataForPlot[Int(recordIndex)][plotField!] {
return num as NSNumber
}
else {
return nil
}
}
// MARK: - Axis Delegate Methods
func axis(axis: CPTAxis, shouldUpdateAxisLabelsAtLocations locations: NSSet!) -> Bool
{
if let formatter = axis.labelFormatter {
let labelOffset = axis.labelOffset
var newLabels = Set<CPTAxisLabel>()
for tickLocation in locations {
if let labelTextStyle = axis.labelTextStyle?.mutableCopy() as? CPTMutableTextStyle {
if tickLocation.doubleValue >= 0.0 {
labelTextStyle.color = CPTColor.greenColor()
}
else {
labelTextStyle.color = CPTColor.redColor()
}
let labelString = formatter.stringForObjectValue(tickLocation)
let newLabelLayer = CPTTextLayer(text: labelString, style: labelTextStyle)
let newLabel = CPTAxisLabel(contentLayer: newLabelLayer)
newLabel.tickLocation = tickLocation as! NSNumber
newLabel.offset = labelOffset
newLabels.insert(newLabel)
}
axis.axisLabels = newLabels
}
}
return false
}
}
This gives me a single line, but i want to add an additional line with a different data.
Any suggestions?
For a starter, create two CPTScatterPlots (e.g. boundLinePlot1 & boundLinePlot2)and configure them with different colors and different identifier then add them
boundLinePlot1.identifier = "Blue Plot"
boundLinePlot2.identifier = "Green Plot"
newGraph.addPlot(boundLinePlot1)
newGraph.addPlot(boundLinePlot2)
Now in the Plot Data Source Methods (numberOfRecordsForPlot & numberForPlot) calculate return value based on plot.identifier
if plot.identifier == "Blue Plot" {
return dataForPlot1[Int(recordIndex)][plotField!]
} else {
return dataForPlot2[Int(recordIndex)][plotField!]
}