// Angular // import { Component, Input, AfterViewInit, OnChanges, ChangeDetectionStrategy, ViewChild, ElementRef, Inject } from '@angular/core'; // Services // import { TranslateService } from '@ngx-translate/core'; // Other // import * as _ from 'underscore'; import * as statics from '@fb/statics'; @Component({ selector: 'fb-graph', changeDetection: ChangeDetectionStrategy.OnPush, template: '
', styleUrls: [] }) export class FbGraphComponent implements AfterViewInit, OnChanges { @Input() data: fb.IFbGraphData[]; @Input() options: fb.IFbGraphOptions; @ViewChild('graphWrapper') graphWrapperElement: ElementRef; svg: D3.Selection; rootElement: D3.Selection; private animationDuration: number = 1000; private animationDelay: number = 200; private currentAngleSet: IAngleSet; constructor( private readonly translate: TranslateService, @Inject('TextWidthService') private readonly textWidthService: fb.ITextWidthService ) { } ngAfterViewInit(): void { this.checkInput(); this.rootElement = d3.select(this.graphWrapperElement.nativeElement); this.refreshGraph(); } ngOnChanges(): void { if (this.rootElement && this.data) { this.refreshGraph(); } } private refreshGraph(): void { if (this.options.graphType === fb.FasITDomain.GraphType.Gauge) { this.data.push({ value: this.options.total - this.data[0].value, color: '#d3d3d3', __isComplement: true }); } this.draw(); this.update(); } private draw(): void { this.svg = this.bootstrap(this.options.width, this.options.height, this.options.graphType); this.createGraph(); if (this.options.graphType === fb.FasITDomain.GraphType.Circle || this.options.graphType === fb.FasITDomain.GraphType.Gauge) { this.createLegend(); } } private bootstrap(width, height, graphType): D3.Selection { this.rootElement.select('svg').remove(); const svg: D3.Selection = this.rootElement.append('svg') .attr('width', width) .attr('height', height) .append('g'); if (_.contains([ fb.FasITDomain.GraphType.Circle, fb.FasITDomain.GraphType.Gauge ], graphType)) { // Grafer som transformeras till mitten av svg:n svg.attr('transform', `translate(${width * 0.5},${height * 0.5})`); } else if (graphType === fb.FasITDomain.GraphType.StackedBar) { svg.attr('transform', `translate(${30},${30})`); } return svg; } private createGraph(): void { this.svg.append('g').attr({ 'id': 'graph' }); } private createLegend(): void { this.svg.append('g') .attr('id', 'label').append('text'); if (this.options.subLabel) { this.svg.append('g') .attr('id', 'subLabel').append('text'); } } private update(): any { switch ( this.options.graphType ) { case fb.FasITDomain.GraphType.Circle: case fb.FasITDomain.GraphType.Gauge: this.updateGraphPie(); this.updateLegend(); break; case fb.FasITDomain.GraphType.StackedBar: this.updateGraphBar(); break; } } private updateGraphPie(): void { const pieLayout: D3.Layout.PieLayout = d3.layout.pie().value(d => d.value); if (this.options.graphType === fb.FasITDomain.GraphType.Gauge) { pieLayout.sort(null); } const paths: D3.UpdateSelection = this.svg.selectAll('#graph') .selectAll('.arc') .data(pieLayout(this.data), d => d.data.color); const tween: (d: any) => (t: any) => string = (d: any) => { const oldAngles: IAngleSet = this.currentAngleSet ? this.currentAngleSet : { startAngle: isNaN(d.startAngle) ? 0 : d.startAngle, endAngle: isNaN(d.startAngle) ? 0 : d.startAngle }; const newAngles: IAngleSet = { startAngle: isNaN(d.startAngle) ? 0 : d.startAngle, endAngle: isNaN(d.endAngle) ? 0 : d.endAngle }; const i: any = d3.interpolate(oldAngles, newAngles); const arc: D3.Svg.Arc = d3.svg.arc() .outerRadius(this.options.width / 2) .innerRadius(this.options.width / 2 - this.options.thickness); return ((t) => { return arc(i(t)); }); }; paths.enter() .append('path') .attr({ 'class': 'arc', 'id': (d, i) => { // tslint:disable-line return `arc_${i}`; } }) .style({ 'fill': (d) => { return d.data.color; }, }); paths .transition() .duration(2500) .attrTween('d', tween) .each('end', (d) => { this.currentAngleSet = { startAngle: isNaN(d.startAngle) ? 0 : d.startAngle, endAngle: isNaN(d.endAngle) ? 0 : d.endAngle }; }); paths.exit().remove(); } private updateLegend(): void { const availableWidth: number = this.options.width - this.options.thickness * 2; const labelLength: number = this.options.label.value.length; let labelFontSize: number = labelLength < 6 ? 32 : labelLength < 9 ? 26 : 18; if (availableWidth < 100) { labelFontSize *= 0.75; } const labelY: number = this.options.subLabel ? 0 : labelFontSize / 4; this.svg.select('#label text') .text(this.options.label.value) .attr({ 'text-anchor': 'middle', 'text-transform': 'uppercase', 'y': labelY, }) .style({ 'font-size': labelFontSize + 'px', 'fill': this.options.label.color, }); if (this.options.subLabel) { const fontsize: number = Math.min(this.textWidthService.getMaxAvaliableWidth(this.options.subLabel.value, this.options.width - this.options.thickness * 4, {} as any), 18); this.svg.select('#subLabel text') .text(this.options.subLabel.value) .attr({ 'text-anchor': 'middle', 'font-size': fontsize + 'px', 'y': 24 }) .style({ 'fill': this.options.subLabel.color, }); } else { this.svg.select('#subLabel text') .text(''); } } private updateGraphBar(): void { const margin: number = 30; const width: number = this.options.width - 2 * margin; const height: number = this.options.height - 2 * margin; this.rootElement.select('.g').remove(); this.rootElement.select('.axis').remove(); this.rootElement.select('.label').remove(); const mappedData: any[] = []; for (let i: number = 0; i < this.data[0].value.length; i++) { let y0: number = 0; const values: IMappedValue[] = this.data.map((item: any): IMappedValue => { const val: IMappedValue = { y0: y0, y1: y0 += item.value[i], color: item.color, icon: item.icon }; return val; }); const total: number = values[values.length - 1].y1; const icon: { icon: string; color: string }[] = _.map(this.data, (item: fb.IFbGraphData) => ({ icon: item.icon[i], color: item.color })); // jshint ignore:line mappedData.push({ values: values, icon: icon, state: this.translate.instant(`DATE_MOMENTMAP.${i}`), total: total }); } const x: D3.Scale.OrdinalScale = d3.scale.ordinal().rangeRoundBands([0, width], 0.33); const y: D3.Scale.QuantitiveScale = d3.scale.linear().rangeRound([height, 0]); const xAxis: D3.Svg.Axis = d3.svg.axis().scale(x).orient('bottom'); const yAxis: D3.Svg.Axis = d3.svg.axis().scale(y).orient('left') .ticks(5) .tickFormat(d3.format('.s')); x.domain(mappedData.map(d => d.state)); y.domain([ 0, d3.max(mappedData, d => d.total) ]); this.svg.append('g').attr('class', 'x axis') .attr('transform', `translate(0,${height})`) .call(xAxis); this.svg.append('g').attr('class', 'y axis') .attr('transform', `translate(${margin / 2},0)`) .call( yAxis.tickSize(-width + margin, 0, 0) ); this.svg.append('g').attr('class', 'y axis no-line') .attr('transform', `translate(${this.options.width - margin * 2 - 15},0)`) .call(yAxis.orient('right')); if (this.options.subLabel) { this.svg.append('g').attr('class', 'label') .append('text') .attr('text-anchor', 'end') .attr('x', this.options.width - 2 * margin) .attr('y', 0) .attr('fill', 'lightgrey') .attr('style', 'font-size: 14px;font-weight: 400;') .text(this.options.subLabel); } let iter: number = 0; const barContainer: D3.Selection = this.svg.selectAll('.bar') .data(mappedData).enter().append('g') .attr('class', 'g').attr('transform', d => `translate(${x(d.state)},0)`); function getIconObj (d: any): any { return _.first( _.filter(d.icon, (item: any) => { return !!(item as any).icon; })) as any; } // const getIconObj: (d: any) => any = d => { // return _.first(_.filter(d.icon, item => { // return !!(item as any).icon; // })) as any; // }; barContainer .append('text') .attr('font-family', 'fasit-ikonpaket') .attr('font-size', () => x.rangeBand() / 2 + 'px') .attr('x', Math.floor(x.rangeBand() / 4)) .attr('y', d => y(d.total) - x.rangeBand() / 4) .attr('fill', d => { const iconObj: any = getIconObj(d); return iconObj && iconObj.color; }) .text( d => { const iconObj: any = getIconObj(d); return iconObj && iconObj.icon; }) .attr('opacity', '0') .transition() .duration(this.animationDuration) .delay( (d, i) => i * this.animationDelay) // tslint:disable-line .attr('opacity', '1'); barContainer.selectAll('rect') .data( d => d.values) .enter().append('rect') .attr('width', x.rangeBand()) .attr('y', d => y(d.y1)) .style('fill', d => d.color) .attr('height', () => 0) .attr('y', (d) => y(d.y0)) .transition() .duration(this.animationDuration) .delay((d, i) => { if (d.y0 !== d.y1) { iter += 1; } return iter * this.animationDelay / 2 + i * this.animationDuration / 3; }) .attr('y', (d) => y(d.y1)) .attr('height', d => y(d.y0) - y(d.y1)); } private checkInput(): void { if (statics.isUndefined(this.data)) { throw new Error(`Fb-graph: not all inputs are defined. Data: ${this.data} | Options: ${this.options}`); } } } export interface IAngleSet { startAngle: number; endAngle: number; } export interface IMappedValue { y0: number; y1: number; color: any; icon: any; }