UNPKG

abstruse

Version:
257 lines (210 loc) 7.41 kB
import { Component, OnDestroy, ElementRef, OnChanges, SimpleChanges } from '@angular/core'; import { WindowService } from '../../services/window.service'; import { StatsService } from '../../services/stats.service'; import { select, scaleTime, scaleLinear, scaleOrdinal, timeParse, line, curveLinear, extent, min, max, axisBottom, axisLeft, area } from 'd3'; import { isSameDay, format, subDays } from 'date-fns'; import { Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; @Component({ selector: 'app-line-chart', templateUrl: 'app-line-chart.component.html' }) export class AppLineChartComponent implements OnDestroy, OnChanges { el: Element; tooltip: any; minDate: string; maxDate: string; sub: Subscription; calendarDateFrom: Date; calendarDateTo: Date; loading: boolean; runs: { success: {}, failed: {} }; constructor( private elementRef: ElementRef, private windowService: WindowService, private statsService: StatsService ) { this.loading = true; this.runs = { success: {}, failed: {} }; this.sub = this.windowService.resize .pipe(filter(x => !!this.el)) .subscribe(e => this.render()); this.calendarDateFrom = subDays(new Date(), 7); this.calendarDateTo = new Date(); this.getData(); } ngOnChanges(changes: SimpleChanges) { if ('data' in changes) { this.render(); } } ngOnDestroy() { if (this.sub) { this.sub.unsubscribe(); } } getData(): void { this.statsService.getJobRunsBetween(this.calendarDateFrom, this.calendarDateTo) .then((runs: any) => { this.loading = false; this.runs = runs; this.render(); }); } timeRangeUpdate(): void { this.getData(); } render() { this.el = this.elementRef.nativeElement.querySelector('.line-chart-container'); select(this.el).select('svg').remove(); const margin = { top: 20, right: 20, bottom: 30, left: 40 }; const style: CSSStyleDeclaration = getComputedStyle(this.el); const width = parseInt(style.width, 10) - margin.left - margin.right; const height = parseInt(style.height, 10) - margin.top - margin.bottom; const svg = select(this.el).append('svg').attr('width', '100%').attr('height', '100%'); const g = svg.append('g') .attr('width', width) .attr('height', height) .attr('transform', `translate(${margin.left}, ${margin.top})`); const parseTime = timeParse('%Y-%m-%d'); const x = scaleTime().rangeRound([0, width]); const y = scaleLinear().rangeRound([height, 0]); let successData = Object.keys(this.runs.success).map(key => { return { date: parseTime(key), value: this.runs.success[key] }; }); let failedData = Object.keys(this.runs.failed).map(key => { return { date: parseTime(key), value: this.runs.failed[key] }; }); successData.forEach(success => { const index = failedData.findIndex(d => isSameDay(new Date(d.date), new Date(success.date))); if (index === -1) { failedData.push({ date: success.date, value: 0 }); } }); failedData.forEach(failed => { const index = successData.findIndex(d => isSameDay(new Date(d.date), new Date(failed.date))); if (index === -1) { successData.push({ date: failed.date, value: 0 }); } }); successData = this.sortByKey(successData, 'date'); failedData = this.sortByKey(failedData, 'date'); const minMaxFormat = 'ddd D.M.'; this.minDate = format(Math.min(...successData .concat(failedData).map(d => Number(format(d.date, 'x')))), minMaxFormat); this.maxDate = format(Math.max(...successData .concat(failedData).map(d => Number(format(d.date, 'x')))), minMaxFormat); if (!successData.length && !failedData.length) { return; } x.domain(extent(successData.concat(failedData), (d: any) => d.date)); y.domain(extent(successData.concat(failedData), (d: any) => d.value)); const l = line() .curve(curveLinear) .x((d: any) => x(d.date)) .y((d: any) => y(d.value)); const a = area() .curve(curveLinear) .x((d: any) => x(d.date)) .y1((d: any) => y(d.value)); a.y0(y(0)); const defs = svg.append('defs'); const successGradient = defs.append('linearGradient') .attr('id', 'successGradient') .attr('x1', '0%').attr('y1', '13%') .attr('x2', '0%').attr('y2', '100%'); successGradient.append('stop') .attr('stop-color', '#2BB415') .attr('stop-opacity', '0.1') .attr('offset', 0); successGradient.append('stop') .attr('stop-color', 'rgba(90, 217, 70, 0.1)') .attr('stop-opacity', '0.1') .attr('offset', 1); const failedGradient = defs.append('linearGradient') .attr('id', 'failedGradient') .attr('x1', '0%').attr('y1', '13%') .attr('x2', '0%').attr('y2', '100%'); failedGradient.append('stop') .attr('stop-color', '#f03e3e') .attr('stop-opacity', '0.1') .attr('offset', 0); failedGradient.append('stop') .attr('stop-color', 'rgba(240, 62, 62, 0.1)') .attr('stop-opacity', '0.1') .attr('offset', 1); g.append('g') .attr('class', 'axis axis--x') .attr('transform', `translate(0, ${height})`) .call(axisBottom(x)); g.append('g') .attr('class', 'axis axis--y') .call(axisLeft(y).tickSizeInner(-width).tickSizeOuter(0).tickPadding(10)); const successPath = g.append('path') .attr('d', l(successData as any)) .attr('fill', 'none') .attr('stroke', '#2BB415') .attr('stroke-width', '2px'); const totalLengthSuccess = (<any>successPath.node()).getTotalLength(); console.log(document.location.href); g.append('path') .attr('d', a(successData as any)) .attr('fill', `url(${document.location.href}#successGradient)`); g.selectAll('dot') .data(successData) .enter().append('circle') .attr('r', 3) .attr('cx', (d: any) => x(d.date)) .attr('cy', (d: any) => y(d.value)) .attr('fill', 'white') .attr('stroke', '#2BB415') .attr('stroke-width', 2); successPath .attr('stroke-dasharray', totalLengthSuccess + ' ' + totalLengthSuccess) .attr('stroke-dashoffset', totalLengthSuccess) .attr('stroke-dashoffset', 0); const failedPath = g.append('path') .attr('d', l(failedData as any)) .attr('fill', 'none') .attr('stroke', '#f03e3e') .attr('stroke-width', '2px'); const totalLengthFailed = (<any>failedPath.node()).getTotalLength(); g.append('path') .attr('d', a(failedData as any)) .attr('fill', `url(${document.location.href}#failedGradient)`); g.selectAll('dot') .data(failedData) .enter().append('circle') .attr('r', 3) .attr('cx', (d: any) => x(d.date)) .attr('cy', (d: any) => y(d.value)) .attr('fill', 'white') .attr('stroke', '#f03e3e') .attr('stroke-width', 2); failedPath .attr('stroke-dasharray', totalLengthFailed + ' ' + totalLengthFailed) .attr('stroke-dashoffset', totalLengthFailed) .attr('stroke-dashoffset', 0); } sortByKey(array, key) { return array.sort((a, b) => { const x = new Date(a[key]); const y = new Date(b[key]); return ((x < y) ? -1 : ((x > y) ? 1 : 0)); }); } }