Статьи

Работа с CorePlot: создание гистограммы

При работе с приложениями, интенсивно использующими данные, разработчик часто должен делать больше, чем просто показывать списки записей данных в табличном представлении. Библиотека CorePlot позволит вам добавить потрясающие визуализации данных в ваши приложения. Узнайте, как в этой серии Tuts + Premium!


  1. Работа с CorePlot: настройка проекта
  2. Работа с CorePlot: основы построения
  3. Работа с CorePlot: стилизация и добавление графиков
  4. Работа с CorePlot: создание гистограммы
  5. Работа с CorePlot: создание круговой диаграммы

В прошлый раз мы рассмотрели, как настроить внешний вид и стиль линейного графика (или точечного графика), используя такие классы, как CPTMutableTextStyle и CPTMutableLineStyle. Мы узнали, как настраивать приращения осей X и Y и стили чисел, используя классы CPTXYAxisSet и CPTXYAxis. Мы также рассмотрели, как добавить несколько графиков на график и изменить методы источника данных, чтобы обеспечить правильные данные для правильных графиков с использованием идентификаторов графиков.


Сегодня мы будем работать с совершенно новым графиком. Мы будем создавать гистограмму, которая показывает общее количество студентов по каждому предмету, где каждый столбец представляет предмет. Мы также рассмотрим, как настроить внешний вид графика. Давайте начнем!


Сначала нам нужно добавить соответствующие классы в наш проект. Давайте создадим ViewController с именем «STBarGraphViewController» и «STGraphView». (Убедитесь, что вы поместили их в соответствующие группы)


Обратите внимание на присвоение имени представлению STGraphView вместо STBarGraphView. В дальнейшем мы будем использовать этот вид для отображения гистограмм и круговых диаграмм.

Прежде чем мы начнем работать с каким-либо кодом, нам нужно добавить кнопку на лист действий в нашем списке студентов. Сначала откройте «STStudentListViewController.h» и импортируйте STBarGraphViewController. Добавьте STBarGraphViewControllerDelegate в список зарегистрированных протоколов (протокол на самом деле еще не существует, но мы создадим его позже):

1
@interface STStudentListViewController : UIViewController <UITableViewDelegate, UITableViewDataSource, AddStudentViewControllerDelegate, UIActionSheetDelegate, STLineGraphViewControllerDelegate, STBarGraphViewControllerDelegate>

Затем перейдите в файл .m и найдите метод «graphButtonWasSelected:». Добавьте «Зачисление по теме» в список «otherButtonTitles:»:

1
UIActionSheet *graphSelectionActionSheet = [[[UIActionSheet alloc] initWithTitle:@»Choose a graph» delegate:self cancelButtonTitle:@»Cancel» destructiveButtonTitle:nil otherButtonTitles:@»Enrolment over time», @»Enrolment by subject», nil] autorelease];

