mirror of https://github.com/ghostfolio/ghostfolio
				
				
			
				 17 changed files with 453 additions and 60 deletions
			
			
		@ -0,0 +1,39 @@ | 
				
			|||
<div class="align-items-center d-flex mb-4"> | 
				
			|||
  <div class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate"> | 
				
			|||
    <span i18n>Benchmarks</span> | 
				
			|||
    <sup i18n>Beta</sup> | 
				
			|||
    <gf-premium-indicator | 
				
			|||
      *ngIf="user?.subscription?.type === 'Basic'" | 
				
			|||
      class="ml-1" | 
				
			|||
    ></gf-premium-indicator> | 
				
			|||
  </div> | 
				
			|||
  <div> | 
				
			|||
    <mat-form-field appearance="outline" class="w-100"> | 
				
			|||
      <mat-label i18n>Compare with...</mat-label> | 
				
			|||
      <mat-select | 
				
			|||
        name="benchmark" | 
				
			|||
        [value]="value" | 
				
			|||
        (selectionChange)="onChangeBenchmark($event.value)" | 
				
			|||
      > | 
				
			|||
        <mat-option *ngFor="let benchmark of benchmarks" [value]="benchmark">{{ | 
				
			|||
          benchmark.symbol | 
				
			|||
        }}</mat-option> | 
				
			|||
      </mat-select> | 
				
			|||
    </mat-form-field> | 
				
			|||
  </div> | 
				
			|||
</div> | 
				
			|||
<div class="chart-container"> | 
				
			|||
  <ngx-skeleton-loader | 
				
			|||
    *ngIf="isLoading" | 
				
			|||
    animation="pulse" | 
				
			|||
    [theme]="{ | 
				
			|||
      height: '100%', | 
				
			|||
      width: '100%' | 
				
			|||
    }" | 
				
			|||
  ></ngx-skeleton-loader> | 
				
			|||
  <canvas | 
				
			|||
    #chartCanvas | 
				
			|||
    class="h-100" | 
				
			|||
    [ngStyle]="{ display: isLoading ? 'none' : 'block' }" | 
				
			|||
  ></canvas> | 
				
			|||
</div> | 
				
			|||
@ -0,0 +1,11 @@ | 
				
			|||
