Gráficos no iOS utilizando Charts

Gráficos no iOS utilizando Charts

Existe um ditado que diz: “Uma imagem vale por mais que mil palavras”. Este ditado também vale para quando precisamos apresentar números. Um gráfico pode melhorar muito o entendimento sobre uma informação que queremos apresentar em um aplicativo. No caso do iOS temos algumas bibliotecas disponíveis no GitHub sobre o tema. Hoje iremos falar sobre a biblioteca Charts.

A biblioteca criada por Daniel Cohen Gindi foi baseada na biblioteca MPAndroidCharts desenvolvida por Philipp Jahoda para o Andoid. A biblioteca foi desenvolvida em Swift e já está migrada para Swift 3.

Entre os recursos da biblioteca podemos destacar:

  • 8 tipos diferentes de gráficos;
  • Redimensionamento por gesto;
  • Gráficos combinados;
  • Salvar o gráfico no rolo da câmera / Exportar para png/jpeg;
  • Animações;
  • Totalmente customizável;
  • Plota dados diretamente do banco de dados Realm.io.

Segue alguns exemplos de gráfico feitos com o Chart.

  • LineChart (with legend, simple design)
    linechart-01
    linechart-02
  • LineChart (cubic lines)
    linechart-03
  • LineChart (gradient fill)
    linechart-04
  • Combined-Chart (bar- and linechart in this case)
    linechart_and_barchart
  • BarChart (with legend, simple design)
    barchart-01
  • BarChart (grouped DataSets)
    barchart-02
  • Horizontal-BarChart
    horizontalbarchart
  • PieChart (with selection, …)
    piechart
  • ScatterChart (with squares, triangles, circles, … and more)
    scatterchart
  • CandleStickChart (for financial data)
    bubblechart
  • RadarChart (spider web chart)
    radarchart

Vamos criar um projeto no Xcode utilizando a biblioteca.

Criando o projeto

Iremos criar um projeto utilizando UITabBar que terá três abas. A primeira irá mostrar um gráfico de pizza (PieChart) com as profissões dos prefeitos eleitos. A segunda irá mostrar um gráfico de barras (BarChart) com a idade média dos prefeitos eleitos. A terceira irá mostrar um gráfico de barras horizontal (HorizontalBarChart) com a quantidade de prefeitos eleitos por partido.

Todos os dados foram extraídos do site do TRE e referem-se aos prefeitos eleitos em Minas Gerais nas eleições de 2016.

O aplicativo ficará assim:

electoral-statistics-2016

Instalação

A biblioteca pode ser instalada adicionando o fonte ao projeto, via Carthage ou via CocoaPods. Iremos instalar via CocoaPods com o comando pod 'Charts'. Você pode encontrar mais informações sobre o CocoaPods aqui.

Criando o modelo

Para cada gráfico iremos criar uma classe para servir de modelo.

import UIKit
import Charts

public class Occupation: NSObject {
  public struct OccupationValues {
    public var label: String
    public var value: Double
    public var percentage: Double
  }

  fileprivate var data: [OccupationValues]
  fileprivate var cutOff: Double
  public let total = 849.0

