Browse Source

Use new component

pull/384/head
Thomas 4 years ago
parent
commit
eeb8ef737f
  1. 12
      apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.component.html
  2. 3
      apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.component.scss
  3. 322
      apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  4. 13
      apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.module.ts
  5. 2
      apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts
  6. 2
      libs/common/src/lib/helper.ts
  7. 23
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.stories.ts
  8. 15
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  9. 2
      libs/ui/tsconfig.json

12
apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.component.html

@ -1,12 +0,0 @@
<ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse"
[theme]="{
height: '15rem',
width: '100%'
}"
></ngx-skeleton-loader>
<canvas
#chartCanvas
[ngStyle]="{ display: isLoading ? 'none' : 'block' }"
></canvas>

3
apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.component.scss

@ -1,3 +0,0 @@
:host {
display: block;
}

322
apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.component.ts

@ -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<PortfolioPosition, 'type'> & { 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 * <number>context.raw;
return `${label} (${value.toFixed(2)}%)`;
} else {
const value = <number>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
];
}
}

13
apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.module.ts

@ -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 {}

2
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';

2
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' };
}
}

23
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<PortfolioProportionChartComponent>;
@ -19,5 +22,17 @@ const Template: Story<PortfolioProportionChartComponent> = (
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 }
}
};

15
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<PortfolioPosition, 'type'> & { value: number };
} = {};
@ViewChild('chartCanvas') chartCanvas;
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
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();

2
libs/ui/tsconfig.json

@ -15,7 +15,7 @@
],
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"strict": true,
"strict": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},

Loading…
Cancel
Save