diff --git a/apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.component.html b/apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.component.html deleted file mode 100644 index 8e86acd33..000000000 --- a/apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.component.html +++ /dev/null @@ -1,12 +0,0 @@ - - diff --git a/apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.component.scss b/apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.component.scss deleted file mode 100644 index 5d4e87f30..000000000 --- a/apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -:host { - display: block; -} diff --git a/apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.component.ts deleted file mode 100644 index e98d594a1..000000000 --- a/apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.component.ts +++ /dev/null @@ -1,322 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - Input, - OnChanges, - OnDestroy, - OnInit, - ViewChild -} from '@angular/core'; -import { UNKNOWN_KEY } from '@ghostfolio/common/config'; -import { getTextColor } from '@ghostfolio/common/helper'; -import { PortfolioPosition } from '@ghostfolio/common/interfaces'; -import { Currency } from '@prisma/client'; -import { Tooltip } from 'chart.js'; -import { LinearScale } from 'chart.js'; -import { ArcElement } from 'chart.js'; -import { DoughnutController } from 'chart.js'; -import { Chart } from 'chart.js'; -import ChartDataLabels from 'chartjs-plugin-datalabels'; -import * as Color from 'color'; - -@Component({ - selector: 'gf-portfolio-proportion-chart', - changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: './portfolio-proportion-chart.component.html', - styleUrls: ['./portfolio-proportion-chart.component.scss'] -}) -export class PortfolioProportionChartComponent - implements OnChanges, OnDestroy, OnInit -{ - @Input() baseCurrency: Currency; - @Input() isInPercent: boolean; - @Input() keys: string[]; - @Input() locale: string; - @Input() maxItems?: number; - @Input() showLabels = false; - @Input() positions: { - [symbol: string]: Pick & { value: number }; - }; - - @ViewChild('chartCanvas') chartCanvas; - - public chart: Chart; - public isLoading = true; - - private colorMap: { - [symbol: string]: string; - } = { - [UNKNOWN_KEY]: `rgba(${getTextColor()}, 0.12)` - }; - - public constructor() { - Chart.register( - ArcElement, - ChartDataLabels, - DoughnutController, - LinearScale, - Tooltip - ); - } - - public ngOnInit() {} - - public ngOnChanges() { - if (this.positions) { - this.initialize(); - } - } - - public ngOnDestroy() { - this.chart?.destroy(); - } - - private initialize() { - this.isLoading = true; - const chartData: { - [symbol: string]: { - color?: string; - subCategory: { [symbol: string]: { value: number } }; - value: number; - }; - } = {}; - - Object.keys(this.positions).forEach((symbol) => { - if (this.positions[symbol][this.keys[0]]) { - if (chartData[this.positions[symbol][this.keys[0]]]) { - chartData[this.positions[symbol][this.keys[0]]].value += - this.positions[symbol].value; - - if ( - chartData[this.positions[symbol][this.keys[0]]].subCategory[ - this.positions[symbol][this.keys[1]] - ] - ) { - chartData[this.positions[symbol][this.keys[0]]].subCategory[ - this.positions[symbol][this.keys[1]] - ].value += this.positions[symbol].value; - } else { - chartData[this.positions[symbol][this.keys[0]]].subCategory[ - this.positions[symbol][this.keys[1]] ?? UNKNOWN_KEY - ] = { value: this.positions[symbol].value }; - } - } else { - chartData[this.positions[symbol][this.keys[0]]] = { - subCategory: {}, - value: this.positions[symbol].value - }; - - if (this.positions[symbol][this.keys[1]]) { - chartData[this.positions[symbol][this.keys[0]]].subCategory = { - [this.positions[symbol][this.keys[1]]]: { - value: this.positions[symbol].value - } - }; - } - } - } else { - if (chartData[UNKNOWN_KEY]) { - chartData[UNKNOWN_KEY].value += this.positions[symbol].value; - } else { - chartData[UNKNOWN_KEY] = { - subCategory: this.keys[1] - ? { [this.keys[1]]: { value: 0 } } - : undefined, - value: this.positions[symbol].value - }; - } - } - }); - - let chartDataSorted = Object.entries(chartData) - .sort((a, b) => { - return a[1].value - b[1].value; - }) - .reverse(); - - if (this.maxItems && chartDataSorted.length > this.maxItems) { - // Add surplus items to unknown group - const rest = chartDataSorted.splice( - this.maxItems, - chartDataSorted.length - 1 - ); - - let unknownItem = chartDataSorted.find((charDataItem) => { - return charDataItem[0] === UNKNOWN_KEY; - }); - - if (!unknownItem) { - const index = chartDataSorted.push([ - UNKNOWN_KEY, - { subCategory: {}, value: 0 } - ]); - unknownItem = chartDataSorted[index]; - } - - rest.forEach((restItem) => { - if (unknownItem?.[1]) { - unknownItem[1] = { - subCategory: {}, - value: unknownItem[1].value + restItem[1].value - }; - } - }); - - // Sort data again - chartDataSorted = chartDataSorted - .sort((a, b) => { - return a[1].value - b[1].value; - }) - .reverse(); - } - - chartDataSorted.forEach(([symbol, item], index) => { - if (this.colorMap[symbol]) { - // Reuse color - item.color = this.colorMap[symbol]; - } else { - const color = - this.getColorPalette()[index % this.getColorPalette().length]; - - // Store color for reuse - this.colorMap[symbol] = color; - - item.color = color; - } - }); - - const backgroundColorSubCategory: string[] = []; - const dataSubCategory: number[] = []; - const labelSubCategory: string[] = []; - - chartDataSorted.forEach(([, item]) => { - let lightnessRatio = 0.2; - - Object.keys(item.subCategory).forEach((subCategory) => { - backgroundColorSubCategory.push( - Color(item.color).lighten(lightnessRatio).hex() - ); - dataSubCategory.push(item.subCategory[subCategory].value); - labelSubCategory.push(subCategory); - - lightnessRatio += 0.1; - }); - }); - - const datasets = [ - { - backgroundColor: chartDataSorted.map(([, item]) => { - return item.color; - }), - borderWidth: 0, - data: chartDataSorted.map(([, item]) => { - return item.value; - }) - } - ]; - - let labels = chartDataSorted.map(([label]) => { - return label; - }); - - if (this.keys[1]) { - datasets.unshift({ - backgroundColor: backgroundColorSubCategory, - borderWidth: 0, - data: dataSubCategory - }); - - labels = labelSubCategory.concat(labels); - } - - const data = { - datasets, - labels - }; - - if (this.chartCanvas) { - if (this.chart) { - this.chart.data = data; - this.chart.update(); - } else { - this.chart = new Chart(this.chartCanvas.nativeElement, { - data, - options: { - cutout: '70%', - layout: { - padding: this.showLabels === true ? 100 : 0 - }, - plugins: { - datalabels: { - color: (context) => { - return this.getColorPalette()[ - context.dataIndex % this.getColorPalette().length - ]; - }, - display: this.showLabels === true ? 'auto' : false, - labels: { - index: { - align: 'end', - anchor: 'end', - formatter: (value, context) => { - return value > 0 - ? context.chart.data.labels[context.dataIndex] - : ''; - }, - offset: 8 - } - } - }, - legend: { display: false }, - tooltip: { - callbacks: { - label: (context) => { - const labelIndex = - (data.datasets[context.datasetIndex - 1]?.data?.length ?? - 0) + context.dataIndex; - const label = context.chart.data.labels[labelIndex]; - - if (this.isInPercent) { - const value = 100 * context.raw; - return `${label} (${value.toFixed(2)}%)`; - } else { - const value = context.raw; - return `${label} (${value.toLocaleString(this.locale, { - maximumFractionDigits: 2, - minimumFractionDigits: 2 - })} ${this.baseCurrency})`; - } - } - } - } - } - }, - type: 'doughnut' - }); - } - - this.isLoading = false; - } - } - - /** - * Color palette, inspired by https://yeun.github.io/open-color - */ - private getColorPalette() { - // - return [ - '#329af0', // blue 5 - '#20c997', // teal 5 - '#94d82d', // lime 5 - '#ff922b', // orange 5 - '#f06595', // pink 5 - '#845ef7', // violet 5 - '#5c7cfa', // indigo 5 - '#22b8cf', // cyan 5 - '#51cf66', // green 5 - '#fcc419', // yellow 5 - '#ff6b6b', // red 5 - '#cc5de8' // grape 5 - ]; - } -} diff --git a/apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.module.ts b/apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.module.ts deleted file mode 100644 index 9472b1cfa..000000000 --- a/apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; - -import { PortfolioProportionChartComponent } from './portfolio-proportion-chart.component'; - -@NgModule({ - declarations: [PortfolioProportionChartComponent], - exports: [PortfolioProportionChartComponent], - imports: [CommonModule, NgxSkeletonLoaderModule], - providers: [] -}) -export class GfPortfolioProportionChartModule {} diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts index 26ef795ff..809b29bd7 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts @@ -1,10 +1,10 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; -import { GfPortfolioProportionChartModule } from '@ghostfolio/client/components/portfolio-proportion-chart/portfolio-proportion-chart.module'; import { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module'; import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module'; import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module'; +import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module'; import { AllocationsPageRoutingModule } from './allocations-page-routing.module'; import { AllocationsPageComponent } from './allocations-page.component'; diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index cfbbcd603..572724621 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -123,7 +123,7 @@ export function resolveFearAndGreedIndex(aValue: number) { return { emoji: '😐', text: 'Neutral' }; } else if (aValue < 75) { return { emoji: '😜', text: 'Greed' }; - } else if (aValue >= 75) { + } else { return { emoji: 'ðŸĪŠ', text: 'Extreme Greed' }; } } diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.stories.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.stories.ts index 3ed928745..6d541c911 100644 --- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.stories.ts +++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.stories.ts @@ -1,14 +1,17 @@ +import { CommonModule } from '@angular/common'; import { Meta, Story, moduleMetadata } from '@storybook/angular'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { PortfolioProportionChartComponent } from './portfolio-proportion-chart.component'; +import { Currency } from '.prisma/client'; export default { - title: 'Value', + title: 'Portfolio Proportion Chart', component: PortfolioProportionChartComponent, decorators: [ moduleMetadata({ - imports: [NgxSkeletonLoaderModule] + declarations: [PortfolioProportionChartComponent], + imports: [CommonModule, NgxSkeletonLoaderModule] }) ] } as Meta; @@ -19,5 +22,17 @@ const Template: Story = ( props: args }); -export const Loading = Template.bind({}); -Loading.args = {}; +export const Simple = Template.bind({}); +Simple.args = { + baseCurrency: Currency.USD, + keys: ['name'], + locale: 'en-US', + positions: { + Africa: { name: 'Africa', value: 983.22461479889288 }, + Asia: { name: 'Asia', value: 12074.754633964973 }, + Europe: { name: 'Europe', value: 34432.837085290535 }, + 'North America': { name: 'North America', value: 26539.89987780503 }, + Oceania: { name: 'Oceania', value: 1402.220605072031 }, + 'South America': { name: 'South America', value: 4938.25202180719859 } + } +}; diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts index 261c97d08..5001fc758 100644 --- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts +++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -1,10 +1,11 @@ import { + AfterViewInit, ChangeDetectionStrategy, Component, + ElementRef, Input, OnChanges, OnDestroy, - OnInit, ViewChild } from '@angular/core'; import { UNKNOWN_KEY } from '@ghostfolio/common/config'; @@ -25,7 +26,9 @@ import * as Color from 'color'; templateUrl: './portfolio-proportion-chart.component.html', styleUrls: ['./portfolio-proportion-chart.component.scss'] }) -export class PortfolioProportionChartComponent implements OnChanges, OnDestroy { +export class PortfolioProportionChartComponent + implements AfterViewInit, OnChanges, OnDestroy +{ @Input() baseCurrency: Currency; @Input() isInPercent = false; @Input() keys: string[] = []; @@ -36,7 +39,7 @@ export class PortfolioProportionChartComponent implements OnChanges, OnDestroy { [symbol: string]: Pick & { value: number }; } = {}; - @ViewChild('chartCanvas') chartCanvas; + @ViewChild('chartCanvas') chartCanvas: ElementRef; public chart: Chart; public isLoading = true; @@ -57,6 +60,12 @@ export class PortfolioProportionChartComponent implements OnChanges, OnDestroy { ); } + public ngAfterViewInit() { + if (this.positions) { + this.initialize(); + } + } + public ngOnChanges() { if (this.positions) { this.initialize(); diff --git a/libs/ui/tsconfig.json b/libs/ui/tsconfig.json index 93b76eee5..849a0b361 100644 --- a/libs/ui/tsconfig.json +++ b/libs/ui/tsconfig.json @@ -15,7 +15,7 @@ ], "compilerOptions": { "forceConsistentCasingInFileNames": true, - "strict": true, + "strict": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true },