  public init(cutOff: Double = 1.7) {
    self.data = [OccupationValues]()
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Mayor", value: "Mayor", comment: ""), value: 139))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Businessman", value: "Businessman", comment: ""), value: 117))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Merchant", value: "Merchant", comment: ""), value: 63))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.AgriculturalProducer", value: "Agricultural Producer", comment: ""), value: 46))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Farmer", value: "Farmer", comment: ""), value: 44))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Doctor", value: "Doctor", comment: ""), value: 39))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Others", value: "Others", comment: ""), value: 39))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Lawyer", value: "Lawyer", comment: ""), value: 38))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.MunicipalPublicServer", value: "Municipal Public Server", comment: ""), value: 38))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.RetiredExceptPublicServant", value: "Retired (Except Public Servant)", comment: ""), value: 35))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Administrator", value: "Administrator", comment: ""), value: 31))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.CattleBreeder", value: "Cattle Breeder", comment: ""), value: 20))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.HighSchoolTeacher", value: "High School Teacher", comment: ""), value: 18))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Engineer", value: "Engineer", comment: ""), value: 17))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Councilman", value: "Councilman", comment: ""), value: 17))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.StatePublicServer", value: "State Public Server", comment: ""), value: 15))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.CivilPublicServantRetired", value: "Civil Public Servant Retired", comment: ""), value: 9))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.ElementarySchoolTeacher", value: "Elementary School Teacher", comment: ""), value: 8))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.FreightTransportDriver", value: "Freight Transport Driver", comment: ""), value: 7))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.BankingAndEconomics", value: "Banking and Economics", comment: ""), value: 6))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Accountant", value: "Accountant", comment: ""), value: 6))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.ConstructionWorker", value: "Construction Worker", comment: ""), value: 6))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Dentistry", value: "Dentistry", comment: ""), value: 5))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Veterinary", value: "Veterinary", comment: ""), value: 5))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.DirectorOfEducationalEstablishment", value: "Director of Educational Establishment", comment: ""), value: 4))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Pharmaceutical", value: "Pharmaceutical", comment: ""), value: 4))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.HigherEducationTeacher", value: "Higher Education Teacher", comment: ""), value: 4))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.RuralWorker", value: "Rural Worker", comment: ""), value: 4))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.OfficeAssistantAndSimilar", value: "Office Assistant and Similar", comment: ""), value: 3))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.HairdresserAndBarber", value: "Hairdresser and Barber", comment: ""), value: 3))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Merchant", value: "Merchant", comment: ""), value: 3))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.FederalPublicServer", value: "Federal Public Server", comment: ""), value: 3))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.TechnicalAccountingStatisticsHomeEconomicsAndAdministration", value: "Technical, Accounting, Statistics, Home Economics and Administration", comment: ""), value: 3))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.TechnicianInAgronomyAndSurveying", value: "Technician in Agronomy and Surveying", comment: ""), value: 3))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.SocialWorker", value: "Social Worker", comment: ""), value: 3))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.RealEstateAgentInsuranceSecuritiesAndExchange", value: "Real Estate Agent, Insurance, Securities and Exchange", comment: ""), value: 2))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Deputy", value: "Deputy", comment: ""), value: 2))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.BusinessDirector", value: "Business Director", comment: ""), value: 2))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Housewife", value: "Housewife", comment: ""), value: 2))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Manager", value: "Manager", comment: ""), value: 2))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.PersonalDriver", value: "Personal Driver", comment: ""), value: 2))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.OccupantOfPositionInCommission", value: "Occupant of Position in Commission", comment: ""), value: 2))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.CommercialRepresentative", value: "Commercial Representative", comment: ""), value: 2))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.AdministrativeAgent", value: "Administrative Agent", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.HealthAndSanitaryAgent", value: "Health and Sanitary Agent", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.PostalAgent", value: "Postal Agent", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Architect", value: "Architect", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.ElectricianAndSimilar", value: "Electrician and Similar", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Nurse", value: "Nurse", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Fiscal", value: "Fiscal", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.PhysiotherapistAndOccupationalTherapist", value: "Physiotherapist and Occupational Therapist", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.JournalistAndEditor", value: "Journalist and Editor", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.ManicureAndMakeupArtist", value: "Manicure and Makeup Artist", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.PassengerTransportVehicleDriver", value: "Passenger Transport Vehicle Driver", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Fisherman", value: "Fisherman", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.CivilPolice", value: "Civil Police", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.MilitaryPolice", value: "Military Police", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.TeacherAndProfessionalTrainingInstructor", value: "Teacher and Professional Training Instructor", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Psychologist", value: "Psychologist", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Adman", value: "Adman", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Chemical", value: "Chemical", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.JusticeServant", value: "Justice Servant", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Sociologist", value: "Sociologist", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.SupervisorInspectorAndPurchasingAndSalesAgent", value: "Supervisor Inspector and Purchasing and Sales Agent", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.Cabby", value: "Cabby", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.ElectricianElectronicsAndTelecommunicationsTechnician", value: "Electrician, Electronics and Telecommunications Technician", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.NursingTechnicianAndSimilarExceptNurse", value: "Nursing Technician and Similar (Except Nurse)", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.CivilWorksRoadsSanitationAndSimilar", value: "Civil Works, Roads, Sanitation and Similar", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.MetallurgicalAndSteelworker", value: "Metallurgical and Steelworker", comment: ""), value: 1))
    self.data.append(OccupationValues(label: NSLocalizedString("Occupation.RetailAndWholesalerRetailer", value: "Retail and Wholesaler Retailer", comment: ""), value: 1))
    self.cutOff = cutOff

    super.init()
  }

  public func occupation() -> [OccupationValues] {
    var occupations = [OccupationValues]()
    var others = 0.0
    let othersLabel = String(format: NSLocalizedString("Occupation.UnderCutOff", value: "Under %.2f%%", comment: ""), cutOff)

    for occupation in data {
      if occupation.value / total * 100.0 > cutOff {
        occupations.append(occupation)
      } else {
        others = others + occupation.value
      }
    }

    if others > 0.0 {
      occupations.append(OccupationValues(label: othersLabel, value: others))
    }

    return occupations
  }
}