Теперь найдите метод ‘actionSheet: clickedButtonAtIndex:’ и измените его для работы с buttonIndex == 1:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if (buttonIndex == 0)
{
    STLineGraphViewController *graphVC = [[STLineGraphViewController alloc] init];
    [graphVC setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
    [graphVC setModalPresentationStyle:UIModalPresentationFullScreen];
    [graphVC setDelegate:self];
    [graphVC setManagedObjectContext:[self managedObjectContext]];
          
    [self presentModalViewController:graphVC animated:YES];
          
    [graphVC release];
  
}
else if (buttonIndex == 1)
{
    STBarGraphViewController *graphVC = [[STBarGraphViewController alloc] init];
    [graphVC setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
    [graphVC setModalPresentationStyle:UIModalPresentationFullScreen];
    [graphVC setDelegate:self];
    [graphVC setManagedObjectContext:[self managedObjectContext]];
          
    [self presentModalViewController:graphVC animated:YES];
          
    [graphVC release];
}

Опять же, это покажет некоторые предупреждения, потому что мы еще не реализовали свойства делегата или managedObjectContext в STBarGraphViewController.

Теперь перейдите в STBarGraphViewController.h. Импортируйте CorePlot-CocoaTouch.h и добавьте следующее объявление свойства:

1
2
3
4
5
@protocol STBarGraphViewControllerDelegate
@required
— (void)doneButtonWasTapped:(id)sender;
  
@end

Теперь добавьте следующие свойства:

1
2
3
@property (nonatomic, strong) CPTGraph *graph;
@property (nonatomic, assign) id<STBarGraphViewControllerDelegate> delegate;
@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;

Наконец, зарегистрируйте, что этот класс будет следовать этим протоколам:

1
@interface STBarGraphViewController : UIViewController <CPTBarPlotDelegate, CPTBarPlotDataSource>

Обратите внимание, что, помимо соответствия различным протоколам, этот класс является тем же, что и STLineViewController. В идеале, у вас должен быть базовый класс, который будет иметь эти свойства, которые вы бы делили на подклассы для уменьшения повторения кода. Этот учебник фокусируется только на основном графике, поэтому мы сосредоточены только на том, как лучше всего работать с платформой CorePlot. Если у вас есть знания и время, не стесняйтесь создавать базовый класс и использовать наследование, но мы собираемся упростить это в примере кода здесь.

Затем перейдите в файл .m, синтезируйте свойства и освободите их в методе dealloc.

Теперь давайте свяжем класс представления как представление контроллера. Импортируйте STBarGraphView.h и добавьте следующий метод:

01
02
03
04
05
06
07
08
09
10
11
— (void)loadView
{
    [super loadView];
      
    [self setTitle:@»Enrolment by subject»];
    [self setView:[[[STGraphView alloc] initWithFrame:self.view.frame] autorelease]];
      
    CPTTheme *defaultTheme = [CPTTheme themeNamed:kCPTPlainWhiteTheme];
      
    [self setGraph:(CPTGraph *)[defaultTheme newGraph]];
}

Теперь мы готовы работать с видом. Откройте STGraphView.h, импортируйте базовый каркас графика (CorePlot-CocoaTouch.h) и добавьте следующее объявление свойства:

1
@property (nonatomic, strong) CPTGraphHostingView *chartHostingView;

Перейдите в .m, синтезируйте свойство и освободите свойство в методе dealloc. Затем создайте CPTGraphHostingView в методе initWithFrame:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
— (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
          
        [self setChartHostingView:[[[CPTGraphHostingView alloc] initWithFrame:CGRectZero] autorelease]];
        [chartHostingView setBackgroundColor:[UIColor purpleColor]];
          
        [self addSubview:chartHostingView];
          
    }
    return self;
}

Наконец, создайте метод layoutSubviews с помощью следующего кода:

01
02
03
04
05
06
07
08
09
10
— (void)layoutSubviews
{
    [super layoutSubviews];
      
    float chartHeight = self.frame.size.height;
    float chartWidth = self.frame.size.width;
      
    [[self chartHostingView] setFrame:CGRectMake(0, 0, chartWidth, chartHeight)];
    [[self chartHostingView] setCenter:[self center]];
}

Вы заметите, что код, используемый для создания этого представления, точно такой же, как и STLineGraphView. Мы можем использовать этот базовый вид для работы со всеми графическими представлениями в будущем.

Вернитесь в представление «STBarGraphViewController.m» и найдите метод «viewDidLoad». Сначала мы хотим создать CPTBarPlot и добавить его в наш график:

1
2
3
4
5
6
7
8
9
[[graphView chartHostingView] setHostedGraph:[self graph]];
  
CPTBarPlot *subjectBarPlot = [[CPTBarPlot alloc] initWithFrame:[graph bounds]];
[subjectBarPlot setIdentifier:@»subjectEnrollement»];
[subjectBarPlot setDelegate:self];
[subjectBarPlot setDataSource:self];
  
[[self graph] addPlot:subjectBarPlot];

Наконец, давайте добавим панель навигации с кнопкой «Готово», чтобы пользователь мог вернуться:

1
2
3
4
5
6
7
8
9
UINavigationItem *navigationItem = [[[UINavigationItem alloc] initWithTitle:self.title] autorelease];
[navigationItem setHidesBackButton:YES];
      
UINavigationBar *navigationBar = [[[UINavigationBar alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 44.0f)] autorelease];
[navigationBar pushNavigationItem:navigationItem animated:NO];
      
[self.view addSubview:navigationBar];
      
[navigationItem setRightBarButtonItem:[[[UIBarButtonItem alloc] initWithTitle:@»Done» style:UIBarButtonItemStyleDone target:[self delegate] action:@selector(doneButtonWasTapped:)] autorelease] animated:NO];

Теперь давайте начнем работать с методами источника данных:


Процесс будет похож на то, как мы создали линейный график, но мы сделаем вещи немного более динамичными. Мы будем создавать собственные методы, которые обеспечат соответствующее пространство для сюжета. Мы также добавим несколько дополнительных методов источника данных, чтобы обеспечить определенный цвет для каждой полосы, а также заголовки по оси X.

Прежде чем мы рассмотрим предоставление данных графика, нам нужно настроить ось и видимые диапазоны. Для этого нам понадобятся два метода, которые вычисляют максимальное значение x и максимальное значение y. Объявите следующие методы в верхней части файла .m:

1
2
3
4
5
6
7
8
[[graphView chartHostingView] setHostedGraph:[self graph]];
  
@interface STBarGraphViewController ()
— (float)getTotalSubjects;
— (float)getMaxEnrolled;
  
@end

Теперь реализуйте их, как показано ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#pragma mark — Private Methods
— (float)getTotalSubjects
{
    NSError *error = nil;
    NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@»STSubject» inManagedObjectContext:managedObjectContext];
    [fetchRequest setEntity:entity];
      
    return [managedObjectContext countForFetchRequest:fetchRequest error:&error];
}
  
