Browse Source

Add story

pull/384/head
Thomas 4 years ago
parent
commit
55e8340d73
  1. 12
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.html
  2. 3
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.scss
  3. 23
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.stories.ts
  4. 318
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  5. 13
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.module.ts

12
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.html

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

3
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.scss

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

23
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.stories.ts

@ -0,0 +1,23 @@
import { Meta, Story, moduleMetadata } from '@storybook/angular';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { PortfolioProportionChartComponent } from './portfolio-proportion-chart.component';
export default {
title: 'Value',
component: PortfolioProportionChartComponent,
decorators: [
moduleMetadata({
imports: [NgxSkeletonLoaderModule]
})
]
} as Meta<PortfolioProportionChartComponent>;
const Template: Story<PortfolioProportionChartComponent> = (
args: PortfolioProportionChartComponent
) => ({
props: args
});
export const Loading = Template.bind({});
Loading.args = {};

318
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts

@ -0,0 +1,318 @@
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 {
@Input() baseCurrency: Currency;
@Input() isInPercent = false;
@Input() keys: string[] = [];
@Input() locale = '';
@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 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
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.module.ts

@ -0,0 +1,13 @@
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 {}
Loading…
Cancel
Save