There is a task to implement a graph of currency exchange rates in Angular through Canvas The condition of the problem: you can not use the library work with Canvas. From the beauties: when moving, a vertical line should follow the graph and highlight the current point on the curve. Naturally, next to the point should be the numerical value of the course, the date and the difference with the previous day. This is my first application in Angular and TypeScript.

Did data loading through service, with data waiting through Observable . Made two classes. Instances of the CanvasSettings class will contain information about the size, padding and everything related to the Canvas layers. There will be 2 of them, one on top of the other. The first one will contain graphics that should not be redrawn, the second one will specialize specifically on dynamic graphics.

The second class, DatePoints , contains detailed information about the courses, the coordinates of points, the difference with yesterday, and the simplest functions for calculating some data.

Class interaction occurs at app.component.ts . The component parses the information from the source, checks the points for correctness, sorts in order and fills the instance of DatePoints , calculates the initial and final values ​​on the Y scale so that the course curve fits with a small indent from the bottom and from the top, calculates intervals for months and days each month on a scale of X, as well as the even distribution of days in each month (not all trading days and the number of trading days in each month are different).

All questions on architecture. This information failed to google either in ru or en segments of the Network.

Actually questions.

  1. The most important question is where in the component should the call for rendering functions of the static Canvas layer be placed, and in which dynamic one?
  2. How to transfer mouse position coordinates over a dynamic layer? @HostListener('document:mousemove', ['$event']) ? If so, how, instead of document:mousemove specify the listening area for мойCanvasЭлемент:mousemove event мойCanvasЭлемент:mousemove ?
  3. How to run the function of redrawing a dynamic layer using Angular at a specified interval? If you need to write your throttling function (aka drag ), such as throttle in lowdash, say so without details, but if this is not the Angular way , then please explain or say google.

