import { ChangeDetectionStrategy , ChangeDetectorRef , Component , ContentChild , ElementRef , EventEmitter , Input , OnChanges , OnDestroy , OnInit , Output , TemplateRef , ViewChild } from '@angular/core' ; import { arc , Arc , BaseType , easeCubic , interpolate , mouse , select , Selection , transition } from 'd3' ; import { Subject } from 'rxjs' ; import { takeUntil } from 'rxjs/operators' ; import { Color } from '../../common/colors/index' ; import { AnchorPlacement } from '../../common/overlay/index' ; import { ResizeService } from '../../directives/resize/index' ; import { ColorService , ThemeColor } from '../../services/color/index' ; @ Component ( { selector : 'ux-nested-donut-chart' , templateUrl : './nested-donut-chart.component.html' , changeDetection : ChangeDetectionStrategy . OnPush } ) export class NestedDonutChartComponent implements OnInit , OnChanges , OnDestroy { /** Define a the dataset to display */ @ Input ( ) dataset : ReadonlyArray < NestedDonutChartData > ; /** Define the maximum range of the arcs */ @ Input ( ) max : number = 100 ; /** Define the thickness of each arc */ @ Input ( ) thickness : number = 8 ; /** Define the spacing of each arc */ @ Input ( ) spacing : number = 8 ; /** Define the track color */ @ Input ( ) trackColor : string | ThemeColor ; /** Determine if we should show the hover effect */ @ Input ( ) disableHover : boolean = false ; /** Determine if we should show a tooltip on arc hover */ @ Input ( ) disableTooltip : boolean = false ; /** Determine the position of the tooltip */ @ Input ( ) tooltipPlacement : AnchorPlacement = 'top' ; /** Set the duration of the animation */ @ Input ( ) animationDuration : number = 750 ; /** Emit whenever an arc is clicked */ @ Output ( ) itemClick = new EventEmitter < NestedDonutChartData > ( ) ; /** Access the SVG element */ @ ViewChild ( 'chart' , { static : true } ) _chartElement : ElementRef ; /** Allow custom tooltip template */ @ ContentChild ( 'tooltip' , { static : false } ) _customTooltip : TemplateRef < NestedDonutChartData > ; /** Indicate if the tooltip should be visible */ _tooltipVisible : boolean = false ; /** Store the tooltip x position */ _tooltipX : number ; /** Store the tooltip y position */ _tooltipY : number ; /** Store the context to provide to the tooltip */ _tooltipContext : NestedDonutChartData ; /** Determine the radius of the chart based on the specified size */ get _radius ( ) : number { return this . _size / 2 ; } /**
     * Get the size of the chart. The chart will always be square to
     * the size will be the smaller of the width/height properties
     */ get _size ( ) : number { return Math . min ( this . _elementRef . nativeElement . offsetWidth , this . _elementRef . nativeElement . offsetHeight ) ; } /** Store the selection when the arcs will be drawn */ private _arcLayer : Selection < BaseType , NestedDonutChartData , HTMLElement , NestedDonutChartData > ; /** Store the selection when the tracks will be drawn */ private _trackLayer : Selection < BaseType , NestedDonutChartData , HTMLElement , NestedDonutChartData > ; /** Store the arc selection */ private _arcs : Selection < BaseType , NestedDonutChartArc , BaseType , NestedDonutChartData > ; /** Store the tracks selection */ private _tracks : Selection < BaseType , NestedDonutChartArc , BaseType , NestedDonutChartData > ; /** Store the previously processed data */ private _arcData : NestedDonutChartArc [ ] = [ ] ; /** Determine if the intial render has taken place */ private _isInitialized : boolean = false ; /** Unsubscribe from all observables automatically */ private _onDestroy = new Subject < void > ( ) ; constructor ( private _colorService , private _changeDetector , private _elementRef , private _resizeService ) { } /** Perform the initial render */ ngOnInit ( ) { this . _trackLayer = select < SVGElement , NestedDonutChartData > ( this . _chartElement . nativeElement ) . append ( 'g' ) ; this . _arcLayer = select < SVGElement , NestedDonutChartData > ( this . _chartElement . nativeElement ) . append ( 'g' ) ; this . render ( ) ; this . _isInitialized = true ; this . _resizeService . addResizeListener ( this . _elementRef . nativeElement ) . pipe ( takeUntil ( this . _onDestroy ) ) . subscribe ( ( ) => { this . render ( ) ; this . _changeDetector . markForCheck ( ) ; } ) ; } /** Any time an input changes we must re-render the chart */ ngOnChanges ( ) { if ( this . _isInitialized ) { this . render ( ) ; } } ngOnDestroy ( ) { this . _resizeService . removeResizeListener ( this . _elementRef . nativeElement ) ; this . _onDestroy . next ( ) ; this . _onDestroy . complete ( ) ; } /** Inset the content so it never overlaps the arcs */ _getContentInset ( ) { return this . dataset . length * ( this . spacing + this . thickness ) ; } /** Get the dimensions of the content area */ _getContentSize ( ) { return this . _size - ( this . _getContentInset ( ) * 2 ) ; } /** Get the dataset formated in an accessible manner */ _getAriaLabel ( ) { return this . dataset . map ( data => `${ data . value } ${ data . name }` ) . join ( '. ' ) ; } /**
     * Display the tracks and arcs defined by the dataset.
     * We also provide the transition configuration so anytime the dataset
     * changes we will animate the update.
     */ private render ( ) { this . _trackLayer . attr ( 'transform' , `translate(${ this . _radius }, ${ this . _radius })` ) ; this . _arcLayer . attr ( 'transform' , `translate(${ this . _radius }, ${ this . _radius })` ) ; this . _arcs = this . _arcLayer . selectAll ( 'path' ) . data ( this . getChartData ( ) ) ; const arcTransition = transition ( ) . ease ( easeCubic ) . duration ( this . animationDuration ) ; this . _tracks = this . _trackLayer . selectAll ( 'path' ) . data ( this . getChartData ( ) ) . enter ( ) . append ( 'path' ) . attr ( 'class' , 'ux-nested-donut-chart-track' ) ; this . _trackLayer . selectAll ( 'path' ) . attr ( 'd' , this . getTrackArc ( ) ) . style ( 'fill' , ( ) => this . getTrackColor ( ) ) ; this . _tracks . exit ( ) . remove ( ) ; this . _arcs . enter ( ) . append ( 'path' ) . attr ( 'class' , 'ux-nested-donut-chart-arc' ) . style ( 'fill' , data => this . getColor ( data . color ) ) . attr ( 'opacity' , 1 ) . on ( 'click' , data => this . itemClick . emit ( data ) ) . on ( 'mouseenter' , ( data , index , nodes ) => this . onArcMouseEnter ( nodes [ index ] , data ) ) . on ( 'mousemove' , ( ) => this . onArcMouseMove ( mouse ( this . _chartElement . nativeElement ) ) ) . on ( 'mouseleave' , ( _data , index , nodes ) => this . onArcMouseLeave ( nodes [ index ] ) ) . transition ( arcTransition ) . attrTween ( 'd' , this . getArcTween . bind ( this ) ) ; this . _arcs . transition ( arcTransition ) . style ( 'fill' , data => this . getColor ( data . color ) ) . attrTween ( 'd' , this . getArcTween . bind ( this ) ) ; this . _arcs . exit ( ) . transition ( arcTransition ) . attrTween ( 'd' , this . getArcTween . bind ( this ) ) . remove ( ) ; } /** Get the interpolation function based on the new and previous angle */ private getArcTween ( data ) { const interpolation = interpolate ( { ... data , endAngle : data . previousEndAngle } , data ) ; return ( delta ) => this . getArc ( ) ( interpolation ( delta ) ) ; } /** Get the arc layout for a specific item in the dataset */ private getArc ( ) { return arc < NestedDonutChartArc > ( ) . innerRadius ( data => this . getArcRadius ( data . index ) ) . outerRadius ( data => this . getArcRadius ( data . index ) + this . thickness ) . startAngle ( data => data . startAngle ) . endAngle ( data => data . endAngle ) ; } /**
     * Get the track arc layout for a specific item in the dataset.
     * This will match the arc of that represents the actual data
     * however the endAngle will always be a complete circle
     */ private getTrackArc ( ) { return this . getArc ( ) . endAngle ( ( ) => Math . PI * 2 ) ; } /**
     * Get the radius of an arc. This is calculated
     * based on the chart radius that has been defined,
     * minus the thickness defined, then taking into account
     * the depth of the arc and the spacing between each arc.
     */ private getArcRadius ( index ) { return ( this . _radius - this . thickness ) - ( index * ( this . thickness + this . spacing ) ) ; } /**
     * Map the dataset to the NestedDonutChartArc interface
     */ private getChartData ( ) { const dataset = this . dataset . map ( ( data , index ) => { let previousEndAngle = 0 ; if ( this . _arcData && this . _arcData [ index ] ) { previousEndAngle = this . _arcData [ index ] . endAngle ; } return { ... data , index , startAngle : 0 , endAngle : this . getAngle ( data ) , previousEndAngle } ; } ) ; this . _arcData = dataset ; return dataset ; } /** Convert the data value to radians */ private getAngle ( data ) { const fraction = data . value / this . max ; const degrees = fraction * 360.0 ; return degrees * ( Math . PI / 180 ) ; } /**
     * Get the color of the arc, this may be a CSS color value, the name of a color
     * from the color set or a ThemeColor object. We return this as a rgba color to
     * support the alpha channel
     */ private getColor ( color ) { return ThemeColor . isInstanceOf ( color ) ? ( color as ThemeColor ) . toRgba ( ) : this . _colorService . resolve ( color as string ) ; } /** If no track color is specified then default to a specific color based on the active colorset */ private getTrackColor ( ) { if ( this . trackColor ) { return this . getColor ( this . trackColor ) ; } if ( this . _colorService . colorExists ( Color . Grey6 ) ) { return this . getColor ( Color . Grey6 ) ; } if ( this . _colorService . colorExists ( 'bright-gray' ) ) { return this . getColor ( 'bright-gray' ) ; } } /** Define the on hover event */ private onArcMouseEnter ( target , data ) { if ( this . disableHover === false ) { select ( target ) . transition ( ) . duration ( 250 ) . attr ( 'opacity' , 0.5 ) ; } this . _tooltipContext = { ... data , color : this . getColor ( data . color ) } ; this . _tooltipVisible = true ; this . _changeDetector . detectChanges ( ) ; } /** Update the tooltip position on mouse move */ private onArcMouseMove ( [ x , y ] ) { this . _tooltipX = x ; this . _tooltipY = y - 2 ; this . _changeDetector . detectChanges ( ) ; } /** Define the on hover out event */ private onArcMouseLeave ( target ) { if ( this . disableHover === false ) { select ( target ) . transition ( ) . duration ( 250 ) . attr ( 'opacity' , 1 ) ; } this . _tooltipContext = null ; this . _tooltipVisible = false ; this . _changeDetector . detectChanges ( ) ; } } export interface NestedDonutChartData { name : string ; value : number ; color : ThemeColor | string ; data ? : { [ key ] : any ; } ; } export interface NestedDonutChartArc extends NestedDonutChartData { index : number ; startAngle : number ; endAngle : number ; previousEndAngle : number ; }