在 swift 中创建饼图
Creating a pie chart in swift
我正在尝试在 swift 中创建一个饼图,我想从头开始创建代码而不是使用第 3 方扩展。
我喜欢它是@IBDesignable 的想法,所以我从这个开始:
import Foundation
import UIKit
@IBDesignable class PieChart: UIView {
var data: Dictionary<String,Int>?
required init(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)!
self.contentMode = .Redraw
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
self.contentMode = .Redraw
}
override fun drawRect(rect: CGRect) {
// draw the chart in here
}
}
我不确定的是如何最好地将数据放入图表中。我应该有这样的东西吗:
@IBOutlet weak var pieChart: PieChart!
override func viewDidLoad() {
pieChart.data = pieData
pieChart.setNeedsDisplay()
}
或者有更好的方法吗?想必是没有办法把数据包含在init函数中吧?
提前致谢!
您可以创建一个包含数据的便利 init,但这只有在您从代码创建视图时才有用。如果您的视图被添加到情节提要中,您将需要一种在创建视图后设置数据的方法。
最好查看标准 UI 元素(如 UIButton
)以获取设计线索。您可以更改 UIButton
的属性,它会更新而无需调用 myButton.setNeedsDisplay()
,因此您应该将饼图设计为以相同的方式工作。
拥有一个保存数据的视图 属性 很好。该视图应负责重绘自身,因此为您的 data
属性 定义 didSet
并在那里调用 setNeedsDisplay()
。
var data: Dictionary<String,Int>? {
didSet {
// Data changed. Redraw the view.
self.setNeedsDisplay()
}
}
然后你可以简单的设置数据,饼图会重新绘制:
pieChart.data = pieData
您可以将其扩展到饼图上的其他属性。例如,您可能想要更改背景颜色。您还可以为 属性 定义 didSet
并调用 setNeedsDisplay
.
注意setNeedsDisplay
只是设置了一个flag,view会在后面绘制。多次调用 setNeedsDisplay
不会导致您的视图多次重绘,因此您可以执行以下操作:
pieChart.data = pieData
pieChart.backgroundColor = .redColor()
pieChart.draw3D = true // draw the pie chart in 3D
饼图只会重绘一次。
不行,如果你已经把它添加到故事板的场景中,你不能在init
方法中设置数据(因为init(coder:)
会被调用)。
所以,是的,您可以在 viewDidLoad
中填充饼图的数据。
override func viewDidLoad() {
super.viewDidLoad()
pieChart.dataPoints = ...
}
但是,因为这个 PieChart
是 IBDesignable
,这意味着您可能想在 IB 中查看饼图的再现。因此,您可以在 PieChart
class 中实现 prepareForInterfaceBuilder
,提供一些示例数据:
override public func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
dataPoints = ...
}
这样您现在就可以在 Interface Builder 中享受可设计的视图(例如,查看预览;可以显示其他可检查的属性)。预览图是我们的示例数据,不是运行时会显示的数据,但可能足以欣赏整体设计:
而且,正如 vacawama 所说,您想要将 setNeedsDisplay
移到 属性 的 didSet
观察器中。
public class PieChart: UIView {
public var dataPoints: [DataPoint]? { // use whatever type that makes sense for your app, though I'd suggest an array (which is ordered) rather than a dictionary (which isn't)
didSet { setNeedsDisplay() }
}
@IBInspectable public var lineWidth: CGFloat = 2 {
didSet { setNeedsDisplay() }
}
...
}
为了防止有人再次查看这个问题,我想 post 我完成的代码,以便它对其他人有用。这是:
import Foundation
import UIKit
@IBDesignable class PieChart: UIView {
var dataPoints: Dictionary<String,Double> = ["Alpha":1,"Beta":2,"Charlie":3,"Delta":4,"Echo":2.5,"Foxtrot":1.4] {
didSet { setNeedsDisplay() }
}
@IBInspectable var lineWidth: CGFloat = 1.0 {
didSet { setNeedsDisplay()
}
}
@IBInspectable var lineColor: UIColor = uicolor_normal {
didSet { setNeedsDisplay() }
}
required init(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)!
self.contentMode = .Redraw
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
self.contentMode = .Redraw
}
override func drawRect(rect: CGRect) {
// set font for labels
let fieldColor: UIColor = UIColor.darkGrayColor()
let fieldFont = uifont_piechartkey
var fieldAttributes: NSDictionary = [
NSForegroundColorAttributeName: fieldColor,
NSFontAttributeName: fieldFont!
]
// get the graphics context and prepare an inset box for the pie
let ctx = UIGraphicsGetCurrentContext()
let margin: CGFloat = lineWidth
let box0 = CGRectInset(self.bounds, margin, margin)
let keyHeight = CGFloat( ceil( Double(dataPoints.count) / 3.0) * 24 ) + 16
let side : CGFloat = min(box0.width, box0.height-keyHeight)
let box = CGRectMake((self.bounds.width-side)/2, (self.bounds.height-side-keyHeight)/2,side,side)
let radius : CGFloat = min(box.width, box.height)/2.0
// converts percentages to radians for drawing the segment
func percent_to_rad(p: Double) -> CGFloat {
let rad = CGFloat(p * 0.02 * M_PI)
return rad
}
// draws a segment
func draw_arc(start: CGFloat, end: CGFloat, color: CGColor) {
CGContextBeginPath(ctx)
CGContextMoveToPoint(ctx, box.midX, box.midY)
CGContextSetFillColorWithColor(ctx, color)
CGContextAddArc(ctx,box.midX,box.midY,radius-lineWidth/2,start,end,0)
CGContextClosePath(ctx)
CGContextFillPath(ctx)
}
// draws a key item
func draw_key(keyName: String, keyValue: Double, color: CGColor, keyX: CGFloat, keyY: CGFloat) {
CGContextBeginPath(ctx)
CGContextMoveToPoint(ctx, keyX, keyY)
CGContextSetFillColorWithColor(ctx, color)
CGContextAddArc(ctx,keyX,keyY,8,0,CGFloat(2 * M_PI),0)
CGContextClosePath(ctx)
CGContextFillPath(ctx)
keyName.drawInRect(CGRectMake(keyX + 12,keyY-8,self.bounds.width/3,16),withAttributes: fieldAttributes as? [String : AnyObject])
}
let total = Double(dataPoints.values.reduce(0, combine: +)) // the total of all values
// convert dictionary to sorted touples
let dataPointsArray = dictionary_to_sorted_array(dataPoints)
// now sort the dictionary into an Array
var start = -CGFloat(M_PI_2) // start at 0 degrees, not 90
var end: CGFloat
var i = 0
// draw all segments
for dataPoint in dataPointsArray {
end = percent_to_rad(Double( (dataPoint.value)/total) * 100 )+start
draw_arc(start,end:end,color: uicolors_chart[i%uicolors_chart.count].CGColor)
start = end
i++
}
// the key
var keyX = self.bounds.minX + 8
var keyY = self.bounds.height - keyHeight + 32
i = 0
for dataPoint in dataPointsArray {
draw_key(dataPoint.key, keyValue: dataPoint.value, color: uicolors_chart[i%uicolors_chart.count].CGColor, keyX: keyX, keyY: keyY)
if((i+1)%3 == 0) {
keyX = self.bounds.minX + 8
keyY += 24
} else {
keyX += self.bounds.width / 3
}
i++
}
}
}
这将创建一个饼图,看起来像这样:
[
您需要的其他代码是颜色数组:
let uicolor_chart_1 = UIColor.init(red: 0.0/255, green:153.0/255, blue:255.0/255, alpha:1.0) //16b
let uicolor_chart_2 = UIColor.init(red: 0.0/255, green:200.0/255, blue:120.0/255, alpha:1.0)
let uicolor_chart_3 = UIColor.init(red: 140.0/255, green:220.0/255, blue:0.0/255, alpha:1.0)
let uicolor_chart_4 = UIColor.init(red: 255.0/255, green:240.0/255, blue:0.0/255, alpha:1.0)
let uicolor_chart_5 = UIColor.init(red: 255.0/255, green:180.0/255, blue:60.0/255, alpha:1.0)
let uicolor_chart_6 = UIColor.init(red: 235.0/255, green:60.0/255, blue:150.0/255, alpha:1.0)
let uicolors_chart : [UIColor] = [uicolor_chart_1,uicolor_chart_2,uicolor_chart_3,uicolor_chart_4,uicolor_chart_5,uicolor_chart_6]
以及将字典转换为数组的代码:
func dictionary_to_sorted_array(dict:Dictionary<String,Double>) ->Array<(key:String,value:Double)> {
var tuples: Array<(key:String,value:Double)> = Array()
let sortedKeys = (dict as NSDictionary).keysSortedByValueUsingSelector("compare:")
for key in sortedKeys {
tuples.append((key:key as! String,value:dict[key as! String]!))
}
return tuples
}
我正在尝试在 swift 中创建一个饼图,我想从头开始创建代码而不是使用第 3 方扩展。
我喜欢它是@IBDesignable 的想法,所以我从这个开始:
import Foundation
import UIKit
@IBDesignable class PieChart: UIView {
var data: Dictionary<String,Int>?
required init(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)!
self.contentMode = .Redraw
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
self.contentMode = .Redraw
}
override fun drawRect(rect: CGRect) {
// draw the chart in here
}
}
我不确定的是如何最好地将数据放入图表中。我应该有这样的东西吗:
@IBOutlet weak var pieChart: PieChart!
override func viewDidLoad() {
pieChart.data = pieData
pieChart.setNeedsDisplay()
}
或者有更好的方法吗?想必是没有办法把数据包含在init函数中吧?
提前致谢!
您可以创建一个包含数据的便利 init,但这只有在您从代码创建视图时才有用。如果您的视图被添加到情节提要中,您将需要一种在创建视图后设置数据的方法。
最好查看标准 UI 元素(如 UIButton
)以获取设计线索。您可以更改 UIButton
的属性,它会更新而无需调用 myButton.setNeedsDisplay()
,因此您应该将饼图设计为以相同的方式工作。
拥有一个保存数据的视图 属性 很好。该视图应负责重绘自身,因此为您的 data
属性 定义 didSet
并在那里调用 setNeedsDisplay()
。
var data: Dictionary<String,Int>? {
didSet {
// Data changed. Redraw the view.
self.setNeedsDisplay()
}
}
然后你可以简单的设置数据,饼图会重新绘制:
pieChart.data = pieData
您可以将其扩展到饼图上的其他属性。例如,您可能想要更改背景颜色。您还可以为 属性 定义 didSet
并调用 setNeedsDisplay
.
注意setNeedsDisplay
只是设置了一个flag,view会在后面绘制。多次调用 setNeedsDisplay
不会导致您的视图多次重绘,因此您可以执行以下操作:
pieChart.data = pieData
pieChart.backgroundColor = .redColor()
pieChart.draw3D = true // draw the pie chart in 3D
饼图只会重绘一次。
不行,如果你已经把它添加到故事板的场景中,你不能在init
方法中设置数据(因为init(coder:)
会被调用)。
所以,是的,您可以在 viewDidLoad
中填充饼图的数据。
override func viewDidLoad() {
super.viewDidLoad()
pieChart.dataPoints = ...
}
但是,因为这个 PieChart
是 IBDesignable
,这意味着您可能想在 IB 中查看饼图的再现。因此,您可以在 PieChart
class 中实现 prepareForInterfaceBuilder
,提供一些示例数据:
override public func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
dataPoints = ...
}
这样您现在就可以在 Interface Builder 中享受可设计的视图(例如,查看预览;可以显示其他可检查的属性)。预览图是我们的示例数据,不是运行时会显示的数据,但可能足以欣赏整体设计:
而且,正如 vacawama 所说,您想要将 setNeedsDisplay
移到 属性 的 didSet
观察器中。
public class PieChart: UIView {
public var dataPoints: [DataPoint]? { // use whatever type that makes sense for your app, though I'd suggest an array (which is ordered) rather than a dictionary (which isn't)
didSet { setNeedsDisplay() }
}
@IBInspectable public var lineWidth: CGFloat = 2 {
didSet { setNeedsDisplay() }
}
...
}
为了防止有人再次查看这个问题,我想 post 我完成的代码,以便它对其他人有用。这是:
import Foundation
import UIKit
@IBDesignable class PieChart: UIView {
var dataPoints: Dictionary<String,Double> = ["Alpha":1,"Beta":2,"Charlie":3,"Delta":4,"Echo":2.5,"Foxtrot":1.4] {
didSet { setNeedsDisplay() }
}
@IBInspectable var lineWidth: CGFloat = 1.0 {
didSet { setNeedsDisplay()
}
}
@IBInspectable var lineColor: UIColor = uicolor_normal {
didSet { setNeedsDisplay() }
}
required init(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)!
self.contentMode = .Redraw
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
self.contentMode = .Redraw
}
override func drawRect(rect: CGRect) {
// set font for labels
let fieldColor: UIColor = UIColor.darkGrayColor()
let fieldFont = uifont_piechartkey
var fieldAttributes: NSDictionary = [
NSForegroundColorAttributeName: fieldColor,
NSFontAttributeName: fieldFont!
]
// get the graphics context and prepare an inset box for the pie
let ctx = UIGraphicsGetCurrentContext()
let margin: CGFloat = lineWidth
let box0 = CGRectInset(self.bounds, margin, margin)
let keyHeight = CGFloat( ceil( Double(dataPoints.count) / 3.0) * 24 ) + 16
let side : CGFloat = min(box0.width, box0.height-keyHeight)
let box = CGRectMake((self.bounds.width-side)/2, (self.bounds.height-side-keyHeight)/2,side,side)
let radius : CGFloat = min(box.width, box.height)/2.0
// converts percentages to radians for drawing the segment
func percent_to_rad(p: Double) -> CGFloat {
let rad = CGFloat(p * 0.02 * M_PI)
return rad
}
// draws a segment
func draw_arc(start: CGFloat, end: CGFloat, color: CGColor) {
CGContextBeginPath(ctx)
CGContextMoveToPoint(ctx, box.midX, box.midY)
CGContextSetFillColorWithColor(ctx, color)
CGContextAddArc(ctx,box.midX,box.midY,radius-lineWidth/2,start,end,0)
CGContextClosePath(ctx)
CGContextFillPath(ctx)
}
// draws a key item
func draw_key(keyName: String, keyValue: Double, color: CGColor, keyX: CGFloat, keyY: CGFloat) {
CGContextBeginPath(ctx)
CGContextMoveToPoint(ctx, keyX, keyY)
CGContextSetFillColorWithColor(ctx, color)
CGContextAddArc(ctx,keyX,keyY,8,0,CGFloat(2 * M_PI),0)
CGContextClosePath(ctx)
CGContextFillPath(ctx)
keyName.drawInRect(CGRectMake(keyX + 12,keyY-8,self.bounds.width/3,16),withAttributes: fieldAttributes as? [String : AnyObject])
}
let total = Double(dataPoints.values.reduce(0, combine: +)) // the total of all values
// convert dictionary to sorted touples
let dataPointsArray = dictionary_to_sorted_array(dataPoints)
// now sort the dictionary into an Array
var start = -CGFloat(M_PI_2) // start at 0 degrees, not 90
var end: CGFloat
var i = 0
// draw all segments
for dataPoint in dataPointsArray {
end = percent_to_rad(Double( (dataPoint.value)/total) * 100 )+start
draw_arc(start,end:end,color: uicolors_chart[i%uicolors_chart.count].CGColor)
start = end
i++
}
// the key
var keyX = self.bounds.minX + 8
var keyY = self.bounds.height - keyHeight + 32
i = 0
for dataPoint in dataPointsArray {
draw_key(dataPoint.key, keyValue: dataPoint.value, color: uicolors_chart[i%uicolors_chart.count].CGColor, keyX: keyX, keyY: keyY)
if((i+1)%3 == 0) {
keyX = self.bounds.minX + 8
keyY += 24
} else {
keyX += self.bounds.width / 3
}
i++
}
}
}
这将创建一个饼图,看起来像这样:
[
您需要的其他代码是颜色数组:
let uicolor_chart_1 = UIColor.init(red: 0.0/255, green:153.0/255, blue:255.0/255, alpha:1.0) //16b
let uicolor_chart_2 = UIColor.init(red: 0.0/255, green:200.0/255, blue:120.0/255, alpha:1.0)
let uicolor_chart_3 = UIColor.init(red: 140.0/255, green:220.0/255, blue:0.0/255, alpha:1.0)
let uicolor_chart_4 = UIColor.init(red: 255.0/255, green:240.0/255, blue:0.0/255, alpha:1.0)
let uicolor_chart_5 = UIColor.init(red: 255.0/255, green:180.0/255, blue:60.0/255, alpha:1.0)
let uicolor_chart_6 = UIColor.init(red: 235.0/255, green:60.0/255, blue:150.0/255, alpha:1.0)
let uicolors_chart : [UIColor] = [uicolor_chart_1,uicolor_chart_2,uicolor_chart_3,uicolor_chart_4,uicolor_chart_5,uicolor_chart_6]
以及将字典转换为数组的代码:
func dictionary_to_sorted_array(dict:Dictionary<String,Double>) ->Array<(key:String,value:Double)> {
var tuples: Array<(key:String,value:Double)> = Array()
let sortedKeys = (dict as NSDictionary).keysSortedByValueUsingSelector("compare:")
for key in sortedKeys {
tuples.append((key:key as! String,value:dict[key as! String]!))
}
return tuples
}