abstruse
Version:
Abstruse CI
257 lines (210 loc) • 7.41 kB
text/typescript
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';
({
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));
});
}
}