From f4f11a8a05171a174e891dbae7db69e87f3ba0bf Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Nakazone Batista Date: Thu, 28 Dec 2023 22:34:36 -0300 Subject: [PATCH] Initial Language Graph Implementation --- package-lock.json | 118 ++++++++++++++++++ package.json | 2 + .../project-card/project-card.component.css | 26 +++- .../project-card/project-card.component.html | 14 ++- .../project-card/project-card.component.ts | 70 ++++++++++- src/app/projects/projects.module.ts | 4 +- .../shared/github-service/github.service.ts | 27 +++- src/app/shared/model/project/project.model.ts | 1 + src/assets/env.sample.js | 1 + 9 files changed, 249 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index c9e31e2..b343608 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,12 +25,14 @@ "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@glidejs/glide": "^3.6.0", + "apexcharts": "^3.45.1", "bootstrap": "^4.6.2", "cookieconsent": "^3.1.1", "cors": "^2.8.5", "envsub": "^4.1.0", "express": "^4.18.1", "jquery": "^3.6.0", + "ng-apexcharts": "^1.8.0", "ngx-cookie-service": "^16.0.1", "ngx-cookieconsent": "^4.0.2", "ngx-glide": "^16.0.0", @@ -5440,6 +5442,11 @@ "node": ">=14.15.0" } }, + "node_modules/@yr/monotone-cubic-spline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==" + }, "node_modules/@zkochan/js-yaml": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz", @@ -5722,6 +5729,20 @@ "node": ">= 8" } }, + "node_modules/apexcharts": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.45.1.tgz", + "integrity": "sha512-pPjj/SA6dfPvR/IKRZF0STdfBGpBh3WRt7K0DFuW9P8erypYkX17EHu3/molPRfo2zSiQwTVpshHC5ncysqfkA==", + "dependencies": { + "@yr/monotone-cubic-spline": "^1.0.3", + "svg.draggable.js": "^2.2.2", + "svg.easing.js": "^2.0.0", + "svg.filter.js": "^2.0.2", + "svg.pathmorphing.js": "^0.1.3", + "svg.resize.js": "^1.4.3", + "svg.select.js": "^3.0.1" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -12063,6 +12084,20 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/ng-apexcharts": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ng-apexcharts/-/ng-apexcharts-1.8.0.tgz", + "integrity": "sha512-NwJuMLHoLm52LSzM08RXV6oOOTyUYREAV53WHVGs+L2qi8UWbxCz19hX0kk+F/xFLEhhuiLegO3T1v30jLbKSQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@angular/core": ">=13.0.0", + "apexcharts": "^3.41.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/ngx-cookie-service": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-16.0.1.tgz", @@ -15233,6 +15268,89 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg.draggable.js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "dependencies": { + "svg.js": "^2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.easing.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", + "dependencies": { + "svg.js": ">=2.3.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.filter.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==" + }, + "node_modules/svg.pathmorphing.js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "dependencies": { + "svg.js": "^2.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "dependencies": { + "svg.js": "^2.6.5", + "svg.select.js": "^2.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js/node_modules/svg.select.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.select.js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "dependencies": { + "svg.js": "^2.6.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", diff --git a/package.json b/package.json index 0b5c8bd..2646c9c 100644 --- a/package.json +++ b/package.json @@ -33,12 +33,14 @@ "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@glidejs/glide": "^3.6.0", + "apexcharts": "^3.45.1", "bootstrap": "^4.6.2", "cookieconsent": "^3.1.1", "cors": "^2.8.5", "envsub": "^4.1.0", "express": "^4.18.1", "jquery": "^3.6.0", + "ng-apexcharts": "^1.8.0", "ngx-cookie-service": "^16.0.1", "ngx-cookieconsent": "^4.0.2", "ngx-glide": "^16.0.0", diff --git a/src/app/projects/project-card/project-card.component.css b/src/app/projects/project-card/project-card.component.css index 12d44ca..baeaaeb 100644 --- a/src/app/projects/project-card/project-card.component.css +++ b/src/app/projects/project-card/project-card.component.css @@ -3,7 +3,6 @@ } .card .card-content { - height: 350px; width: 100%; padding: 40px; display: flex; @@ -34,13 +33,9 @@ justify-content: center; flex-direction: row; } -.card .card-content .card-content-f .card-info { - display: flex; - flex-direction: column; -} .card .card-content .card-content-f .card-stats { display: flex; - flex-direction: column; + flex-direction: row; justify-content: space-around; } @@ -72,3 +67,22 @@ width: 100%; flex-direction: row !important; } + +.card-languages { + padding: 0; + margin-top: 25px; + margin-bottom: 25px; +} + +/* COMPUTER FORMAT */ +@media only screen and (min-width: 712px) { + .card-languages { + margin: 0; + } + + .card .card-content .card-content-f .card-stats { + display: flex; + flex-direction: column; + justify-content: space-around; + } +} diff --git a/src/app/projects/project-card/project-card.component.html b/src/app/projects/project-card/project-card.component.html index 02af6f7..5877570 100644 --- a/src/app/projects/project-card/project-card.component.html +++ b/src/app/projects/project-card/project-card.component.html @@ -7,10 +7,18 @@

{{project.description}}

-
-

Language: {{project.languages}}

+
+ +
-
+
diff --git a/src/app/projects/project-card/project-card.component.ts b/src/app/projects/project-card/project-card.component.ts index eb7f79d..9f32f85 100644 --- a/src/app/projects/project-card/project-card.component.ts +++ b/src/app/projects/project-card/project-card.component.ts @@ -1,18 +1,42 @@ -import {Component, Input} from '@angular/core'; +import {Component, Input, OnInit, ViewChild} from '@angular/core'; import { faCodeFork, faEye, faStar } from '@fortawesome/free-solid-svg-icons'; -import {Project} from "../../shared/model/project/project.model"; +import {Language, Project} from "../../shared/model/project/project.model"; import {faScaleBalanced} from "@fortawesome/free-solid-svg-icons/faScaleBalanced"; +import { + ApexAnnotations, + ApexChart, ApexDataLabels, + ApexNonAxisChartSeries, + ApexPlotOptions, + ApexResponsive, + ChartComponent +} from "ng-apexcharts"; + + +export type ChartOptions = { + series: ApexNonAxisChartSeries; + colors: string[]; + chart: ApexChart; + responsive: ApexResponsive[]; + labels: string[]; + plotOptions: ApexPlotOptions; + dataLabels: ApexDataLabels; +}; @Component({ selector: 'app-project-card', templateUrl: './project-card.component.html', styleUrls: ['./project-card.component.css'] }) -export class ProjectCardComponent { +export class ProjectCardComponent implements OnInit { @Input() inverted: boolean = false; @Input() project!: Project; + @ViewChild('language-chart') + languageChart: ChartComponent | undefined; + + chartOptions: ChartOptions | undefined; + // Stats Icons Definitions faLicense = faScaleBalanced; @@ -22,6 +46,13 @@ export class ProjectCardComponent { faEye = faEye; + ngOnInit() { + if (!!this.project.languages) { + this.chartOptions = this.generateChart(this.project.languages); + } + + } + get hasLicense(): boolean { return this.project.license !== undefined; } @@ -30,4 +61,37 @@ export class ProjectCardComponent { return this.project.languages !== undefined && this.project.languages?.length > 0; } + + private generateChart(languages: Language[]): ChartOptions { + return { + series: languages.map(value => value.percentage), + colors: languages.map(value => value.color), + chart: { + width: 380, + type: "pie" + }, + labels: languages.map(value => value.name), + responsive: [ + { + breakpoint: 480, + options: { + chart: { + width: 300 + }, + legend: { + position: "bottom" + } + } + } + ], + plotOptions: { + pie: { + expandOnClick: true, + } + }, + dataLabels: { + enabled: false + } + }; + } } diff --git a/src/app/projects/projects.module.ts b/src/app/projects/projects.module.ts index 665f97d..25c2b8f 100644 --- a/src/app/projects/projects.module.ts +++ b/src/app/projects/projects.module.ts @@ -4,6 +4,7 @@ import {CommonModule, NgOptimizedImage} from "@angular/common"; import { ProjectCardComponent } from './project-card/project-card.component'; import {MatIconModule} from "@angular/material/icon"; import {FontAwesomeModule} from "@fortawesome/angular-fontawesome"; +import {NgApexchartsModule} from "ng-apexcharts"; @NgModule({ declarations: [ @@ -14,7 +15,8 @@ import {FontAwesomeModule} from "@fortawesome/angular-fontawesome"; CommonModule, NgOptimizedImage, MatIconModule, - FontAwesomeModule + FontAwesomeModule, + NgApexchartsModule ], exports: [] }) diff --git a/src/app/shared/github-service/github.service.ts b/src/app/shared/github-service/github.service.ts index 5905cea..568cffa 100644 --- a/src/app/shared/github-service/github.service.ts +++ b/src/app/shared/github-service/github.service.ts @@ -15,9 +15,17 @@ export class GithubService { GITHUB_API_URL = 'https://api.github.com'; + GITHUB_API_COLORS = 'https://raw.githubusercontent.com/ozh/github-colors/master/colors.json'; + GITHUB_USER = environment.githubUser; - constructor(private http: HttpClient) { } + colors!: Map; + + constructor(private http: HttpClient) { + this.getLanguageColor().subscribe((colors: Map) => { + this.colors = colors; + }); + } getProjects(): Observable { return this.http.get(this.apiReposString()).pipe( @@ -61,6 +69,7 @@ export class GithubService { return Object.keys(languages).map((language: string) => { return { name: language, + color: this.colors.get(language) || this.getRandColor(), percentage: (languages[language]/totalBytes)*100 } as Language; }); @@ -68,6 +77,22 @@ export class GithubService { ); } + private getLanguageColor(): Observable> { + return this.http.get(this.GITHUB_API_COLORS).pipe( + map((colors: any) => { + const colorMap = new Map(); + Object.keys(colors).forEach((language: string) => { + colorMap.set(language, colors[language].color); + }); + return colorMap; + }) + ); + } + + private getRandColor(): string { + return `#${Math.floor(Math.random()*16777215).toString(16)}`; + } + private apiReposString() { return `${this.GITHUB_API_URL}/users/${this.GITHUB_USER}/repos`; } diff --git a/src/app/shared/model/project/project.model.ts b/src/app/shared/model/project/project.model.ts index 8fd9c7b..af47098 100644 --- a/src/app/shared/model/project/project.model.ts +++ b/src/app/shared/model/project/project.model.ts @@ -1,5 +1,6 @@ export type Language = { name: string; + color: string; percentage: number; } diff --git a/src/assets/env.sample.js b/src/assets/env.sample.js index ac046df..151211c 100644 --- a/src/assets/env.sample.js +++ b/src/assets/env.sample.js @@ -4,4 +4,5 @@ // Environment variables window["env"]["BACKEND_URL"] = "${BACKEND_URL}"; window["env"]["BACKEND_OAUTH_URL"] = "${BACKEND_OAUTH_URL}"; + window["env"]["GITHUB_USER"] = "${GITHUB_USER}"; })(this);