extension Occupation: IAxisValueFormatter {
  public func stringForValue(_ value: Double, axis: AxisBase?) -> String {
    return data[Int(value)].label
  }
}

Para o primeiro gráfico criaremos a classe Occupation que possui três propriedades: data que é um array de OccupationValues e armazena os registros das ocupações dos prefeitos. A struct OccupationValues possui duas propriedades label e value. Usaremos a propriedade value para montar o gráfico e a propriedade label para a legenda.

A propriedade cutOff serve para informarmos o percentual mínimo de prefeitos com determinada ocupação que irá aparecer no gráfico. Os que estiverem abaixo deste percentual serão agrupados em um registro “Abaixo de X%”.

A propriedade total serve para calcularmos o percentual de prefeitos em cada ocupação.

No initializer criamos os registros e no método occupation() retornamos os registros utilizando o percentual de corte.

Para que o gráfico obtenha os labels dos registros apresentados temos que passar um objeto que implemente o protocolo IAxisValueFormatter. O protocolo exige um objeto que herde de NSObject e possui apenas o método stringForValue(_:axis:) que retorna uma String. O método recebe o valor plotado e o eixo a qual ele pertence.

import UIKit
import Charts

public class AgeGroup: NSObject {
  public struct AgeGroupValues {
    public var label: String
    public var value: Double
  }

  fileprivate var data: [AgeGroupValues]
  fileprivate let total = 849.0

  override public init() {
    self.data = [AgeGroupValues]()
    self.data.append(AgeGroupValues(label: NSLocalizedString("AgeGroup.25To29", value: "25\n to\n 29", comment: ""), value: 14))
    self.data.append(AgeGroupValues(label: NSLocalizedString("AgeGroup.30TO34", value: "30\n to\n 34", comment: ""), value: 39))
    self.data.append(AgeGroupValues(label: NSLocalizedString("AgeGroup.35TO39", value: "35\n to\n 39", comment: ""), value: 75))
    self.data.append(AgeGroupValues(label: NSLocalizedString("AgeGroup.40TO44", value: "40\n to\n 44", comment: ""), value: 118))
    self.data.append(AgeGroupValues(label: NSLocalizedString("AgeGroup.45TO49", value: "45\n to\n 49", comment: ""), value: 133))
    self.data.append(AgeGroupValues(label: NSLocalizedString("AgeGroup.50TO54", value: "50\n to\n 54", comment: ""), value: 178))
    self.data.append(AgeGroupValues(label: NSLocalizedString("AgeGroup.55TO59", value: "55\n to\n 59", comment: ""), value: 116))
    self.data.append(AgeGroupValues(label: NSLocalizedString("AgeGroup.60TO64", value: "60\n to\n 64", comment: ""), value: 96))
    self.data.append(AgeGroupValues(label: NSLocalizedString("AgeGroup.65TO69", value: "65\n to\n 69", comment: ""), value: 44))
    self.data.append(AgeGroupValues(label: NSLocalizedString("AgeGroup.70TO74", value: "70\n to\n 74", comment: ""), value: 24))
    self.data.append(AgeGroupValues(label: NSLocalizedString("AgeGroup.75TO79", value: "75\n to\n 79", comment: ""), value: 9))
    self.data.append(AgeGroupValues(label: NSLocalizedString("AgeGroup.80TO84", value: "80\n to\n 84", comment: ""), value: 2))
    self.data.append(AgeGroupValues(label: NSLocalizedString("AgeGroup.85TO89", value: "85\n to\n 89", comment: ""), value: 1))

    super.init()
  }

  public func ageGroups() -> [AgeGroupValues] {
    return data
  }
}

extension AgeGroup: IAxisValueFormatter {
  public func stringForValue(_ value: Double, axis: AxisBase?) -> String{
    return data[Int(value)].label
  }
}

