0

Updated with code to clarify. The TVC Component hosts a Trading View lightweight-charts component.

There is a side nav with a list of items. And every time a new/different item is selected, it fires this.data.getDataForSymbol() in the Main Content Component. The chart re-renders perfectly when NOT using the caching... but when the cache is used (and confirmed to be working)... the graph does not re-render.

Here is the component that renders the chart:

@Component({
  selector: 'tvc',
  template: '<div #chart></div>',
})
export class TvcComponent implements AfterViewInit {

  @ViewChild('chart') chartElem: ElementRef;

  @Input()
  data: (BarData | WhitespaceData)[] | null;

  chart: IChartApi = null;

  ngAfterViewInit() {
    this.buildChart();
  }

  buildChart() {
    this.chart = createChart(<HTMLElement>this.chartElem.nativeElement, {
      width: 600,
      height: 300,
      crosshair: {
        mode: CrosshairMode.Normal,
      },
    });

    this.chart.timeScale().fitContent();
    const candleSeries = this.chart.addCandlestickSeries();
    candleSeries.setData(this.data);
  }
}

And here is the component that hosts the TvcComponent, providing data to the chart:

@Component({
  selector: 'main-content',
  template: `
      <div *ngIf="monthly$ | async as monthly">
        <tvc
          [data]="monthly"
        ></tvc>
      </div>`
})
export class MainContentComponent implements OnInit {

  monthly$: Observable<any[]>;

  constructor(
    private route: ActivatedRoute,
    private itemStore: ItemStore,
    private data: DataService
  ) {}

  ngOnInit(): void {
    this.route.params.subscribe((params) => {
      let id = params['id'];
      this.itemStore.items$.subscribe((items) => {
        this.monthly$ = this.data.getDataForSymbol(id, 'monthly');
      });
    });
  }
}

Here is the relevant code for the interceptor service:

@Injectable({ providedIn: 'root' })
export class CacheInterceptor implements HttpInterceptor {
  constructor(private cache: HttpCacheService) {}

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const cachedResponse = this.cache.get(req.urlWithParams);

    if (cachedResponse) {
      console.log(`${req.urlWithParams}: cached response`);
      return of(cachedResponse);
    }
    return next.handle(req).pipe(
      tap((event) => {
        if (event instanceof HttpResponse) {
          this.cache.put(req.urlWithParams, event);
          console.log(`${req.urlWithParams}: response from server`);
        }
      })
    );
  }
}

and the caching service:

@Injectable()
export class HttpCacheService {
  private cache = {};

  get(url: string): HttpResponse<any> {
    return this.cache[url];
  }
  put(url: string, resp: HttpResponse<any>): void {
    this.cache[url] = resp;
  }
}

I've implemented an HttpInterceptor for caching (example from Angular Github), and am caching the HttpResponse for data that is then subscribed to with an async pipe in the template - and passed as an input property to a child component. The observable contains data that renders a chart.

The data is (largely) static and selecting different items triggers a new Http Request. So if someone bounces back-and-forth between several charts, they will be making multiple (repeat) calls unnecessarily. Hence, the caching.

The problem is that while the cache works beautifully as determined via console logging)... the graph does not update/re-render when accessing data from the cache. The first time you select Item A, it gets the data from the server and renders correctly. If you move select Item B (not in cache), it makes the server request, puts the response in the cache, and renders the correct graph. The problem is if you switch BACK to Item A, it gets the correct data from the cache, but does NOT update the graph.

I am using default Change Detection.

timocov
  • 1,088
  • 6
  • 16
Kevin
  • 895
  • 2
  • 10
  • 21
  • 1
    Mmmm code is needed to check what is hppening with your refresh.. – Victor Martinez Calvo Jan 03 '21 at 01:10
  • i would assume it is not a change detection problem at all. I would guess the problem is that the tvc component does not react to changes in data. it only ever updates the data when recreated (ngAfterViewInit is only called once) which will probably not happen in the case you described. If it is actually the change detection then a call to https://angular.io/api/core/ChangeDetectorRef#detectChanges after loading the data would fix the problem. If it is not the change detection, then you probably need a setter on tvc.data to update the graph – x4rf41 Jan 04 '21 at 08:20
  • Thank you for the input @x4rf41. It has helped me get closer to a solution. I noticed an interesting thing... when the response from```this.monthly$ = this.data.getDataForSymbol(id, 'monthly');``` (in the MainContentComponent above) is NOT in the cache (and therefore makes an HTTP request), ngAfterViewInit fires. When that same call returns data from the cache, ngAfterViewInit does NOT fire. How can I determine what is causing ngAfterViewInit to fire? – Kevin Jan 05 '21 at 13:06
  • ngAfterViewInit only fires if the tvc component gets created. That can only happen when either `*ngIf="monthly$ | async as monthly"` changes from null to something or if the parent component get created because of a route change for example but only if the route changes from another component, not if just the params change which i guess is what happens in your case) – x4rf41 Jan 05 '21 at 14:52

1 Answers1

0

I assume that the monthly$: Observable<any[]> variable gets changed correctly from what you wrote and that the Observable gets the new value (you can check that by logging). If that is the case, then the [data]="monthly" binding will be correctly updated by the change-detection.

That would mean, your problem is that the tvc component does not update correctly, because it does not react to changes in @Input() data. If you change the component to the following, it should work:

(There might be syntax errors on my part because I write this without being able to check it because you did not provide a working example code)

@Component({
  selector: 'tvc',
  template: '<div #chart></div>',
})
export class TvcComponent implements AfterViewInit {

  @ViewChild('chart') chartElem: ElementRef;
  
  private _data: (BarData | WhitespaceData)[] | null;

  get data(): (BarData | WhitespaceData)[] | null {
     return this._data;
  }

  @Input()
  set data(value: (BarData | WhitespaceData)[] | null) {
     // this gets called by the change-detection when the value of monthly changes from the [data]="monthly" binding
     // with that, we can use it to refresh the data
     // because the data is not bound to the chart by angular through a template, we have to do it manually. the change-detection only goes so far
     this._data = value;
     this.refreshData(); 
  }

  chart: IChartApi = null;
  candleSeries: any = null; // I don't know the correct type so I use any. You should change that

  ngAfterViewInit() {
    this.buildChart();
    this.refreshData();
  }

  buildChart() {
    this.chart = createChart(<HTMLElement>this.chartElem.nativeElement, {
      width: 600,
      height: 300,
      crosshair: {
        mode: CrosshairMode.Normal,
      },
    });

    this.chart.timeScale().fitContent();
    this.candleSeries = this.chart.addCandlestickSeries();
  }

  refreshData() {
    if (!this.candleSeries) return; // might not be initialized yet
    // I don't know the library, so I can't be sure that this is enough to update the data. 
    // You may have to do more. You can put a log here to see if it triggers
    this.candleSeries.setData(this.data);
  }
}

I hope this works for you. Just make sure that the data setter gets called correctly when you change the data. The rest can then be handled in the refreshData() method

x4rf41
  • 5,184
  • 2
  • 22
  • 33