— (float)getMaxEnrolled
{
    float maxEnrolled = 0;
      
    NSError *error = nil;
      
    for (int i = 0; i < [self getTotalSubjects]; i++)
    {
        NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
        NSEntityDescription *entity = [NSEntityDescription entityForName:@»STStudent» inManagedObjectContext:managedObjectContext];
          
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@»subjectID == %d», i];
        [fetchRequest setEntity:entity];
        [fetchRequest setPredicate:predicate];
          
        float subjectMax = [managedObjectContext countForFetchRequest:fetchRequest error:&error];
        NSLog(@»enrolled for Subject %d is %f», i, subjectMax);
        [fetchRequest release];
          
        if (subjectMax > maxEnrolled)
        {
            maxEnrolled = subjectMax;
        }
    }
      
    return maxEnrolled;
}

‘getTotalSubjects’ просто получает количество всех предметов в основном хранилище данных. ‘getMaxEnrolled’ просматривает все предметы и ищет наибольшее количество студентов в каждом из них и возвращает наибольшее значение.

С этими методами, мы можем вернуться к нашему методу viewDidLoad и добавить следующий код ниже, где мы добавляем гистограмму к нашему графику:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
CPTXYPlotSpace *studentPlotSpace = (CPTXYPlotSpace *)[graph defaultPlotSpace];
[studentPlotSpace setXRange:[CPTPlotRange plotRangeWithLocation:CPTDecimalFromInt(0) length:CPTDecimalFromInt([self getTotalSubjects] + 1)]];
[studentPlotSpace setYRange:[CPTPlotRange plotRangeWithLocation:CPTDecimalFromInt(0) length:CPTDecimalFromInt([self getMaxEnrolled] + 1)]];
      
[[graph plotAreaFrame] setPaddingLeft:40.0f];
[[graph plotAreaFrame] setPaddingTop:10.0f];
[[graph plotAreaFrame] setPaddingBottom:120.0f];
[[graph plotAreaFrame] setPaddingRight:0.0f];
[[graph plotAreaFrame] setBorderLineStyle:nil];
      
CPTMutableTextStyle *textStyle = [CPTMutableTextStyle textStyle];
[textStyle setFontSize:12.0f];
[textStyle setColor:[CPTColor colorWithCGColor:[[UIColor grayColor] CGColor]]];
      
