Multiple ShinobiCharts in UIScrollView with vertical scrolling


#1

Hi guys, 

I’d like to add multiple charts to a UIScrollView with vertical scrolling. Panning on the y axis on all charts have been disabled, horizontal panning on the charts work as usual. However, once the user’s pan gesture goes vertically, the UIScrollView starts to scroll, the panning on the charts stop.

In order to prevent this from happening, I figured I’d just add a pan gesture recognizer to the chart(s) and disable scrolling of the ScrollView in case the charts are being panned horizontally.

UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(mainChartPanGesture:)];
[mainChart addGestureRecognizer:panGestureRecognizer];
 
Unfortunately, as soon as I add this recognizer to the chart, the UIScrollView won't scroll any more. It seems as if the charts block the UIScrollView completely.
 
Simultaneous gesture recognition is enabled by the way;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
     returnYES;
 
}
 
Do you guys have an idea how to solve this? I've been pulling my hair out all day already, but nothing comes to mind.
 
 
Edit: 
Updated question on stackoverflow: http://stackoverflow.com/questions/27060966/uiscrollview-with-touch-interceptor-for-contentsize-to-control-subviews
I'm not entirely sure if this is a general UIKit problem, or if the gesture recognizsers of the ShinobiControls do form a special case, hence my updated question at stackoverflow.
 
Cheers,
Max

#2

Still couln’t figure it out yet. So if anyone as a clue, please shoot!  :blush:

For the record, based on the answer on StackOverflow, I’m now using a UIView on top of the ScrollView containing the sepearate ShinobiCharts. This UIView should take care of all the gestures that will get forwarded to the main chart placed in the ScrollView.

Unfortunately, the ShinobiChart won’t pick up any gestures. I subclassed ShinobiChart and added separate gesture recognizers for panning and pinching and they do in fact get called. The chart itself doens’t do anything though. 

Is there something that might block the chart from receiving the appropriate events?

If you guys need a demo project that demonstrates this behaviour, please let me know!


#3

Greetings Program!

I hope I’m replicating what you’re trying to do; in the GettingStarted sample app I added a scrollview and two charts to it.

I found that if you scroll the scrollView, you must let it completely stop scrolling before you can pan horizontally. Also, in the simulator, if you’re panning and the mouse moves out of the simulator and back in, it loses the panning gesture and it will scroll the scrollView.

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
    scrollView.scrollEnabled = YES;
    scrollView.contentSize = CGSizeMake(650, 1200);
    
    [self.view addSubview:scrollView];
    
    // Create the chart
    self.view.backgroundColor = [UIColor whiteColor];
    
    //CGFloat margin = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ? 10.0 : 50.0;
    ShinobiChart *chart = [[ShinobiChart alloc] initWithFrame:CGRectMake(60, 0, 650, 600)];
    chart.title = @"Trigonometric Functions";
    chart.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    
    chart.licenseKey = @""; // TODO: add your trial licence key here!
    
    // add a pair of axes
    SChartNumberAxis *xAxis = [SChartNumberAxis new];
    xAxis.title = @"X Value";
    chart.xAxis = xAxis;
    
    SChartNumberAxis *yAxis = [SChartNumberAxis new];
    yAxis.title = @"Y Value";
    yAxis.rangePaddingLow = @(0.1);
    yAxis.rangePaddingHigh = @(0.1);
    chart.yAxis = yAxis;
    
    // enable gestures
    yAxis.enableGesturePanning = NO;
    yAxis.enableGestureZooming = YES;
    xAxis.enableGesturePanning = YES;
    xAxis.enableGestureZooming = YES;
    
    // add to the view
    [scrollView addSubview:chart];
    
    chart.datasource = self;
    
    chart.legend.hidden = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone;


    ShinobiChart *chart2 = [[ShinobiChart alloc] initWithFrame:CGRectMake(60, 600, 650, 600)];
    chart2.title = @"Trigonometric Functions";
    chart2.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    
    chart2.licenseKey = @""; // TODO: add your trial licence key here!
    
    // add a pair of axes
    SChartNumberAxis *xAxis2 = [SChartNumberAxis new];
    xAxis2.title = @"X Value";
    chart2.xAxis = xAxis2;
    
    SChartNumberAxis *yAxis2 = [SChartNumberAxis new];
    yAxis2.title = @"Y Value";
    yAxis2.rangePaddingLow = @(0.1);
    yAxis2.rangePaddingHigh = @(0.1);
    chart2.yAxis = yAxis2;
    
    // enable gestures
    yAxis2.enableGesturePanning = NO;
    yAxis2.enableGestureZooming = YES;
    xAxis2.enableGesturePanning = YES;
    xAxis2.enableGestureZooming = YES;
    
    // add to the view
    [scrollView addSubview:chart2];
    
    chart2.datasource = self;
    
    chart2.legend.hidden = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone;
}

