3

I am using charts library (Charts)

I am developing the application allows me to show number of guests in restaurant realtime and compare data between different days. For example look at this picture enter image description here

The dashed line means data for the compared to date. I would like to create BarChart like this but library only allows me to show 4 grouped bars. I want to show dashed bars above colored with small offset. Help me out please

enter image description here

My code is:

for (int i = 0; i < days.count; i++) {
        BarChartDataEntry *guysEntry = [[BarChartDataEntry alloc] initWithX:i y:[guys[i] integerValue]];
        [guysChartDataArray addObject:guysEntry];

        BarChartDataEntry *girlsEntry = [[BarChartDataEntry alloc] initWithX:i y:[girls[i] integerValue]];
        [girlsChartDataArray addObject:girlsEntry];

        BarChartDataEntry *guysCompareToEntry = [[BarChartDataEntry alloc] initWithX:i y:[guysCompareTo[i] integerValue]];
        [guysCompareToChartDataArray addObject:guysCompareToEntry];

        BarChartDataEntry *girlsCompareToEntry = [[BarChartDataEntry alloc] initWithX:i y:[girlsCompareTo[i] integerValue]];
        [girlsCompareToChartDataArray addObject:girlsCompareToEntry];
    }
    BarChartDataSet *guysChartDataSet = [[BarChartDataSet alloc] initWithValues:guysChartDataArray label:@"Guys"];
    guysChartDataSet.colors = @[[UIColor maleColor]];
    guysChartDataSet.valueTextColor = [UIColor clearColor];

    BarChartDataSet *girlsChartDataSet = [[BarChartDataSet alloc] initWithValues:girlsChartDataArray label:@"Girls"];
    girlsChartDataSet.colors = @[[UIColor femaleColor]];
    girlsChartDataSet.valueTextColor = [UIColor clearColor];

    LineChartXAxisFormatter *barGraphXFormatter = [[LineChartXAxisFormatter alloc] init];
    barGraphXFormatter.xLabels = [days mutableCopy];
    self.barChartView.xAxis.valueFormatter = barGraphXFormatter;
    self.barChartView.xAxis.centerAxisLabelsEnabled = YES;

    self.combinedChartView.xAxis.valueFormatter = barGraphXFormatter;
    self.combinedChartView.xAxis.centerAxisLabelsEnabled = YES;

    float groupSpace = 0.06f;
    float barSpace = 0.02f;
    float barWidth = 0.45f;

    BarChartDataSet *guysCompareToChartDataSet = [[BarChartDataSet alloc] initWithValues:guysCompareToChartDataArray label:@"Guys (Compare)"];
    guysCompareToChartDataSet.colors = @[[UIColor clearColor]];
    guysCompareToChartDataSet.barBorderWidth = 1.f;
    guysCompareToChartDataSet.barBorderColor = [UIColor grayColor];
    guysCompareToChartDataSet.isDashedBorder = YES;

    guysCompareToChartDataSet.axisDependency = AxisDependencyLeft;
    guysCompareToChartDataSet.valueTextColor = [UIColor clearColor];

    BarChartDataSet *girlsCompareToChartDataSet = [[BarChartDataSet alloc] initWithValues:girlsCompareToChartDataArray label:@"Girls (Compare)"];
    girlsCompareToChartDataSet.colors = @[[UIColor clearColor]];
    girlsCompareToChartDataSet.barBorderWidth = 1.f;
    girlsCompareToChartDataSet.barBorderColor = [UIColor grayColor];
    girlsCompareToChartDataSet.isDashedBorder = YES;
    girlsCompareToChartDataSet.axisDependency = AxisDependencyLeft;
    girlsCompareToChartDataSet.valueTextColor = [UIColor clearColor];

    NSArray *dataSets = @[guysChartDataSet, girlsChartDataSet, guysCompareToChartDataSet, girlsCompareToChartDataSet];

    BarChartData *barChartData = [[BarChartData alloc] initWithDataSets:dataSets];
    barChartData.barWidth = barWidth;

    CGFloat initialValue = 0;
    CGFloat groupCount = days.count;

    self.barChartView.xAxis.axisMinimum = initialValue;

    self.barChartView.xAxis.axisMaximum = initialValue + [barChartData groupWidthWithGroupSpace:groupSpace barSpace: barSpace] * groupCount;

    [barChartData groupBarsFromX:0 groupSpace:groupSpace barSpace:barSpace];
    self.barChartView.data = barChartData;

I want to make something like: enter image description here

Artem Z.
  • 1,243
  • 2
  • 14
  • 36

2 Answers2

1

With Charts, it looks you can:

  1. Stack data into the same bar.
  2. Group bars so that they overlap.

It also looks like you can't:

  1. Give each data entry in the stacked bar a separate border color. (If you supply a border color, it will apply to the entire bar.)

Note about documentation

Do remember that Charts is modeled after MPAndroidChart and follows its API very closely.