CPTXYAxisSet *axisSet = (CPTXYAxisSet *)[graph axisSet];
      
CPTXYAxis *xAxis = [axisSet xAxis];
[xAxis setMajorIntervalLength:CPTDecimalFromInt(1)];
[xAxis setMinorTickLineStyle:nil];
[xAxis setLabelingPolicy:CPTAxisLabelingPolicyFixedInterval];
[xAxis setLabelTextStyle:textStyle];
      
CPTXYAxis *yAxis = [axisSet yAxis];
[yAxis setMajorIntervalLength:CPTDecimalFromInt(1)];
[yAxis setMinorTickLineStyle:nil];
[yAxis setLabelingPolicy:CPTAxisLabelingPolicyFixedInterval];

Большинство из вышеперечисленного должно быть знакомо с прошлого раза, однако вместо установки жестко закодированных значений для нашей максимальной длины диапазона графика x и y, мы используем наши новые методы для динамического создания значений. После этого мы просто устанавливаем базовое форматирование оси и добавляем рамку графика, чтобы ось отображалась соответствующим образом.

Теперь нам нужно снабдить график данными, используя методы источника данных. Предоставить количество записей просто:

1
2
3
4
5
#pragma mark — CPTBarPlotDataSourceMethods
— (NSUInteger)numberOfRecordsForPlot:(CPTPlot *)plot
{
    return [self getTotalSubjects];
}

Теперь предоставим значения x и y для записей:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
— (NSNumber *)numberForPlot:(CPTPlot *)plot field:(NSUInteger)fieldEnum recordIndex:(NSUInteger)index
{
    int x = index + 1;
    int y = 0;
      
    NSError *error;
      
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@»STStudent» inManagedObjectContext:managedObjectContext];
      
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@»subjectID == %d», index];
      
    [fetchRequest setEntity:entity];
    [fetchRequest setPredicate:predicate];
      
    y = [managedObjectContext countForFetchRequest:fetchRequest error:&error];
      
    [fetchRequest release];
      
    switch (fieldEnum)
    {
        case CPTScatterPlotFieldX:
            return [NSNumber numberWithInt:x];
            break;
        case CPTScatterPlotFieldY:
            return [NSNumber numberWithInt:y];
            break;
              
        default:
            break;
    }
      
    return nil;
}

Вышеуказанный метод очень похож на то, как мы предоставляем данные для точечной диаграммы. Мы меняем вызов в хранилище данных, чтобы получить счет всех студентов, зачисленных по предмету. Мы также устанавливаем значение х +1. Это значит, что полоса начинается с «1» и обеспечивает небольшое отступление между первой полосой и линией оси Y.

Сохраните и запустите проект … вы должны увидеть свою гистограмму!



Есть еще кое-что, что мы можем сделать, чтобы на это было проще смотреть. Мы хотим присвоить каждому столбцу свой цвет и указать имя субъекта в виде метки оси X вместо числа. Мы можем сделать первое с помощью метода CPTBarPlotDataSource с именем ‘barFillForBarPlot: recordIndex’:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
-(CPTFill *)barFillForBarPlot:(CPTBarPlot *)barPlot recordIndex:(NSUInteger)index
{
    CPTColor *areaColor = nil;
      
    switch (index)
    {
        case 0:
            areaColor = [CPTColor redColor];
            break;
              
        case 1:
            areaColor = [CPTColor blueColor];
            break;
          
        case 2:
            areaColor = [CPTColor orangeColor];
            break;
              
        case 3:
            areaColor = [CPTColor greenColor];
            break;
              
        default:
            areaColor = [CPTColor purpleColor];
            break;
    }
      
    CPTFill *barFill = [CPTFill fillWithColor:areaColor];
      
    return barFill;
}

Это вернет разный цвет для каждого столбца на нашем графике. Если вы хотите сделать его еще более стильным, вы можете сделать его градиентом. Это также часть нашего графика, которая не полностью динамична, потому что, если кто-то добавляет новый предмет, он будет использовать цвет по умолчанию. Возможно, способ обойти это в реальном приложении будет иметь цвет при создании нового объекта, который хранится в хранилище данных