Fizemos o mesmo para a classe AgeGroup para armazenar as idades dos prefeitos eleitos. A diferença é aqui não temos um valor de corte e o método ageGroups() retorna todos os registros em data.

import UIKit
import Charts

class Party: NSObject {
  public struct PartyValues {
    public var label: String
    public var value: Double
  }

  fileprivate var data: [PartyValues]
  fileprivate var cutOff: Double
  fileprivate let total = 849.0

  public init(cutOff: Double = 0.0) {
    self.data = [PartyValues]()
    self.data.append(PartyValues(label: "DEM", value: 53))
    self.data.append(PartyValues(label: "PC do B", value: 3))
    self.data.append(PartyValues(label: "PDT", value: 30))
    self.data.append(PartyValues(label: "PEN", value: 2))
    self.data.append(PartyValues(label: "PHS", value: 16))
    self.data.append(PartyValues(label: "PMDB", value: 164))
    self.data.append(PartyValues(label: "PMN", value: 6))
    self.data.append(PartyValues(label: "PP", value: 54))
    self.data.append(PartyValues(label: "PPS", value: 30))
    self.data.append(PartyValues(label: "PR", value: 60))
    self.data.append(PartyValues(label: "PRB", value: 27))
    self.data.append(PartyValues(label: "PROS", value: 9))
    self.data.append(PartyValues(label: "PRTB", value: 1))
    self.data.append(PartyValues(label: "PSB", value: 48))
    self.data.append(PartyValues(label: "PSC", value: 9))
    self.data.append(PartyValues(label: "PSD", value: 56))
    self.data.append(PartyValues(label: "PSDB", value: 132))
    self.data.append(PartyValues(label: "PSDC", value: 2))
    self.data.append(PartyValues(label: "PSL", value: 4))
    self.data.append(PartyValues(label: "PT", value: 41))
    self.data.append(PartyValues(label: "PT do B", value: 6))
    self.data.append(PartyValues(label: "PTB", value: 48))
    self.data.append(PartyValues(label: "PTC", value: 5))
    self.data.append(PartyValues(label: "PTN", value: 2))
    self.data.append(PartyValues(label: "PV", value: 27))
    self.data.append(PartyValues(label: "SD", value: 14))
    self.cutOff = cutOff

    super.init()
  }

  public func parties() -> [PartyValues] {
    var parties = [PartyValues]()
    var others = 0.0
    let othersDescription = NSLocalizedString("Party.Others", value: "Others", comment: "")

    for party in data {
      if party.value / total * 100.0 > cutOff {
        parties.append(party)
      } else {
        others = others + party.value
      }
    }

    if others > 0.0 {
      parties.append(PartyValues(label: othersDescription, value: others))
    }

    return parties
  }
}

extension Party: IAxisValueFormatter {
  public func stringForValue(_ value: Double, axis: AxisBase?) -> String{
    return data[Int(value)].label
  }
}

Por último temos a classe Party com os dados dos partidos dos prefeitos eleitos.

Criando a interface

Nosso projeto terá três UIViewController responsáveis pela apresentação dos gráficos. São eles OccupationsViewController, AgeGroupsViewController e PartiesViewController. Cada viewController terá um UILabel e um UIView. Adicione as constraints necessárias e altere a classe das views para PieChartView, BarChartView e HorizontalBarChartView respectivamente.

storyboard

import UIKit
import Charts
import Foundation

class OccupationsViewController: UIViewController {
  @IBOutlet weak var titleLabel: UILabel!
  @IBOutlet weak var professionChart: PieChartView!

  fileprivate lazy var percentageFormatter: NumberFormatter = {
    let percentageFormatter = NumberFormatter()
    percentageFormatter.numberStyle = .percent
    percentageFormatter.minimumFractionDigits = 2
    percentageFormatter.maximumFractionDigits = 2

    return percentageFormatter
  }()