:host { | 
				
			|||
  display: block; | 
				
			|||
 | 
				
			|||
  .chart-container { | 
				
			|||
    aspect-ratio: 16 / 9; | 
				
			|||
 | 
				
			|||
    ngx-skeleton-loader { | 
				
			|||
      height: 100%; | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
} | 
				
			|||
@ -0,0 +1,261 @@ | 
				
			|||
import 'chartjs-adapter-date-fns'; | 
				
			|||
 | 
				
			|||
import { | 
				
			|||
  ChangeDetectionStrategy, | 
				
			|||
  Component, | 
				
			|||
  Input, | 
				
			|||
  OnChanges, | 
				
			|||
  OnDestroy, | 
				
			|||
  ViewChild | 
				
			|||
} from '@angular/core'; | 
				
			|||
import { | 
				
			|||
  getTooltipOptions, | 
				
			|||
  getTooltipPositionerMapTop, | 
				
			|||
  getVerticalHoverLinePlugin | 
				
			|||
} from '@ghostfolio/common/chart-helper'; | 
				
			|||
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; | 
				
			|||
import { | 
				
			|||
  getBackgroundColor, | 
				
			|||
  getDateFormatString, | 
				
			|||
  getTextColor, | 
				
			|||
  parseDate, | 
				
			|||
  transformTickToAbbreviation | 
				
			|||
} from '@ghostfolio/common/helper'; | 
				
			|||
import { UniqueAsset, User } from '@ghostfolio/common/interfaces'; | 
				
			|||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; | 
				
			|||
import { | 
				
			|||
  Chart, | 
				
			|||
  LineController, | 
				
			|||
  LineElement, | 
				
			|||
  LinearScale, | 
				
			|||
  PointElement, | 
				
			|||
  TimeScale, | 
				
			|||
  Tooltip | 
				
			|||
} from 'chart.js'; | 
				
			|||
import annotationPlugin from 'chartjs-plugin-annotation'; | 
				
			|||
import { addDays, isAfter, parseISO, subDays } from 'date-fns'; | 
				
			|||
 | 
				
			|||
@Component({ | 
				
			|||
  selector: 'gf-benchmark-comparator', | 
				
			|||
  changeDetection: ChangeDetectionStrategy.OnPush, | 
				
			|||
  templateUrl: './benchmark-comparator.component.html', | 
				
			|||
  styleUrls: ['./benchmark-comparator.component.scss'] | 
				
			|||
}) | 
				
			|||
export class BenchmarkComparatorComponent implements OnChanges, OnDestroy { | 
				
			|||
  @Input() benchmarks: UniqueAsset[]; | 
				
			|||
  @Input() currency: string; | 
				
			|||
  @Input() daysInMarket: number; | 
				
			|||
  @Input() investments: InvestmentItem[]; | 
				
			|||
  @Input() isInPercent = false; | 
				
			|||
  @Input() locale: string; | 
				
			|||
  @Input() user: User; | 
				
			|||
 | 
				
			|||
  @ViewChild('chartCanvas') chartCanvas; | 
				
			|||
 | 
				
			|||
  public chart: Chart; | 
				
			|||
  public isLoading = true; | 
				
			|||
  public value; | 
				
			|||
 | 
				
			|||
  private data: InvestmentItem[]; | 
				
			|||
 | 
				
			|||
  public constructor() { | 
				
			|||
    Chart.register( | 
				
			|||
      annotationPlugin, | 
				
			|||
      LinearScale, | 
				
			|||
      LineController, | 
				
			|||
      LineElement, | 
				
			|||
      PointElement, | 
				
			|||
      TimeScale, | 
				
			|||
      Tooltip | 
				
			|||
    ); | 
				
			|||
 | 
				
			|||
    Tooltip.positioners['top'] = (elements, position) => | 
				
			|||
      getTooltipPositionerMapTop(this.chart, position); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public ngOnChanges() { | 
				
			|||
    if (this.investments) { | 
				
			|||
      this.initialize(); | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public onChangeBenchmark(aBenchmark: any) { | 
				
			|||
    console.log(aBenchmark); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public ngOnDestroy() { | 
				
			|||
    this.chart?.destroy(); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  private initialize() { | 
				
			|||
    this.isLoading = true; | 
				
			|||
 | 
				
			|||
    // Create a clone
 | 
				
			|||
    this.data = this.investments.map((a) => Object.assign({}, a)); | 
				
			|||
 | 
				
			|||
    if (this.data?.length > 0) { | 
				
			|||
      // Extend chart by 5% of days in market (before)
 | 
				
			|||
      const firstItem = this.data[0]; | 
				
			|||
      this.data.unshift({ | 
				
			|||
        ...firstItem, | 
				
			|||
        date: subDays( | 
				
			|||
          parseISO(firstItem.date), | 
				
			|||
          this.daysInMarket * 0.05 || 90 | 
				
			|||
        ).toISOString(), | 
				
			|||
        investment: 0 | 
				
			|||
      }); | 
				
			|||
 | 
				
			|||
      // Extend chart by 5% of days in market (after)
 | 
				
			|||
      const lastItem = this.data[this.data.length - 1]; | 
				
			|||
      this.data.push({ | 
				
			|||
        ...lastItem, | 
				
			|||
        date: addDays( | 
				
			|||
          parseDate(lastItem.date), | 
				
			|||
          this.daysInMarket * 0.05 || 90 | 
				
			|||
        ).toISOString() | 
				
			|||
      }); | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    const data = { | 
				
			|||
      labels: this.data.map((investmentItem) => { | 
				
			|||
        return investmentItem.date; | 
				
			|||
      }), | 
				
			|||
      datasets: [ | 
				
			|||
        { | 
				
			|||
          backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, | 
				
			|||
          borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, | 
				
			|||
          borderWidth: 2, | 
				
			|||
          data: this.data.map((position) => { | 
				
			|||
            return position.investment; | 
				
			|||
          }), | 
				
			|||
          label: $localize`Deposit`, | 
				
			|||
          segment: { | 
				
			|||
            borderColor: (context: unknown) => | 
				
			|||
              this.isInFuture( | 
				
			|||
                context, | 
				
			|||
                `rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.67)` | 
				
			|||
              ), | 
				
			|||
            borderDash: (context: unknown) => this.isInFuture(context, [2, 2]) | 
				
			|||
          }, | 
				
			|||
          stepped: true | 
				
			|||
        }, | 
				
			|||
        { | 
				
			|||
          backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`, | 
				
			|||
          borderColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`, | 
				
			|||
          borderWidth: 2, | 
				
			|||
          data: this.data.map((position) => { | 
				
			|||
            return position.investment * 1.75; | 
				
			|||
          }), | 
				
			|||
          label: $localize`Benchmark` | 
				
			|||
        } | 
				
			|||
      ] | 
				
			|||
    }; | 
				
			|||
 | 
				
			|||
    if (this.chartCanvas) { | 
				
			|||
      if (this.chart) { | 
				
			|||
        this.chart.data = data; | 
				
			|||
        this.chart.options.plugins.tooltip = <unknown>( | 
				
			|||
          this.getTooltipPluginConfiguration() | 
				
			|||
        ); | 
				
			|||
        this.chart.update(); | 
				
			|||
      } else { | 
				
			|||
        this.chart = new Chart(this.chartCanvas.nativeElement, { | 
				
			|||
          data, | 
				
			|||
          options: { | 
				
			|||
            animation: false, | 
				
			|||
            elements: { | 
				
			|||
              line: { | 
				
			|||
                tension: 0 | 
				
			|||
              }, | 
				
			|||
              point: { | 
				
			|||
                hoverBackgroundColor: getBackgroundColor(), | 
				
			|||
                hoverRadius: 2, | 
				
			|||
                radius: 0 | 
				
			|||
              } | 
				
			|||
            }, | 
				
			|||
            interaction: { intersect: false, mode: 'index' }, | 
				
			|||
            maintainAspectRatio: true, | 
				
			|||
            plugins: <unknown>{ | 
				
			|||
              annotation: { | 
				
			|||
                annotations: { | 
				
			|||
                  yAxis: { | 
				
			|||
                    borderColor: `rgba(${getTextColor()}, 0.1)`, | 
				
			|||
                    borderWidth: 1, | 
				
			|||
                    scaleID: 'y', | 
				
			|||
                    type: 'line', | 
				
			|||
                    value: 0 | 
				
			|||
                  } | 
				
			|||
                } | 
				
			|||
              }, | 
				
			|||
              legend: { | 
				
			|||
                display: false | 
				
			|||
              }, | 
				
			|||
              tooltip: this.getTooltipPluginConfiguration(), | 
				
			|||
              verticalHoverLine: { | 
				
			|||
                color: `rgba(${getTextColor()}, 0.1)` | 
				
			|||
              } | 
				
			|||
            }, | 
				
			|||
            responsive: true, | 
				
			|||
            scales: { | 
				
			|||
              x: { | 
				
			|||
                display: true, | 
				
			|||
                grid: { | 
				
			|||
                  borderColor: `rgba(${getTextColor()}, 0.1)`, | 
				
			|||
                  borderWidth: 1, | 
				
			|||
                  color: `rgba(${getTextColor()}, 0.8)`, | 
				
			|||
                  display: false | 
				
			|||
                }, | 
				
			|||
                type: 'time', | 
				
			|||
                time: { | 
				
			|||
                  tooltipFormat: getDateFormatString(this.locale), | 
				
			|||
                  unit: 'year' | 
				
			|||
                } | 
				
			|||
              }, | 
				
			|||
              y: { | 
				
			|||
                display: !this.isInPercent, | 
				
			|||
                grid: { | 
				
			|||
                  borderColor: `rgba(${getTextColor()}, 0.1)`, | 
				
			|||
                  color: `rgba(${getTextColor()}, 0.8)`, | 
				
			|||
                  display: false, | 
				
			|||
                  drawBorder: false | 
				
			|||
                }, | 
				
			|||
                position: 'right', | 
				
			|||
                ticks: { | 
				
			|||
                  callback: (value: number) => { | 
				
			|||
                    return transformTickToAbbreviation(value); | 
				
			|||
                  }, | 
				
			|||
                  display: true, | 
				
			|||
                  mirror: true, | 
				
			|||
                  z: 1 | 
				
			|||
                } | 
				
			|||
              } | 
				
			|||
            } | 
				
			|||
          }, | 
				
			|||
          plugins: [getVerticalHoverLinePlugin(this.chartCanvas)], | 
				
			|||
          type: 'line' | 
				
			|||
        }); | 
				
			|||
      } | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    this.isLoading = false; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  private getTooltipPluginConfiguration() { | 
				
			|||
    return { | 
				
			|||
      ...getTooltipOptions({ | 
				
			|||
        locale: this.isInPercent ? undefined : this.locale, | 
				
			|||
        unit: this.isInPercent ? undefined : this.currency | 
				
			|||
      }), | 
				
			|||
      mode: 'index', | 
				
			|||
      position: <unknown>'top', | 
				
			|||
      xAlign: 'center', | 
				
			|||
      yAlign: 'bottom' | 
				
			|||
    }; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  private isInFuture<T>(aContext: any, aValue: T) { | 
				
			|||
    return isAfter(new Date(aContext?.p1?.parsed?.x), new Date()) | 
				
			|||
      ? aValue | 
				
			|||
      : undefined; | 
				
			|||
  } | 
				
			|||
} | 
				
			|||
@ -0,0 +1,20 @@ | 
				
			|||
import { CommonModule } from '@angular/common'; | 
				
			|||
import { NgModule } from '@angular/core'; | 
				
			|||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; | 
				
			|||
import { MatSelectModule } from '@angular/material/select'; | 
				
			|||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; | 
				
			|||
 | 
				
			|||
import { BenchmarkComparatorComponent } from './benchmark-comparator.component'; | 
				
			|||
 | 
				
			|||
@NgModule({ | 
				
			|||
  declarations: [BenchmarkComparatorComponent], | 
				
			|||
  exports: [BenchmarkComparatorComponent], | 
				
			|||
  imports: [ | 
				
			|||
    CommonModule, | 
				
			|||
    FormsModule, | 
				
			|||
    MatSelectModule, | 
				
			|||
    NgxSkeletonLoaderModule, | 
				
			|||
    ReactiveFormsModule | 
				
			|||
  ] | 
				
			|||
}) | 
				
			|||
export class GfBenchmarkComparatorModule {} | 
				
			|||
					Loading…
					
					
				
		Reference in new issue