Therefore, if you need help, refer to their documentation. The Java syntax is a bit foreign to me, but with the help of Xcode's autocomplete, I was able to find everything I needed.

Code:

Please take note of three specific parts:

  1. How to create stacked data entries.
  2. Inability to set separate borders per item in stacked bar.
  3. How to overlap bars by providing a negative bar spacing.

I'm by no means an expert in this library, but simply reading the documentation, I was able to put this together.

// MARK: Data Entries

for (int i = 0; i < days.count; i++) {
    NSNumber *guyValue = guys[i];
    NSNumber *girlValue = girls[i];


    // NOTE 1: To get "stacked" bars, use the initializer `initWithX:yValues:`
    BarChartDataEntry *guyGirlDataEntry = [[BarChartDataEntry alloc] initWithX:i
                                                                       yValues:@[guyValue, girlValue]];


    NSNumber *guyCompareToValue = guysCompareTo[i];
    NSNumber *girlCompareToValue = girlsCompareTo[i];

    BarChartDataEntry *guyGirlCompareToEntry = [[BarChartDataEntry alloc] initWithX:i
                                                                            yValues:@[guyCompareToValue, girlCompareToValue]];

    [guyGirlChartDataArray addObject:guyGirlDataEntry];
    [guyGirlCompareToChartDataArray addObject:guyGirlCompareToEntry];
}



// MARK: Data Sets

BarChartDataSet *guysGirlsChartDataSet = [[BarChartDataSet alloc] initWithValues:guyGirlChartDataArray label:nil];
BarChartDataSet *guysGirlsCompareToChartDataSet = [[BarChartDataSet alloc] initWithValues:guyGirlCompareToChartDataArray label:nil];

BarChartData *data = [[BarChartData alloc] initWithDataSets:@[guysGirlsCompareToChartDataSet, guysGirlsChartDataSet]];



// MARK: Styling

guysGirlsChartDataSet.stackLabels = @[@"Guys", @"Girls"];
guysGirlsChartDataSet.colors = @[[UIColor maleColor],
                                 [UIColor femaleColor]];

// NOTE 2: Unfortunately, you can only set one border color to the bar.
// It seems, you won't be able to use separate dashed borders in the same bar, like you want.
// For demonstration purposes, I've simply made the comparison colors 50% transparent.
guysGirlsCompareToChartDataSet.stackLabels = @[@"Guys (Compare)", @"Girls (Compare)"];
guysGirlsCompareToChartDataSet.colors = @[[[UIColor maleColor] colorWithAlphaComponent:0.5],
                                          [[UIColor femaleColor] colorWithAlphaComponent:0.5]];

// Sets the x axis label interval, so it doesn't show labels like "0.9"
barChartView.xAxis.granularity = 1;

// Centers the x axis label over the bar group, rather than on the grid line.
barChartView.xAxis.centerAxisLabelsEnabled = YES;



// MARK: Displaying Chart Data

barChartView.data = data;

// Grouping

// I still can't seem to set this properly.
// Even though I tell it to fit the bars exactly and don't show below 0 on the x axis,
// the chart still shows below zero and the right-most bar gets cut off.

// If this isn't a bug, then perhaps you can find the answer to this elsewhere.
barChartView.fitBars = YES;
[barChartView setVisibleXRangeMinimum:0];

// Calculate bar grouping parameters.
NSInteger barCountPerGroup = data.dataSetCount;
double barWidth = 0.4;

// NOTE 3: Negative bar spacing will make the bars overlap.
double barSpace = -0.3;

// According to documentation, total group width (bars and spacing) must add up to 1
double groupSpace = 1 - (barWidth + barSpace) * barCountPerGroup;

// Set the grouping parameters.
data.barWidth = barWidth;
[barChartView groupBarsFromX:0 groupSpace:groupSpace barSpace:barSpace];

// Refresh the chart (if necessary)
[barChartView notifyDataSetChanged];

Output:

The styling isn't very good, but the bars are grouped and overlap like you want.

I trust you can find help with the styling elsewhere.

chart with overlapping bars, resulting from sample code