  override func viewDidLoad() {
    super.viewDidLoad()

    self.navigationItem.title = NSLocalizedString("Occupation.Navigation.Title", value: "Electoral Statistics 2016", comment: "")
    titleLabel.text = NSLocalizedString("Occupation.TabBar.Title", value: "Occupation of the Elected Mayors - Minas Gerais", comment: "")
  }

  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    loadData()
  }

  private func loadData() {
    // Get and prepare the data
    let occupations = Occupation().occupation()

    // Initialize an array to store chart data entries (values; y axis)
    var occupationEntries = [PieChartDataEntry]()

    var i = 0.0

    for occupation in occupations {
      // Create single chart data entry and append it to the array
      occupationEntries.append(PieChartDataEntry(value: occupation.value, label: occupation.label))

      i += 1
    }

    // Create bar chart data set containing salesEntries
    let chartDataSet = PieChartDataSet(values: occupationEntries, label: NSLocalizedString("Occupation.PieChart.Label", value: "Parties", comment: ""))

    // Create bar chart data with data set and array with values for x axis
    professionChart.data = PieChartData(dataSet: chartDataSet)
  }
}

A classe OccupationsViewController possui dois outlets para ligarmos com o label e o gráfico criados no storyboard. No método viewDidLoad() definimos os títulos da tela e no método viewWillAppear(_:) chamamos o método loadData() que irá popular o gráfico. Também temos a propriedade lazy percentageFormatter do tipo NumberFormatter que iremos utilizar posteriormente.

No método loadData() instanciamos um objeto do nosso modelo e armazenamos os dados na propriedade occupations. Criamos um array de objetos da classe PieChartDataEntry onde iremos instanciar um objeto com o initializer PieChartDataEntry(value:label:) para cada valor do nosso modelo.

Com o array preenchido criaremos um PieChartDataSet com o initializer PieChartDataSet(values:label:)

Rodando o aplicativo podemos ver o gráfico.

occupation-parcial

Como pudemos ver na imagem acima o gráfico possui apenas uma cor. Vamos criar um novo arquivo Utils.swift e acrescentar o código abaixo para criarmos uma paleta de cores.

import UIKit
import Foundation

public extension UIColor {
  public static var turquoise: UIColor {
    return UIColor(red: 26/255.0, green: 188/255.0, blue: 256/255.0, alpha: 1.0)
  }

  public static var emerland: UIColor {
    return UIColor(red: 46/255.0, green: 204/255.0, blue: 113/255.0, alpha: 1.0)
  }

  public static var peterriver: UIColor {
    return UIColor(red: 52/255.0, green: 152/255.0, blue: 219/255.0, alpha: 1.0)
  }

  public static var amethyst: UIColor {
    return UIColor(red: 155/255.0, green: 89/255.0, blue: 182/255.0, alpha: 1.0)
  }

  public static var wetaspalth: UIColor {
    return UIColor(red: 52/255.0, green: 73/255.0, blue: 94/255.0, alpha: 1.0)
  }

  public static var greensea: UIColor {
    return UIColor(red: 22/255.0, green: 160/255.0, blue: 133/255.0, alpha: 1.0)
  }

  public static var nephritis: UIColor {
    return UIColor(red: 39/255.0, green: 174/255.0, blue: 96/255.0, alpha: 1.0)
  }

  public static var belizehole: UIColor {
    return UIColor(red: 41/255.0, green: 128/255.0, blue: 185/255.0, alpha: 1.0)
  }

  public static var wisteria: UIColor {
    return UIColor(red: 142/255.0, green: 68/255.0, blue: 173/255.0, alpha: 1.0)
  }

  public static var midnightblue: UIColor {
    return UIColor(red: 44/255.0, green: 62/255.0, blue: 80/255.0, alpha: 1.0)
  }

  public static var sunflower: UIColor {
    return UIColor(red: 241/255.0, green: 196/255.0, blue: 15/255.0, alpha: 1.0)
  }

  public static var carrot: UIColor {
    return UIColor(red: 230/255.0, green: 126/255.0, blue: 34/255.0, alpha: 1.0)
  }

  public static var alizarin: UIColor {
    return UIColor(red: 231/255.0, green: 76/255.0, blue: 60/255.0, alpha: 1.0)
  }

  public static var clouds: UIColor {
    return UIColor(red: 236/255.0, green: 240/255.0, blue: 241/255.0, alpha: 1.0)
  }

  public static var concrete: UIColor {
    return UIColor(red: 149/255.0, green: 165/255.0, blue: 166/255.0, alpha: 1.0)
  }

  public static var orange: UIColor {
    return UIColor(red: 243/255.0, green: 156/255.0, blue: 18/255.0, alpha: 1.0)
  }

  public static var pumpkin: UIColor {
    return UIColor(red: 211/255.0, green: 84/255.0, blue: 0/255.0, alpha: 1.0)
  }

