import { Component, OnInit, ElementRef, Input, OnChanges, SimpleChanges, Optional, Inject, OnDestroy, HostListener, ViewChild, AfterViewInit, HostBinding } from '@angular/core'; import * as d3 from 'd3'; import { Subject, fromEvent, merge } from 'rxjs'; import { debounceTime, takeUntil, tap } from 'rxjs/operators'; import { Dataset, ChartType, ChartContext, Point } from './chart.model'; @Component({ selector: 'app-chart', template: `
{{ tooltipXValue }}
{{ tooltipYValue }}

Aucune donnée

Données non disponibles

`, styles: [ ':host {display: block; position: relative;}', '.loader-wrapper { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }' ] }) export class ChartComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit { @Input() dataset: Dataset; @Input() type = 'scatter-graph'; @Input() tooltipDisabled = false; @Input() tooltipOnClick = false; @Input() tooltipOnHover = false; tooltipXValue: string; tooltipYValue: string; tooltipTopPosition: number; tooltipLeftPosition: number; tooltipVisible = false; tooltipFixed = false; @ViewChild('tooltip') tooltip: ElementRef; private chart: d3.Selection; private chartTypes: ChartType[]; private unsubscriber = new Subject(); private changes$ = new Subject(); private _context: ChartContext; constructor( private host: ElementRef, @Optional() @Inject('ChartType') chartTypes: ChartType[] ) { this.chartTypes = chartTypes || []; merge( fromEvent(window, 'resize').pipe( debounceTime(300), tap(() => { this._context = this.context; }) ), this.changes$ ).pipe(takeUntil(this.unsubscriber)).subscribe(() => { if (this.chart) { this.update(); } }); } ngAfterViewInit(): void { if (!this.chart) { this.initChart(); } // For unknown reasons, the width and height may be changed, so we avoid this behavior by freezing the context. this._context = this.context; this.changes$.next(); } ngOnInit(): void { } ngOnDestroy(): void { this.unsubscriber.next(); this.unsubscriber.complete(); } ngOnChanges(changes: SimpleChanges) { if (changes.type && changes.type.previousValue) { const previousType = this.chartTypes.find(t => t.type === changes.type.previousValue); const width = this.width; const height = this.height; previousType.destroy({ height, width, chart: this.chart }); } if (changes.dataset) { this.changes$.next(); } } update() { this.chartType.udpate(this.dataset, this.context); } private get context(): ChartContext { if (this._context) { return this._context; } return { height: this.height, width: this.width, chart: this.chart }; } private get width(): number { return (this.host.nativeElement as HTMLElement).getBoundingClientRect().width; } private get height(): number { return (this.host.nativeElement as HTMLElement).getBoundingClientRect().height; } private initChart() { this.chart = d3.select(this.host.nativeElement) .append('svg') .attr('width', '100%') .attr('height', '100%'); } private get chartType(): ChartType { return this.chartTypes.find(t => t.type === this.type); } @HostListener('click', ['$event']) onClick(event: MouseEvent) { if (this.tooltipOnClick && (event.target as SVGElement).classList.contains('clickable')) { this.tooltipFixed = true; this.tooltipVisible = true; const rect = (event.target as SVGElement).getBoundingClientRect(); this.tooltipTopPosition = rect.top; this.tooltipLeftPosition = rect.left + rect.width / 2; const point: Point = JSON.parse((event.target as SVGElement).getAttribute('data-point')); const xFormatter = this.dataset.axis && this.dataset.axis.x && this.dataset.axis.x.tooltipFormatter || ((value, _point, dataset) => value); const yFormatter = this.dataset.axis && this.dataset.axis.y && this.dataset.axis.y.tooltipFormatter || ((value, _point, dataset) => value); this.tooltipXValue = xFormatter(point.x, point, this.dataset); this.tooltipYValue = yFormatter(point.y, point, this.dataset); } else { this.tooltipVisible = false; } } @HostListener('mousemove', ['$event']) onMouseMove(event: MouseEvent) { if (!this.tooltipOnHover) { return; } if (this.chart) { this.chartType.globalMouseUpdate(this.dataset, this.context, event); } if (!this.tooltipDisabled && this.tooltip && this.chartType.scaleX && this.chartType.scaleY) { const rect = this.tooltip.nativeElement.getBoundingClientRect(); const padding = this.chartType.padding; let x = event.offsetX + padding; let y = event.offsetY + padding; if (event.offsetX + rect.width + padding * 2 > this.width) { x = event.offsetX - rect.width - padding; } if (event.offsetY + rect.height + padding * 2 > this.height) { y = event.offsetY - rect.height - padding; } this.tooltipTopPosition = y; this.tooltipLeftPosition = x; const xFormatter = this.dataset.axis && this.dataset.axis.x && this.dataset.axis.x.tooltipFormatter || ((value) => value); const yFormatter = this.dataset.axis && this.dataset.axis.y && this.dataset.axis.y.tooltipFormatter || ((value) => value); this.tooltipXValue = xFormatter(this.chartType.scaleX.invert(event.offsetX)); this.tooltipYValue = yFormatter(this.chartType.scaleY.invert(event.offsetY)); } } @HostListener('mouseenter') onMouseEnter() { if (this.tooltipOnHover) { this.tooltipVisible = true; } } @HostListener('mouseleave') onMouseLeave() { if (this.tooltipOnHover) { this.tooltipVisible = false; } } }