ABeard89
  • 911
  • 9
  • 17
  • Thanks! I'll take a look shortly. Will accept the answer if it works – Artem Z. Jun 18 '18 at 11:56
  • @ArtemZ. Again, sorry about the styling and the x axis. Hopefully you can get that looking better than I did. But at least the grouping should work for you. – ABeard89 Jun 18 '18 at 11:59
  • Is it possible to not combine girls and guys in one Bar? just make the same like I did? – Artem Z. Jun 18 '18 at 12:02
  • @ArtemZ. I was going off your first image where you have stacked bars. Is that not what you wanted? – ABeard89 Jun 18 '18 at 12:03
  • @ArtemZ. It seemed like stacked bars were your goal because of your first image. – ABeard89 Jun 18 '18 at 12:04
  • I've updated the question (added one screenshot at last) – Artem Z. Jun 18 '18 at 12:08
  • @ArtemZ. That changes the whole question. (First graphic is very misleading) Yeah you can do something like that. But you’ll have to position the bars yourself. You won’t be able to use the auto grouping feature. – ABeard89 Jun 18 '18 at 12:11
  • @ArtemZ. Basically, you’re gonna want to use the same `BarChartDataEntry` initializer that you are currently using and position each bar’s x value to the point on the axis that you want. That x value will change depending on the bar width you set. – ABeard89 Jun 18 '18 at 12:15
  • @ArtemZ. Sorry I just got off of work, and this source is on my work computer. I won’t be able to rework this until tomorrow morning. (Currently night here) But like I said, if you look at the documentation for `MPAndroidChart`, you should be able to figure it out. – ABeard89 Jun 18 '18 at 12:17
  • Could you help me to rework if possible? I'll be waiting for – Artem Z. Jun 18 '18 at 12:24
  • @ArtemZ. Sure, I'll give it another shot, but I won't be able to until tomorrow. In the meantime, I would suggest you give it a shot. Check out the documentation, and try a few things out. – ABeard89 Jun 18 '18 at 12:33
  • I'll take a look – Artem Z. Jun 18 '18 at 12:42
  • Thanks in advance – Artem Z. Jun 18 '18 at 12:43
  • @ArtemZ. Good luck. I'll get back to you when I can. – ABeard89 Jun 18 '18 at 12:59
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/173365/discussion-between-abeard89-and-artem-z). – ABeard89 Jun 19 '18 at 01:30
1

Now that you've changed your question, this requires a different solution.

As I mentioned in the comments before, you need to manually calculate each bar's position. Then, don't use the grouping feature, because you've already grouped them how you want.

// Determine bar sizing and spacing parameters.
int barsPerGroup = 2;
double targetGroupWidth = 1;
double barSpacing = 0.2;
double groupSpacing = 0.3;
double barWidth = (targetGroupWidth - groupSpacing - barSpacing * barsPerGroup) / barsPerGroup;
double compareBarOffset = barWidth / 3;

for (int i = 0; i < entryCount; i++) {

    // Determine X position for each bar
    // NOTE: This is the most important step!
    double groupStartPosition = targetGroupWidth * i;

    double group1X = groupStartPosition + barWidth / 2;
    double group1CompareX = group1X + compareBarOffset;

    double group2X = group1X + barWidth + barSpacing;
    double group2CompareX = group2X + compareBarOffset;


    // Create data entries positioned at values calculated by previous step
    NSNumber *group1Value = group1[i];
    NSNumber *group1CompareValue = group1Compare[i];

    NSNumber *group2Value = group2[i];
    NSNumber *group2CompareValue = group2Compare[i];


    BarChartDataEntry *group1DataEntry = [[BarChartDataEntry alloc] initWithX:group1X
                                                                            y:[group1Value doubleValue]];
    BarChartDataEntry *group1CompareDataEntry = [[BarChartDataEntry alloc] initWithX:group1CompareX
                                                                                   y:[group1CompareValue doubleValue]];

    BarChartDataEntry *group2DataEntry = [[BarChartDataEntry alloc] initWithX:group2X
                                                                            y:[group2Value doubleValue]];
    BarChartDataEntry *group2CompareEntry = [[BarChartDataEntry alloc] initWithX:group2CompareX
                                                                               y:[group2CompareValue doubleValue]];


    // ...
}

// Create Data Sets, set styles, etc.
// ...

// Do NOT use this method because bars are already grouped.
//[barChartView groupBarsFromX:0 groupSpace:groupSpacing barSpace:barSpacing];

RESULT:

Note: I can't find the property isDashedBorder, so I don't know what version of the library you're using. Instead, I just set clear bars with a solid gray border.

Chart with orange and blue bars with overlapping bars for comparison

ABeard89
  • 911
  • 9
  • 17
  • Sorry for late corresponding. Thank you, but this solution doesn't work. It shows me https://puu.sh/AICI9/b5b8501183.png – Artem Z. Jun 20 '18 at 09:10
  • That looks like all your data entries have the same x value. Make sure that 1) each data entry has a different x value and 2) you're ***not*** using the automatic grouping function `groupBarsFromX:groupSpace:barSpace`. – ABeard89 Jun 20 '18 at 09:14
  • Really thank you! Helped a lot! Can you give me advice how to increase width of each bar? Sry for late corresponding – Artem Z. Jun 26 '18 at 05:53
  • 1
    @ArtemZ. Glad that helped. Bar width is set on the `BarChartData` object: `data.barWidth = barWidth;` Change the parameters at the top to whatever you need. Try lowering the `barSpacing` or the `groupSpacing` parameters. Or you could change the calculation completely (i.e. set `barWidth` and calculate one of the other values instead.) – ABeard89 Jul 02 '18 at 08:15