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;
}
}
}