Wg


#4

What I’m trying to achieve is actually a little different than what you’ve posted.

Here is the screenshot from my question on Stackoverflow again. 

Yes, it looks exactly like the code you’ve shown above. The difference is, that I need to synchronise all charts on user interaction. For that to work I need a blank canvas the same size as the ScrollView that detects pan/pinch/tap/longpress and so on. That way, the user only interacts with a single canvas and doesn’t need to touch exactly within the frame of a ShinobiChart. I hope you get what I’m trying to achieve here.

Take a chart like the following;

Remove the range selector at the bottom (that will be placed outside of the ScrollView) and what you’ve got there are two charts, the main line chart and the volume indicator at the bottom. In order to pan/zoom both charts simultaneously, you’d have to link them by code and add gestures recognizers to each chart which will then set the range of the other chart as well.

That works, but you’d have to hit each chart in it’s frame exactly, which is just a bad user experience. The user wants to touch the entire thing (the combined view area consisting of n charts) and just pinch, pan, without the need to put both fingers in chart a or b. 

That’s why I need some sort of touch interceptor that overlays the entire UIScrollView (not the content area, but the visible area) and intercept all touches for that given frame.

Those touches would have to get forwarded to the main chart, which in turn updates the ranges of the indicator charts.

I hope that’s understandable. In case there are any questions left, please let me know.


#5

Ok, I think I understand now.

I took the information from this post and came up with this; I believe it does what you want. Again, this is using the GettingStarted sample app.

  1. Imported SChartCanvas.h and SChartCanvasOverlay.h
  2. Added pan gesture recognizer in viewDidLoad
  3. Set gesture recognizer delegate for chart canvas to self in configureChart
  4. Last 4 methods; specficially the panByValue: in handlePanSwipe and zoomToPoint in handlePinch

The pan doesn’t bounce back if it’s beyond the range of the data like it does if you pan inside the chart. You’ll probably need to bounce it back manually after checking if it’s beyond the bounds.

It’s not quite there yet; just a little more fine-tuning.

@interface ViewController : UIViewController <SChartDatasource, UIGestureRecognizerDelegate>

@property (nonatomic, retain) ShinobiChart *chart1;
@property (nonatomic, retain) ShinobiChart *chart2;
@property (nonatomic, retain) UIScrollView *scrollView;

@end

#import "ViewController.h"
#import <ShinobiCharts/ShinobiCharts.h>
#import <ShinobiCharts/SChartCanvas.h>
#import <ShinobiCharts/SChartCanvasOverlay.h>

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
    self.scrollView.scrollEnabled = YES;
    self.scrollView.contentSize = CGSizeMake(650, 1200);
    
    [self.view addSubview:self.scrollView];
    
    // Add the pan gesture recognizer, set the delegate to self and add it to the current view.
    UIPanGestureRecognizer* panSwipeRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanSwipe:)];
    panSwipeRecognizer.delegate = self;
    [self.view addGestureRecognizer:panSwipeRecognizer];

    UIPinchGestureRecognizer* pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
    pinchRecognizer.delegate = self;
    [self.view addGestureRecognizer:pinchRecognizer];
    
    // Create the chart
    self.view.backgroundColor = [UIColor whiteColor];
    
    //CGFloat margin = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ? 10.0 : 50.0;
    self.chart1 = [[ShinobiChart alloc] initWithFrame:CGRectMake(60, 0, 650, 600)];
    self.chart1 = [self configureChart:self.chart1];
    
    // add to the view
    [self.scrollView addSubview:self.chart1];
    
    self.chart2 = [[ShinobiChart alloc] initWithFrame:CGRectMake(60, 600, 650, 600)];
    self.chart2 = [self configureChart:self.chart2];
    
    // add to the view
    [self.scrollView addSubview:self.chart2];
}