  public static var pomegranate: UIColor {
    return UIColor(red: 192/255.0, green: 57/255.0, blue: 43/255.0, alpha: 1.0)
  }

  public static var silver: UIColor {
    return UIColor(red: 189/255.0, green: 195/255.0, blue: 199/255.0, alpha: 1.0)
  }

  public static var asbestos: UIColor {
    return UIColor(red: 127/255.0, green: 140/255.0, blue: 141/255.0, alpha: 1.0)
  }

  public static func flatColors() -> [UIColor] {
    return [turquoise, emerland, peterriver, amethyst, wetaspalth, greensea, nephritis, belizehole, wisteria, midnightblue, sunflower, carrot, alizarin, concrete, orange, pumpkin, pomegranate, silver, asbestos]
  }
}

Criamos algumas propriedades estáticas no struct UIColor que retornam novos objetos UIColor. Também criamos o método flatColors() que retorna um array com as novas cores.

Voltando ao método loadData() da classe OccupationsViewController vamos acrescentar o código abaixo.

// 1- Remove description text
professionChart.chartDescription?.text = ""

// 2 - Put some color to chart
chartDataSet.colors = UIColor.flatColors()

// 3 - Hide the legend
professionChart.drawEntryLabelsEnabled = false

// 4 - Fill the hole in middle of the chart
professionChart.drawHoleEnabled = false

// 5 - Put some animation
professionChart.animate(yAxisDuration: 1.5, easingOption: .easeInOutCirc)

Realizamos algumas alterações no gráfico:

  • 1 – Removemos o texto “Description Label” que aparecia como descrição do gráfico
  • 2 – Atribuimos a nossa paleta de cores ao gráfico. O Charts possui a classe ChartColorTemplates com algumas paletas.
  • 3 – Mostramos a legenda do gráfico no rodapé.
  • 4 – Preenchemos a região central do gráfico.
  • 5 – Adicionamos uma animação na plotagem do gráfico.

Depois de realizar as alterações o gráfico ficará assim:

occupation_final

Por último iremos habilitar o clique no gráfico para mostrar o detalhe do registro. Pra isto temos que definir o controller como o delegate do gráfico.

professionChart.delegate = self

Depois implementar o protocolo ChartViewDelegate.

extension OccupationsViewController: ChartViewDelegate {
  func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) {
    if let entry = entry as? PieChartDataEntry {
      let percentage = entry.value / Occupation().total

      let alert = UIAlertController(title: entry.label!, message: String(format: NSLocalizedString("Occupation.Alert.Message", value: "There are %.0f (%@) elected mayors with %@ occupation.", comment: ""), entry.value, percentageFormatter.string(from: NSNumber(value: percentage))!, entry.label!), preferredStyle: .alert)

      let closeAction = UIAlertAction(title: NSLocalizedString("Occupation.Alert.Close", value: "Close", comment: ""), style: .default) { (action) in
        self.professionChart.highlightValues(nil)

        alert.dismiss(animated: true, completion: nil)
      }
      alert.addAction(closeAction)

      self.present(alert, animated: true, completion: nil)
    }
  }
}

No protocolo ChartViewDelegate iremos implementar o método chartValueSelected(_:entry:highlight:). O método recebe uma referencia ao gráfico, o PieChartDataEntry e um objeto Highlight que contém a informação como a posição da seção do gráfico, o dataSetIndex, etc.

Primiero calculamos o percentual do valor do registro sobre o valor total e depois mostramos a informação em um alert. Ao clicar no botão fechar chamamos o método highlightValues(_:) passando nil para desselecionar a região destacada.

O destaque ficará assim:

occupation-final

Repetiremos o mesmo processo com a classe AgeGroupsViewController. A partir de agora destacarei apenas as diferenças entre os gráficos.

import UIKit
import Charts

class AgeGroupsViewController: UIViewController {
  @IBOutlet weak var titleLabel: UILabel!
  @IBOutlet weak var barChart: BarChartView!

