I want to achieve the following effect, shown below in my iOS app (written in Swift)
So far I have been able to achieve this, using Charts by danielgindi. But I am not able to get the desired effect as I want to. Is there any way to add rounded corners to each Pie Chart slice like in this example image here?:
My Chart setup is as follows:
let data1 = PieChartDataEntry(value: 3)
let data2 = PieChartDataEntry(value: 5)
let data3 = PieChartDataEntry(value: 4)
let data4 = PieChartDataEntry(value: 6)
let data5 = PieChartDataEntry(value: 8)
let values = [data1, data2, data3, data4, data5]
let chartDataSet = PieChartDataSet(entries: values, label: nil)
let chartData = PieChartData(dataSet: chartDataSet)
let colors = [UIColor.fuelTintColor, UIColor.maintenanceTintColor, UIColor.insuranceTintColor, UIColor.fastagTintColor, UIColor.miscTintColor]
chartDataSet.colors = colors as! [NSUIColor]
chartDataSet.sliceSpace = 10
pieChart1.data = chartData
pieChart1.holeRadiusPercent = 0.8
I think it can be done using the PieChartRenderer but I have no idea how I should proceed.
Also, if you have any suggestions for other ways to implement this do let me know.
Solved it, and here's the output:
ring-chart-img
Basically I am using this library, called CircularProgressView (https://cocoapods.org/pods/CircleProgressView) to achieve the individual rings. Since I needed 5 rings, I stacked 5 such views (with clear background) on top of each other and rotated them to achieve my desired effect.
Cocoapod setup
First, you have to install the CircularProgressView pod in your project. I am using Cocoapods for importing the library here.
NOTE: If you do not have Cocoapods setup already then you need to install Cocoapods first using steps here: https://guides.cocoapods.org/using/getting-started.html
To add it using Cocoapods, add the following line in your Podfile.
pod 'CircleProgressView'
and run pod install on the Terminal from within your project directory.
Interface Builder
In your Storyboard file, go to your View Controller and add a view and set its contents. You can also create this view programmatically but here I will be using the storyboard.
Create 4 more views and use Auto Layout to stack them on top of one another.
Select all 5 views and go to the Identity Inspector (right panel in storyboard, 4th item in top bar).
Set the Class field value (under Custom Class) as 'CircularProgressView'.
Link all 5 views to your ViewController.swift file.
I have named them as follows:
#IBOutlet weak var pieChart1: CircleProgressView!
#IBOutlet weak var pieChart2: CircleProgressView!
#IBOutlet weak var pieChart3: CircleProgressView!
#IBOutlet weak var pieChart4: CircleProgressView!
#IBOutlet weak var pieChart5: CircleProgressView!
Call the function showRingChart() in viewDidLoad() to setup the views.
Here is the code for the functions:
// Function to convert degrees to radian
func degToRad(_ rotationDegrees: Double) -> CGFloat {
let rotationAngle = CGFloat(rotationDegrees * .pi/180.0)
return rotationAngle
}
// Function to Show Ring Chart
func showRingChart() {
// Values for the graph, can be changed as per your need
let val1 = self.val1
let val2 = self.val2
let val3 = self.val3
let val4 = self.val4
let val5 = self.val5
let totalVal = (val1 + val2 + val3 + val4 + val5)
var spacing = 0.05 * totalVal // Spacing is set to 5% (ie. 0.05). Change it according to your needs
print("Spacing: ", spacing)
let totalSpacing = 5 * spacing //Total spacing value to be added in the chart
let total = totalVal + totalSpacing //Total corresponding to 100% on the chart
if val1 == 0.0
&& val2 == 0.0
&& val3 == 0.0
&& val4 == 0.0
&& val5 == 0.0 {
// NO DATA, HIDE ALL CHARTS
pieChart1.isHidden = true
pieChart2.isHidden = true
pieChart3.isHidden = true
pieChart4.isHidden = true
pieChart5.isHidden = true
} else {
// DATA AVAILABLE
// Calculate Percentage of each value in the ring chart (ie. progress)
let valOnePerc = (val1 / total)
let valTwoPerc = (val2 / total)
let valThreePerc = (val3 / total)
let valFourPerc = (val4 / total)
let valFivePerc = (val5 / total)
let spacingPerc = spacing / total
// Angle offsets (in degrees)
let offset1 = (valOnePerc + spacingPerc) * 360
let offset2 = (valOnePerc + valTwoPerc + (2 * spacingPerc)) * 360
let offset3 = (valOnePerc + valTwoPerc + valThreePerc + (3 * spacingPerc)) * 360
let offset4 = (valOnePerc + valTwoPerc + valThreePerc + valFourPerc + (4 * spacingPerc)) * 360
print("--- PRINTING CHART VALUES HERE ---- ")
print(total)
print(valOnePerc)
print(valTwoPerc)
print(valThreePerc)
print(valFourPerc)
print(valFivePerc)
print(offset1)
print(offset2)
print(offset3)
print(offset4)
print("------------------")
// Setup ring chart sections
pieChart1.trackFillColor = UIColor.tintColorOne
pieChart1.progress = valOnePerc
pieChart2.trackFillColor = UIColor.tintColorTwo
pieChart2.progress = valTwoPerc
pieChart2.transform = CGAffineTransform(rotationAngle: degToRad(offset1))
pieChart3.trackFillColor = UIColor.tintColorThree
pieChart3.progress = valThreePerc
pieChart3.transform = CGAffineTransform(rotationAngle: degToRad(offset2))
pieChart4.trackFillColor = UIColor.tintColorFour
pieChart4.progress = valFourPerc
pieChart4.transform = CGAffineTransform(rotationAngle: degToRad(offset3))
pieChart5.trackFillColor = UIColor.tintColorFive
pieChart5.progress = valFivePerc
pieChart5.transform = CGAffineTransform(rotationAngle: degToRad(offset4))
// Unhide all charts
pieChart1.isHidden = false
pieChart2.isHidden = false
pieChart3.isHidden = false
pieChart4.isHidden = false
pieChart5.isHidden = false
}
}
It should do it. Change the (val1, val2, ..., val5) values to change the progress of the rings.
Related
When creating a line chart from more than one data sets, the line chart only shows one of the data sets and when zooming or panning the chart it crashes with Fatal error: Can't form Range with upperBound < lowerBound.
If I create the line chart from one data set it works as expected.
This problem only occurs when the two datasets have completely different ranges of X values.
The code below should draw a chart with x ranging from 0 to 19 (i.e. 2 datasets). But it only draws the second dataset. The chart crashes if you pan or zoom it.
If I edit the code, replacing for x in (10..<20) with for x in (0..<10), both datasets are correctly drawn and the chart does not crash.
To summarise: when adding two dataSets that have entries with different ranges of X coordinates the chart draws incorrectly and will crash.
Is there an iOS_charts API call needed to prevent this? How can I draw two datasets that do not have overlapping X-coordinates?
I've been able to produce the same crash when running code using this demo code if I modify it to create multiple datasets that have non-overlapping x-coordinates.
class ElevationChartViewController: UIViewController {
#IBOutlet var chartView: LineChartView!
override func viewDidLoad() {
super.viewDidLoad()
chartView.backgroundColor = .white
chartView.legend.enabled = false
chartView.maxVisibleCount = 20000
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let dataSets = createChartDataSets()
chartView.data = LineChartData(dataSets: dataSets)
}
}
func createChartDataSets() -> [LineChartDataSet] {
var dataSets = [LineChartDataSet]()
var entriesOne = [ChartDataEntry]()
var entriesTwo = [ChartDataEntry]()
var y = 0.0
for x in (0..<10) {
entriesOne.append( ChartDataEntry(x: Double(x), y: y))
y = y + 10
if y > 60 {y = 0.0}
}
dataSets.append(LineChartDataSet(entriesOne))
for x in (10..<20) {
entriesTwo.append( ChartDataEntry(x: Double(x), y: y))
y = y + 10
if y > 50 {y = 0.0}
}
dataSets.append(LineChartDataSet(entriesTwo))
return dataSets
}
Swift version: 5.4
Xcode 12.4
Observed running on a real iPhone 12 sw version 14.4
Charts v4.0.1
I have been facing a similar issue and this solution worked for me so far. Not sure about potential side effects that could arise from this. I have not tested with panning or zooming.
Subclass LineChartDataSet and override entryIndex(x xValue:closestToY yValue:rounding) copy and paste the super implementation, but remove the guard statement at the top of the function
var closest = partitioningIndex { $0.x >= xValue }
guard closest < endIndex else { return -1 }
and replace with
var closest = partitioningIndex { $0.x >= xValue }
if closest >= endIndex {
closest = endIndex - 1
}
I am trying to make the X direction grid line and tick labels fixed at mid of the visible range, whether the chart is zoomed or paned.
I had try to create custom TickProvider for my xAxis:
class CustomTickProvider: SCIDateTimeTickProvider {
private var tickCount: Int
init(tickCount: Int) {
self.tickCount = tickCount
}
override func getMajorTicks(fromAxis axis: SCIAxisCoreProtocol!) -> SCIArrayController! {
let visibleRange = axis.visibleRange
let min = visibleRange?.min.doubleData
let max = visibleRange?.max.doubleData
let array: SCIArrayController = SCIArrayController.init(type: SCIDataType.double)
let step = (max! - min!) / Double(self.tickCount - 1)
var current = min!
while current <= max! {
array.append(SCIGeneric(current))
current += step
}
return array
}
}
xAxis.tickProvider = CustomTickProvider.init(tickCount: 3)
When I set xAxis.autoTicks = true, the grid line and tick labels will be relocated,they can't stay at the same position either.
When I set xAxis.autoTicks = false, no grid line and tick labels will be
drawn.
How can I get the effect of fixed grid line and tick labels?
Maybe not the cleanest way to do it, but it worked for me in a comparable situation:
Edit the MajorDelta/MinorDelta after the visibleRange changed.
I use the third party SwiftAlertView class(https://github.com/dinhquan/SwiftAlertView) and update Swift from 2.3 to 3.When I complied, the app and I found the error message as following.
Binary operator '+' cannot be applied to operands of type 'Double' and 'Double!'
The code is following. The titleTopMargin and titleToMessageSpacing are Double!That can't add Double(titleLabel.frame.size.height).
let topPartHeight = (contentView == nil) ? (titleTopMargin + Double(titleLabel.frame.size.height) + titleToMessageSpacing + Double(messageLabel.frame.size.height) + messageBottomMargin) : Double(contentView!.frame.size.height)
The definition is following.
// customize the margin & spacing of title & message
open var titleSideMargin: Double! // default is 20 px
open var messageSideMargin: Double! // default is 20 px
open var titleTopMargin: Double! // default is 20 px
open var messageBottomMargin: Double! // default is 20 px
open var titleToMessageSpacing: Double! // default is 10 px
How do I fix the problem? Thanks.
Thanks everybody for giving me suggestion.
These variables still need to use Double. So I need to update the following code.
open var titleSideMargin: Double = 20.0
open var messageSideMargin: Double = 20.0
open var titleTopMargin: Double = 20.0
open var messageBottomMargin: Double = 20.0
open var titleToMessageSpacing: Double = 10.0
UIKit and CoreGraphics always use CGFloat for size, they never use to Double type. So, I think we should use to CGFloat here.
Exactly as #Rahul answers. So set type to CGFloat:
var titleSideMargin: CGFloat = 20.0
var messageSideMargin: CGFloat = 20.0
var titleTopMargin: CGFloat = 20.0
var messageBottomMargin: CGFloat = 20.0
var titleToMessageSpacing: CGFloat = 10.0
let topPartHeight = (contentView == nil) ? (titleTopMargin + titleLabel.frame.size.height + titleToMessageSpacing + messageLabel.frame.size.height + messageBottomMargin) : contentView!.frame.size.height
Hi i need a graph as attached for ios. I am using ios-Chart library(swift alternative of MPAndroidChart) for swift .
I have managed to get these points on the graph using the scatter graph. But i couldn't figure out how will i connect the two vertical points. Any help or early response will be appreciate able.
my current code is :
func drawChart(dataPoints:[String] , value1 :[Double] , value2:[Double])
{
var dataEntries1:[ChartDataEntry] = []
for i in 0..<dataPoints.count {
let dataEntry = ChartDataEntry(value:value1[i] , xIndex : i)
dataEntries1.append(dataEntry)
}
var dataEntries2:[ChartDataEntry] = []
for i in 0..<dataPoints.count {
let dataEntry = ChartDataEntry(value:value2[i] , xIndex : i)
dataEntries2.append(dataEntry)
}
let dataSet1 = ScatterChartDataSet(yVals: dataEntries1, label: "Value1" )
dataSet1 .setColor(UIColor.blueColor())
let dataSet2 = ScatterChartDataSet(yVals: dataEntries2 ,label: "Value2")
dataSet2.setColor(UIColor.greenColor())
var bloodPressureDataSets = [ScatterChartDataSet]()
bloodPressureDataSets.append(dataSet1)
bloodPressureDataSets.append(dataSet2)
let barChartData = ScatterChartData(xVals: dataPoints, dataSets: bloodPressureDataSets)
bpChart.xAxis.labelPosition = .Bottom
bpChart.rightAxis.enabled=false
//barChart.legend.enabled=false
bpChart.descriptionText=""
bpChart.data = barChartData
}
Currently i can see this type of graph using the above code.
I want to join these two vertical points like the graph below,
take a look at scatter chart renderer, drawDataSet func. You can connect the dots there
UPDATE towards your comments:
first, go to ScatterChartRenderer and locate to
internal func drawDataSet(context context: CGContext, dataSet: ScatterChartDataSet)
This is where we calculate the position and draw the shape here
There is a main loop:
for (var j = 0, count = Int(min(ceil(CGFloat(entries.count) * _animator.phaseX), CGFloat(entries.count))); j < count; j++)
{
let e = entries[j]
point.x = CGFloat(e.xIndex)
point.y = CGFloat(e.value) * phaseY
point = CGPointApplyAffineTransform(point, valueToPixelMatrix);
...
}
Here's the iteration of the data entries your provide in the dataSet, we just get the xIndex and y value here and convert it to the coordinate on the screen.
So what you can do is to sub class this renderer, override this function to get what you want.
e.g. You want to connect the data entries(the dot) for the same xIndex, you should first iterate each data set to collect all the entries on same xIndex, use CGPointApplyAffineTransform(point, valueToPixelMatrix) to convert and use CoreGraphics APIs to draw the line. You don't need to worry about the math, the library already gives you the API to convert between data value and the screen coordinate value. You just focus on how to draw the chart.
I wish I had more reputation so I could attach a screenshot of what I'm working on but you can see an example in the second link down below.
Basically there is a middle button in the tab bar which brings out option buttons (apple images in this case).
These apple icons are animated to their positions using UIDynamicAnimator and UIAttachmentBehavior.
Here is the code which adds the option buttons
func showOptions() {
var numberOfItems = self.delegate.brOptionsButtonNumberOfItems(self)
//NSAssert(numberOfItems > 0 , "number of items should be more than 0")
var angle = 0.0
var radius:Int = 20 * numberOfItems
angle = (180.0 / Double(numberOfItems))
// convert to radians
angle = angle/180.0 * M_PI
for(var i = 0; i<numberOfItems; i++) {
var csCalc = Float((angle * Double(i)) + (angle/2))
var buttonX = Float(radius) * cosf(csCalc)
var buttonY = Float(radius) * sinf(csCalc)
var wut = (angle * Double(i)) + (angle/2)
var brOptionItem = self.createButtonItemAtIndex(i)
var mypoint = self.tabBar.convertPoint(self.center, fromView:self.superview)
var x = mypoint.x + CGFloat(buttonX)
var y = self.frame.origin.y - CGFloat(buttonY)
var buttonPoint = CGPointMake(x, y)
//println("Button Point of Button Item x:\(x) y: \(y)")
brOptionItem.layer.anchorPoint = self.layer.anchorPoint
brOptionItem.center = mypoint
var attachment = UIAttachmentBehavior(item:brOptionItem, attachedToAnchor:buttonPoint)
attachment.damping = self.damping
attachment.frequency = self.frequency
attachment.length = 1
// set the attachment for dragging behavior
brOptionItem.attachment = attachment
self.dynamicItem!.addItem(brOptionItem)
//if(self.delegate.respondsToSelector("willDisplayButtonItem")) { //Fix me
//self.delegate.brOptionsButton(self, willDisplayButtonItem:brOptionItem)
//}
self.tabBar.insertSubview(brOptionItem, belowSubview: self.tabBar)
self.dynamicsAnimator!.addBehavior(attachment)
self.items.addObject(brOptionItem)
}
}
func createButtonItemAtIndex(indexz:NSInteger) -> BROptionsItem {
println("Create button item at index")
var brOptionItem = BROptionsItem(initWithIndex:indexz)
brOptionItem.addTarget(self, action:"buttonItemPressed:", forControlEvents:.TouchUpInside)
brOptionItem.autoresizingMask = UIViewAutoresizing.None
var image = self.delegate.brOptionsButton(self, imageForItemAtIndex:indexz)
//if((image) != nil) {
brOptionItem.setImage(image, forState: UIControlState.Normal)
//}
/*
var buttonTitle = self.delegate.brOptionsButton(self, titleForItemAtIndex:indexz)
if(buttonTitle.utf16Count > 0) {
brOptionItem.setTitle(buttonTitle, forState:UIControlState.Normal)
}
*/
return brOptionItem;
}
In showOptions the animator is assigned an attachment behavior for a brOptionItem that was just created when createButtomItemAtIndex was called.
In createButtonItemAtIndex, the brOptionItem is created and a target was added to execute a function when the button was tapped.
The option buttons work without the animation. I can click them and the target function is executed.
However, when the animation is added and the option buttons are placed where they are, nothing happens when the buttons are tapped.
I am extremely stuck on this. I have no idea why the animation stops the tapping actions. The buttons are supposed to be draggable as well.
Here is my source code. The code I reference can be see in the BROptiosnButton file.
https://github.com/WobbleDev/BROptionsSwift
Here is the reference code I used
https://github.com/BasheerSience/BROptionsButton
Thanks!