- (ShinobiChart *)configureChart:(ShinobiChart *)chart
{
    // Set the recognizer delegate of the chart to self.
    for (UIGestureRecognizer *recognizer in chart.canvas.overlay.gestureRecognizers)
    {
        if ([recognizer isKindOfClass:[UIPanGestureRecognizer class]] || [recognizer isKindOfClass:[UIPinchGestureRecognizer class]])
        {
            recognizer.delegate = self;
        }
    }

    chart.title = @"Trigonometric Functions";
    chart.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    
    chart.licenseKey = @""; // TODO: add your trial licence key here!
    
    // add a pair of axes
    SChartNumberAxis *xAxis = [SChartNumberAxis new];
    xAxis.title = @"X Value";
    chart.xAxis = xAxis;
    
    SChartNumberAxis *yAxis = [SChartNumberAxis new];
    yAxis.title = @"Y Value";
    yAxis.rangePaddingLow = @(0.1);
    yAxis.rangePaddingHigh = @(0.1);
    chart.yAxis = yAxis;
    
    // enable gestures
    yAxis.enableGesturePanning = NO;
    yAxis.enableGestureZooming = YES;
    xAxis.enableGesturePanning = YES;
    xAxis.enableGestureZooming = YES;
    
    chart.datasource = self;
    
    chart.legend.hidden = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone;
    
    return chart;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

-(BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
    if (![gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) { return YES; }

    CGPoint trans = [gestureRecognizer translationInView:gestureRecognizer.view];
    
    // If we have moved more Y than we have X, ignore the gesture recognizers and let it propagate up to anyone else who cares about it!
    if (abs(trans.x) < abs(trans.y)) return NO;
    
    return YES;
}

- (void)handlePanSwipe:(UIPanGestureRecognizer*)gestureRecognizer
{
    CGPoint trans = [gestureRecognizer translationInView:gestureRecognizer.view];

    [self.chart1.xAxis panByValue:(trans.x * -1) withAnimation:YES withBounceLimits:YES andRedraw:NO];
    [self.chart2.xAxis panByValue:(trans.x * -1) withAnimation:YES withBounceLimits:YES andRedraw:NO];
}

- (void)handlePinch:(UIPinchGestureRecognizer*)gestureRecognizer
{
    float scale = gestureRecognizer.scale;
   
    // Not the right formula but shows it working.
    double x = self.chart1.canvas.overlay.frame.origin.x * scale;
    [self.chart1.xAxis zoomToPoint:x withRange:scale withAnimation:YES usingBounceLimits:YES];

    x = self.chart2.canvas.overlay.frame.origin.x * scale;
    [self.chart2.xAxis zoomToPoint:x withRange:scale withAnimation:YES usingBounceLimits:YES];
    
    double y = self.chart1.canvas.overlay.frame.origin.y * scale;
    [self.chart1.yAxis zoomToPoint:y withRange:scale withAnimation:YES usingBounceLimits:YES];
    
    y = self.chart2.canvas.overlay.frame.origin.y * scale;
    [self.chart2.yAxis zoomToPoint:y withRange:scale withAnimation:YES usingBounceLimits:YES];
}

Hope this helps.

Wg


#6

Thanks a lot Wizgod, that’ll definitely get me started.

The basic principle of this works just as expected. There’s just some Shinobi charts related issues that I need to work out with the Shinobi squad now, such as the bouncing issue mentioned in the thread you’ve linked.

Again, thanks a lot for your help and effort. Highly appreciate it.


#7

Hi Maximilian,

I had a quick read of this thread, I just want to confirm what you’re trying to achieve.

You want the user to be able to use gestures on ANY part of the scroll view to control the chart. To do this, you’re adding your own gesture recognizers to the scroll view & manually setting the range on your chart. I suppose I’m a little confused as to what the problem is here - you mention that the chart just isn’t picking up any gestures, but I’m not sure why that’s an issue if you are using your UIView in the foreground to pick up gestures? I suppose that might be besides the point. 

I would have suggested exaclty what wizgod suggested, is that working for you now? On another point, I’ll take a look at your other forum post and get back to you!

Thanks,
Jan


#8

Hi Jan,

Thanks a lot for looking into this. The solution presented by wizgod does work, there are some minor issues with it such as the chart not bouncing back when the range has been scrolled out of range. Also, I couldn’t figure out the values to the chart in my gesture recognisers yet.

I’d like to continue this discussion on the other thread, if that’s fine for you. There’s a lot more information about this there, including example code.

However, I will keep this thread updated for other readers.