import {Component, OnInit, Input, ViewEncapsulation} from "@angular/core"; import {Http} from '@angular/http'; import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/map'; import {ChartHelpers, IPadding, XAxisSide, YAxisSide} from "../utilities/chart-helpers"; import * as d3 from "d3"; import {EventHelpers} from "../utilities/event-helpers"; import {forEach} from "lodash"; import {ZoomTransform} from "@types/d3-zoom"; import {AreaChartBase} from "../base-classes/area-chart-base"; interface IOverlayElement { xOffset: number; yOffset: number; xOffsetRelativeToGraph: number; yOffsetRelativeToGraph: number; visibility: string; } interface ICursor extends IOverlayElement{ xValue: number, yValue: number, tooltipDirection: string; } interface IPeak extends IOverlayElement{ xValue: number, yValue: number, } enum CursorInfoLocation { Top = 1, Bottom } @Component({ selector: 'chart', templateUrl: './chart.component.html', encapsulation: ViewEncapsulation.None, styleUrls: ['./chart.component.scss'] }) export class ChartComponent extends AreaChartBase implements OnInit { @Input() public controlsShown: boolean = true; // General Properties public chartMode: string = 'zoom'; // Overlay Properties public cursors: Array = []; public peaks: Array = []; public cursorOverlayPointerEvents: string = 'none'; public cursorInfoLocation = CursorInfoLocation.Top; public innerPadding: IPadding = { top: 10, right: 70, bottom: 40, left: 20 }; public overlayBoundaries; private zoomInteractionGroup; private cursorInfoWidth = 120; private indicatorHeight = 20; // Zoom State private currentXZoom: number = 1; private currentYZoom: number = 1; private currentXTranslate: number = 0; private currentYTranslate: number = 0; //mock data private peakData: Array = [419, 757]; protected pollingInterval = 100; constructor(protected http: Http) { super(http); if(this.cursorInfoLocation == CursorInfoLocation.Top){ this.overlayBoundaries = { top: 0, right: this.innerPadding.right + 'px', bottom: this.innerPadding.bottom + 'px', left: this.innerPadding.left + 'px' } } else { this.overlayBoundaries = { top: this.innerPadding.top + 'px', right: this.innerPadding.right + 'px', bottom: 0, left: this.innerPadding.left + 'px' } } } ngOnInit() { super.getXData().subscribe(wavelengths => { super.getYData().subscribe(intensities => { super.onInit(wavelengths, intensities); this.xAxis = ChartHelpers.buildXAxis(this.svg, this.xScale, this._width, this._height, this.innerPadding, this.xAxisSide, "Wavelength"); this.yAxis = ChartHelpers.buildYAxis(this.svg, this.yScale, this._width, this._height, this.innerPadding, YAxisSide.Right, "Intensity", true); ChartHelpers.drawGrid(this.svg, this._width, this._height, this.xAxisSide, YAxisSide.Right); this.setupZoomAndPan(); }); }); } public onModeChange (mode: string) { this.chartMode = mode; if(this.chartMode == 'zoom') { this.cursorOverlayPointerEvents = 'none'; } else { this.cursorOverlayPointerEvents = 'all' } } public addCursor(event){ let currentOffset = event.offsetX; let xOffsetRelativeToGraph = (currentOffset - this.currentXTranslate)/this.currentXZoom + this.innerPadding.left; this.cursors.push({ xValue: Math.round(this.xScale.invert(xOffsetRelativeToGraph)*100)/100, yValue: this.interpolateYValue(this.xScale.invert(xOffsetRelativeToGraph)), xOffsetRelativeToGraph: xOffsetRelativeToGraph, yOffsetRelativeToGraph: 0, xOffset: event.offsetX, yOffset: 0, visibility: "visible", tooltipDirection: currentOffset + this.innerPadding.left > this._width - this.cursorInfoWidth ? "toLeft" : "toRight" }); } private setupZoomAndPan() { this.zoomInteractionGroup = this.svg.append("g") .attr("width", this._width) .attr("height", this._height); this.zoomInteractionGroup.append("rect") .attr("width", this._width - this.innerPadding.left - this.innerPadding.right) .attr("height", this._height - this.innerPadding.bottom) .attr("class", "xyZoom") .attr("transform", "translate(" + this.innerPadding.left + ",0)") .attr("opacity", "0"); /* We're throttling this event handler, because rescaling the axes too often causes it to get choppy in IE and Firefox. */ this.zoomInteractionGroup.call(d3.zoom() .scaleExtent([1, 40]) .translateExtent([[0, 0],[this._width, this._height]]) .on("zoom", EventHelpers.throttleEventHandler(this.zoomCallback, 30, [], this))); } private zoomCallback() { let gX = this.svg.select(".x-axis"); let gY = this.svg.select(".y-axis"); if(d3.event.sourceEvent.target.getAttribute && d3.event.sourceEvent.target.getAttribute("class") == "xyZoom"){ this.moveOverlayElements(d3.event.transform); this.zoomAndPanTarget.attr("transform", "translate(" + d3.event.transform.x + "," + d3.event.transform.y + ") " + "scale(" + d3.event.transform.k + ", " + d3.event.transform.k +")"); gX.call(this.xAxis.scale(d3.event.transform.rescaleX(this.xScale))); gY.call(this.yAxis.scale(d3.event.transform.rescaleY(this.yScale))); this.currentXTranslate = d3.event.transform.x; this.currentYTranslate = d3.event.transform.y; this.currentXZoom = d3.event.transform.k; this.currentYZoom = d3.event.transform.k; } ChartHelpers.drawGrid(this.svg, this._width, this._height, this.xAxisSide, YAxisSide.Right); } private moveOverlayElements (transform: ZoomTransform) { forEach (this.cursors, (cursor: ICursor) => { this.moveOverlayElement(cursor, transform); if (cursor.xOffset + this.innerPadding.left > this._width - this.innerPadding.right - this.cursorInfoWidth) { cursor.tooltipDirection = 'toLeft'; } else { cursor.tooltipDirection = 'toRight'; } }); forEach (this.peaks, (peak: IPeak) => { this.moveOverlayElement(peak, transform); }); } private moveOverlayElement (overlayElement: IOverlayElement, transform) { // Must subtract inner padding from transform.x because the cursor inside the (smaller) overlay container overlayElement.xOffset = overlayElement.xOffsetRelativeToGraph * transform.k + transform.x - this.innerPadding.left; overlayElement.yOffset = overlayElement.yOffsetRelativeToGraph * transform.k + transform.y; // Hide element if it's outside of the main graph window if (overlayElement.xOffset < 0 || overlayElement.xOffset + this.innerPadding.left > this._width - this.innerPadding.right) { overlayElement.visibility = 'hidden'; } else { overlayElement.visibility = 'visible'; } } private interpolateYValue(xValue) : number { for(let i = 0; i < this.dataSet.length; i++) { if (this.dataSet[i][0] > xValue) { let distanceFromLeftPoint = xValue - this.dataSet[i-1][0] ; let slope = (this.dataSet[i][1] - this.dataSet[i-1][1])/(this.dataSet[i][0] - this.dataSet[i-1][0]); return Math.round(100*(slope*distanceFromLeftPoint + this.dataSet[i-1][1]))/100; } } } private createPeakObjects() { forEach(this.peakData, (peakXValue) => { let yValue = this.interpolateYValue(peakXValue); let yOffset = this.yScale(yValue) - this.indicatorHeight; if(this.cursorInfoLocation == CursorInfoLocation.Bottom){ yOffset -= this.innerPadding.bottom; } this.peaks.push({ xValue: Math.round(peakXValue*100)/100, yValue: yValue, xOffsetRelativeToGraph: this.xScale(peakXValue), yOffsetRelativeToGraph: yOffset, xOffset: this.xScale(peakXValue) - this.innerPadding.left, yOffset: yOffset, visibility: "visible" }); }); } }