Sample code app.component.ts

 import { Component, OnInit } from '@angular/core'; import { CanvasSettings } from './canvas-settings'; import { Rate } from './rate'; import { RateService } from './rates.service'; import { DatePoints, Year, Month, Day } from './dates'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], providers: [RateService] }) export class AppComponent implements OnInit { title = 'Exchange Rates via Canvas'; bgrdCanvas = new CanvasSettings("bgrdCanvas"); // статический слой interactiveCanvas = new CanvasSettings("iaCanvas"); // динамический слой rates: Rate[]; dateArray: DatePoints; constructor(private _rateService: RateService){ } ngOnInit() { this.getRates(); // получаем массив котировок в this.rates this.parseDataArray(); // перебиваем значения в объект this.dateArray, точки, не соответсвующие формату данных, выбрасываем this.organizeArray(); // сортируем, чтобы даты в массивах this.dateArray шли по порядку друг за другом this.setDiffs(); // высчитываем diffы между каждой парой точек в this.dateArray this.calculateCoords(); // вычисляем координаты каждой точки на Canvas слое, учитывая отступы и равномерную разбивку дней внутри каждого месяца this.setAxisExtremums(); } getRates():void { this._rateService.getRates() .subscribe(rates => this.rates = rates); } parseDateFromRate(rate: Rate): Array<number> { //... } parseDataArray():void { //... } organizeArray():void { //... } setDiffs():void { //... } calculateCoords():void { //... } setAxisExtremums():void { //... } drawStaticCanvas():void { // где вызывать? let canvas = document.getElementById(this.bgrdCanvas.idSelector); console.log(canvas); let ctx = canvas.getContext("2d"); } drawDynamicCanvas():void { // где вызывать? Есть ли throttle? //... } } 

    1 answer 1

    1. The call to the rendering function of the dynamic Canvas layer occurs in the HTML code of the component, or rather, the call is not the drawing function itself, but the wrapper function that controls the call frequency. The function is called in the onmousemove event inside the tag: (mousemove)="redrawWrapper($event)" . Below is the entire code.

    app.component.html ():

     <div style="text-align:center"> <h1> Welcome to {{ title }}! </h1> <img width="300" alt="Angular Logo" src="..."> </div> <div class="canvasOuter"> <!-- Это статический слой --> <canvas id="{{bgrdCanvas.idSelector}}" width="{{bgrdCanvas.width}}" height="{{bgrdCanvas.height}}"></canvas> <!-- Это динамически перерисовываемый слой --> <canvas id="{{dynCanvas.idSelector}}" width="{{dynCanvas.width}}" height="{{dynCanvas.height}}" (mousemove)="redrawWrapper($event)"></canvas> <div class="canv-helper" #helper> <span class="canv-helper__date"></span> <span class="canv-helper__data helper-data"> <span class="helper-data__cost"></span> <span class="helper-data__diff"></span> </span> </div> </div> <footer></footer> 
    1. The forwarding of mouse position coordinates over the element in the AppComponent carried out as follows. I underline the word above because the coordinates above the other elements will not be recorded. The event itself and the coordinates of the mouse gives us the code (mousemove)="redrawWrapper($event)" , from the previous question.

    In the listing below, I cite a strongly truncated code to demonstrate only the forwarding of coordinates.

    app.component.ts :

     import { Component, OnInit, AfterViewInit, ElementRef, ViewChild, Renderer2 } from '@angular/core'; import { CanvasSettings } from './canvas-settings'; import { Rate } from './rate'; import { RateService } from './rates.service'; import { DatePoints, Year, Month, Day, Monthes, ofMonth } from './dates'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], providers: [RateService] }) export class AppComponent implements OnInit, AfterViewInit { event: MouseEvent; // создаём свойство внутри компонента clientX: number = 0; clientY: number = 0; onEvent(event: MouseEvent): void { this.event = event; // и записываем в него событие } redrawWrapper(event: MouseEvent) { // запускает redraw через дроссель, //но об этом потом let that = this; let event = that.event; // вот тут само таинство, делаем переменную event ссылкой на объект события let clientX: number = event.clientX; // и перебиваем координаты let clientY: number = event.clientY; } } 
    1. Unfortunately, my throttle function makes a delay for only one of the component methods (the method is called the object function). This is not a universal solution, but it demonstrates how to assemble your brakes .

       import { Component, OnInit, AfterViewInit, ElementRef, ViewChild, Renderer2 } from '@angular/core'; import { CanvasSettings } from './canvas-settings'; import { Rate } from './rate'; import { RateService } from './rates.service'; import { DatePoints, Year, Month, Day, Monthes, ofMonth } from './dates'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], providers: [RateService] }) export class AppComponent implements OnInit, AfterViewInit { @ViewChild("helper") helper: ElementRef; event: MouseEvent; //... isThrottled: boolean = false; // текущий статус, тикает сейчас таймер или нет onEvent(event: MouseEvent): void { this.event = event; } redrawWrapper(event: MouseEvent) { // функция-дроссель для метода redraw this.event = event; let that = this; let savedThis: any; function wrapper() { if (that.isThrottled) { // если запущен, сохраняем контекст запуска, т.е. this. savedThis = that; // Записываем контекст последнего вызова. // Это больше актуально, когда дросселировать // предполагается разные функции, а не одну, как у меня. // В моём случае эта переменная является индикатором, // контролирующим, был ли вызов функции после её // последнего исполнения. Это нужно, чтобы вызвать // функцию ещё раз, если вызов был. Без этого // анимация не проследует за курсором, // т.к. во время действия таймера курсор сдвинулся // ещё немного. return; } // запускаем функцию. that - это контекст, т.е. объект, // который будет подразумеваться под this, // that.event - это свойство текущего компонента that.redraw.call(that, that.event); that.isThrottled = true; setTimeout(function() { that.isThrottled = false; if (savedThis) { // обратите внимание, вызываем не redraw(), а wrapper() wrapper.apply(savedThis); savedThis = null; } }, 40); // количество миллисекунд, через которое вызываем функцию } return wrapper(); } redraw(event: MouseEvent): void { // эта функция обеспечивает динамическую перерисовку Canvas //... this.redrawHelper(point, whichYear, whichMonth, currentDayIndex); //... } redrawHelper(point: Day, year: number, month: number, day: number):void { //... this.setHelperPosition(point); } setHelperPosition(point: Day):void { //... } constructor(private _rateService: RateService, private renderer: Renderer2){ } //... 

      }

    If you google it, I hope my code helped you with something. The project repository is here: https://bitbucket.org/frost7/exchange-rates