  override func viewDidLoad() {
    super.viewDidLoad()

    self.navigationItem.title = NSLocalizedString("AgeGroup.Navigation.Title", value: "Electoral Statistics 2016", comment: "")
    titleLabel.text = NSLocalizedString("AgeGroup.TabBar.Title", value: "Age Group of the Elected Mayors - Minas Gerais", comment: "")
  }

  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    loadData()
  }

  private func loadData() {
    // Get and prepare the data
    let ageGroupModel = AgeGroup()
    let groups = ageGroupModel.ageGroups()

    // Initialize an array to store chart data entries (values; y axis)
    var ageGroupEntries = [BarChartDataEntry]()

    var i = 0.0

    for group in groups {
      // Create single chart data entry and append it to the array
      ageGroupEntries.append(BarChartDataEntry(x: i, y: group.value))

      i += 1
    }

    // Create bar chart data set containing salesEntries
    let chartDataSet = BarChartDataSet(values: ageGroupEntries, label: NSLocalizedString("AgeGroup.PieChart.Label", value: "Age in Years", comment: ""))

    // Create bar chart data with data set and array with values for x axis
    barChart.data = BarChartData(dataSet: chartDataSet)
  }
}

No gráfico de barra utilizaremos um array de objetos da classe BarChartDataEntry. Instanciamos o objeto com o initializer BarChartDataEntry(x:y:).

O gráfico ficará assim.

age-parcial

// 1 - Remove description text
barChart.chartDescription?.text = ""

// 2 - Show the month label
barChart.xAxis.labelPosition = .bottom
barChart.xAxis.valueFormatter = ageGroupModel
barChart.xAxis.labelCount = Int(i)

// 3 - Set the chart scale
barChart.leftAxis.axisMinimum = 0.0
barChart.leftAxis.axisMaximum = 200.0

// 4 - Put some color to chart
chartDataSet.colors = ChartColorTemplates.colorful()

// 5 - Change the font of values
chartDataSet.valueFont = UIFont.preferredFont(forTextStyle: .caption2)

// 6 - Hide the legend
barChart.legend.enabled = false

// 7 - Disable zoom
barChart.scaleYEnabled = false
barChart.scaleXEnabled = false
barChart.doubleTapToZoomEnabled = false

// 8 - Hide grid lines
barChart.rightAxis.enabled = false
barChart.xAxis.drawGridLinesEnabled = false
barChart.leftAxis.enabled = false

// 9 - Put some animation
barChart.animate(yAxisDuration: 1.5, easingOption: .easeInOutQuart)
  • 1 – Removemos o texto “Description Label” que aparecia como descrição do gráfico
  • 2 – Definimos a posição da legenda para a parte inferior do gráfico;
    Definimos o nosso model a propriedade valueFormatter que utilizará o método stringForValue(_:axis:) do protocolo IAxisValueFormatter para obter a legenda;
    definimos a quantidade de legendas.
  • 3 – Definimos a escala do gráfico para começar do zero e ir até o 200.
  • 4 – Atribuimos a nossa paleta de cores ao gráfico.
  • 5 – Alteramos a fonte dos valores mostrados no gráfico.
  • 6 – Habilitamos as legendas.
  • 7 – Desabilitamos o zoom.
  • 8 – Escondemos as linhas de grid do gráfico.
  • 9 – Adicionamos uma animação na plotagem do gráfico.

Após as alterações o gráfico ficará assim.

age_final

Adicionamos o controller como delegate do gráfico.

barChart.delegate = self

E implementamos o protocolo ChartViewDelegate.

extension AgeGroupsViewController: ChartViewDelegate {
  func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) {
    let ageGroups = AgeGroup().ageGroups()
    let index = Int(entry.x)
    let label = ageGroups[index].label.replacingOccurrences(of: "\n", with: " ")

    let alert = UIAlertController(title: label, message: String(format: NSLocalizedString("AgeGroup.Alert.Message", value: "There was %.0f elected mayors between %@ years.", comment: ""), label, ageGroups[index].value), preferredStyle: .alert)

    let closeAction = UIAlertAction(title: NSLocalizedString("AgeGroup.Alert.Close", value: "Close", comment: ""), style: .default) { (action) in
      self.barChart.highlightValues(nil)

      alert.dismiss(animated: true, completion: nil)
    }
    alert.addAction(closeAction)

    self.present(alert, animated: true, completion: nil)
  }
}

No método chartValueSelected(_:entry:highlight:) utilizamos a propriedade x do objeto ChartDataEntry para obtermos o label no nosso model.

O alert ficará assim.

age-highlight

Vamos ao último controller. A classe PartiesViewController.

import UIKit
import Charts