Наконец, давайте добавим несколько пользовательских заголовков оси X. Для этого нам нужно работать с осью X. Найдите, где мы устанавливаем ось X и измените код, чтобы сделать следующее:

01
02
03
04
05
06
07
08
09
10
CPTXYAxis *xAxis = [axisSet xAxis];
[xAxis setMajorIntervalLength:CPTDecimalFromInt(1)];
[xAxis setMinorTickLineStyle:nil];
[xAxis setLabelingPolicy:CPTAxisLabelingPolicyNone];
[xAxis setLabelTextStyle:textStyle];
[xAxis setLabelRotation:M_PI/4];
      
NSArray *subjectsArray = [self getSubjectTitlesAsArray];
      
[xAxis setAxisLabels:[NSSet setWithArray:subjectsArray]];

В приведенном выше коде есть несколько изменений. Во-первых, мы меняем политику маркировки на «нет». Это гарантирует, что CorePlot не распечатывает саму этикетку и вместо этого будет использовать то, что мы ей даем. Далее мы устанавливаем поворот метки так, чтобы он лучше соответствовал графику. Наконец, мы устанавливаем свойство «Метки оси», которое принимает NSSet значений NSString. Мы создаем NSSet, используя NSArray, созданный методом ‘getSubjectTitlesAsArray’. Этот метод еще не существует, поэтому давайте его создадим. Добавьте объявление в начало файла .m, а затем напишите следующую реализацию:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
— (NSArray *)getSubjectTitlesAsArray
{
    NSError *error = nil;
      
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@»subjectID» ascending:YES];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@»STSubject» inManagedObjectContext:managedObjectContext];
    [request setEntity:entity];
    [request setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
    [request setResultType:NSDictionaryResultType];
    [request setReturnsDistinctResults:NO];
    [request setPropertiesToFetch :[NSArray arrayWithObject:@»subjectName»]];
      
    NSArray *titleStrings = [managedObjectContext executeFetchRequest:request error:&error];
    NSMutableArray *labelArray = [NSMutableArray array];
      
    CPTMutableTextStyle *textStyle = [CPTMutableTextStyle textStyle];
    [textStyle setFontSize:10];
      
    for (int i = 0; i < [titleStrings count]; i++)
    {
        NSDictionary *dict = [titleStrings objectAtIndex:i];
          
        CPTAxisLabel *axisLabel = [[CPTAxisLabel alloc] initWithText:[dict objectForKey:@»subjectName»] textStyle:textStyle];
        [axisLabel setTickLocation:CPTDecimalFromInt(i + 1)];
        [axisLabel setRotation:M_PI/4];
        [axisLabel setOffset:0.1];
        [labelArray addObject:axisLabel];
        [axisLabel release];
    }
      
    return [NSArray arrayWithArray:labelArray];
}

В вышеприведенном коде много чего происходит. Чтобы предоставить графу пользовательские метки, нам нужно передать NSSet, содержащий объекты типа «CPTAxisLabel». Сначала мы получаем массив всех имен субъектов, упорядоченных по subjectID, поэтому он будет в том же порядке, что и график. Затем, для количества имен, которые мы возвращаем, мы проходим цикл и создаем CPTAxisLabel со строкой имени субъекта и предопределенным стилем текста. «Тиковое местоположение» — это отметка, для которой оно будет отображаться. Нам нужно добавить 1 к нашему значению, потому что мы начинаем наш первый бар с 1 вместо 0. Затем мы устанавливаем поворот и смещение и добавляем его в массив. Наконец, мы возвращаем массив axisLabels.

Если мы сохраним и запустим проект, у нас будет законченная, красочная гистограмма с пользовательскими метками!



Мы узнали много нового о CorePlot на этой сессии. Мы узнали, как создавать гистограмму, изменять цвета панели и даже добавлять собственные метки к оси.

В следующий раз мы расскажем, как сделать потрясающую круговую диаграмму, которая показывает те же данные, что и гистограмма. Увидимся в следующий раз!