class PartiesViewController: UIViewController {
  @IBOutlet weak var titleLabel: UILabel!
  @IBOutlet weak var HorizontalBarChart: HorizontalBarChartView!

  override func viewDidLoad() {
    super.viewDidLoad()

    self.navigationItem.title = NSLocalizedString("Party.Navigation.Title", value: "Electoral Statistics 2016", comment: "")
    titleLabel.text = NSLocalizedString("Party.TabBar.Title", value: "Party of the Elected Mayors - Minas Gerais", comment: "")
  }

  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    loadData()
  }

  private func loadData() {
    // Get and prepare the data
    let partyModel = Party()
    let sales = partyModel.parties()

    // Initialize an array to store chart data entries (values; y axis)
    var partiesEntries = [BarChartDataEntry]()

    var i = 0.0

    for sale in sales {
      // Create single chart data entry and append it to the array
      partiesEntries.append(BarChartDataEntry(x: i, y: sale.value))

      i += 1
    }

    // Create bar chart data set containing salesEntries
    let chartDataSet = BarChartDataSet(values: partiesEntries, label: NSLocalizedString("Party.HorizontalBarChart.Label", value: "Parties", comment: ""))

    // Create bar chart data with data set and array with values for x axis
    HorizontalBarChart.data = BarChartData(dataSet: chartDataSet)
  }
}

O HorizontalBarChartView utiliza os mesmos objetos da classe BarChartView e nesta parte não há diferenças.

O gráfico ficará assim.

party-parcial

// 1 - Remove description text
    HorizontalBarChart.chartDescription?.text = ""

    // 2 - Hide the legend
    HorizontalBarChart.legend.enabled = false

    // 3 - Show the month label
    HorizontalBarChart.xAxis.labelPosition = .bottom
    HorizontalBarChart.xAxis.valueFormatter = partyModel
    HorizontalBarChart.xAxis.labelCount = Int(i)

    // 4 - Put some color to chart
    chartDataSet.colors = UIColor.flatColors()

    // 5 - Change the font size
    chartDataSet.valueFont = UIFont.preferredFont(forTextStyle: .caption2)

    // 6 - Hide grid lines
    HorizontalBarChart.rightAxis.enabled = false
    HorizontalBarChart.xAxis.drawGridLinesEnabled = false
    HorizontalBarChart.leftAxis.enabled = false

    // 7 - Set the chart scale
    HorizontalBarChart.leftAxis.axisMinimum = 0.0

    // 8 - Disable zoom
    HorizontalBarChart.scaleYEnabled = false
    HorizontalBarChart.scaleXEnabled = false
    HorizontalBarChart.doubleTapToZoomEnabled = false

    // Put some animation
    HorizontalBarChart.animate(yAxisDuration: 1.5, easingOption: .linear)

Também não tivemos diferenças com relação ao AgeGroupsViewController.

O gráfico ficará assim.

party_final

Pra finalizar definimos o nosso delegate.

HorizontalBarChart.delegate = self

E implementamos o protocolo ChartViewDelegate.

extension PartiesViewController: ChartViewDelegate {
  func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) {
    let parties = Party().parties()
    let index = Int(entry.x)

    let alert = UIAlertController(title: parties[index].label, message: String(format: NSLocalizedString("Party.Alert.Message", value: "The %@ had %.0f votes.", comment: ""), parties[index].label, parties[index].value), preferredStyle: .alert)

    let closeAction = UIAlertAction(title: NSLocalizedString("Party.Alert.Close", value: "Close", comment: ""), style: .default) { (action) in
      self.HorizontalBarChart.highlightValues(nil)

      alert.dismiss(animated: true, completion: nil)
    }
    alert.addAction(closeAction)

    self.present(alert, animated: true, completion: nil)
  }
}

O alert ficará assim.

party-highlight

Conclusão

A biblioteca Charts é uma boa opção para gráficos no iOS. Ela facilita muito o trabalho e possui bons recursos. Como ponto negativo temos a falta de documentação e a quantidade de tipos de gráficos diferentes. Mas o fonte é bem comentado e você pode suprir a falta de documentação analisando o fonte ou vendo a documentação da biblioteca MPAndroidCharts.

Espero que tenham gostado do artigo. Qualquer dúvida, crítica ou sugestão de novos assuntos deixe um comentário abaixo. Caso queira entrar em contato pode me chamar no twitter @mateusfsilva. Se gostou compartilhe.

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s