From c5b5aae1db3df95e7f9eebd0a36e5063ea04a85b Mon Sep 17 00:00:00 2001 From: "maxim.gorbatyuk" Date: Wed, 17 Jan 2024 20:53:17 +0600 Subject: [PATCH] Added chart --- .../about-us/about-us.component.html | 2 +- .../salaries-by-grades-chart.component.html | 29 +++- .../salaries-by-grades-chart.component.scss | 11 ++ .../salaries-by-grades-chart.component.ts | 94 +++++-------- .../salaries-chart-js-object.ts | 114 +++++++++++++++ .../salaries-chart.component.html | 133 +++++++++--------- .../salaries-chart.component.ts | 1 + .../salaries-chart/salaries-chart.ts | 20 ++- .../salary-block-value.component.html | 2 +- .../salaries-chart/stub-salaries-chart.ts | 2 +- .../components/salaries-per-profession.ts | 53 +++++++ src/app/services/user-salaries.service.ts | 7 +- .../splitted-by-whitespaces-string.ts | 4 + src/index.html | 5 +- 14 files changed, 335 insertions(+), 142 deletions(-) create mode 100644 src/app/modules/salaries/components/salaries-by-grades-chart/salaries-chart-js-object.ts create mode 100644 src/app/modules/salaries/components/salaries-per-profession.ts diff --git a/src/app/modules/home/components/about-us/about-us.component.html b/src/app/modules/home/components/about-us/about-us.component.html index ffcf4ebd..83093a90 100644 --- a/src/app/modules/home/components/about-us/about-us.component.html +++ b/src/app/modules/home/components/about-us/about-us.component.html @@ -4,7 +4,7 @@
- Author: Maxim Gorbatyuk + Author: Maxim Gorbatyuk

diff --git a/src/app/modules/salaries/components/salaries-by-grades-chart/salaries-by-grades-chart.component.html b/src/app/modules/salaries/components/salaries-by-grades-chart/salaries-by-grades-chart.component.html index 67ec9d5e..fe4794c5 100644 --- a/src/app/modules/salaries/components/salaries-by-grades-chart/salaries-by-grades-chart.component.html +++ b/src/app/modules/salaries/components/salaries-by-grades-chart/salaries-by-grades-chart.component.html @@ -1,3 +1,28 @@ -
- {{chartData}} +
+
{{ title }}
+ +
+
+
Кол-во анкет
+ +
    + +
+
+ +
+
+ {{chartDataLocal}} +
+
+ +
+
\ No newline at end of file diff --git a/src/app/modules/salaries/components/salaries-by-grades-chart/salaries-by-grades-chart.component.scss b/src/app/modules/salaries/components/salaries-by-grades-chart/salaries-by-grades-chart.component.scss index bcf541aa..9f768937 100644 --- a/src/app/modules/salaries/components/salaries-by-grades-chart/salaries-by-grades-chart.component.scss +++ b/src/app/modules/salaries/components/salaries-by-grades-chart/salaries-by-grades-chart.component.scss @@ -1,3 +1,14 @@ #canvas { min-height: 400px; } + +#canvas-container { + position: relative; + width: 100%; + height: 100%; + min-height: 400px; +} + +.list-group-item { + font-size: smaller; +} \ No newline at end of file diff --git a/src/app/modules/salaries/components/salaries-by-grades-chart/salaries-by-grades-chart.component.ts b/src/app/modules/salaries/components/salaries-by-grades-chart/salaries-by-grades-chart.component.ts index 33a08198..ca4e025c 100644 --- a/src/app/modules/salaries/components/salaries-by-grades-chart/salaries-by-grades-chart.component.ts +++ b/src/app/modules/salaries/components/salaries-by-grades-chart/salaries-by-grades-chart.component.ts @@ -7,6 +7,9 @@ import { SalariesChart } from '../salaries-chart/salaries-chart'; import { Chart, ChartType } from 'chart.js/auto'; import { RandomRgbColor } from './random-rgb-color'; import { UserProfession } from '@models/salaries/user-profession'; +import { SalariesChartJsObject } from './salaries-chart-js-object'; +import { SalariesByMoneyBarChart } from '@services/user-salaries.service'; +import { SalariesPerProfession } from '../salaries-per-profession'; @Component({ selector: 'app-salaries-by-grades-chart', @@ -16,76 +19,47 @@ import { UserProfession } from '@models/salaries/user-profession'; export class SalariesByGradesChartComponent implements OnInit, OnDestroy { @Input() - chart: SalariesChart | null = null; + chart: SalariesByMoneyBarChart | null = null; - chartData: Chart | null = null; + @Input() + title: string | null = null; - constructor() {} + @Input() + salaries: Array | null = null; - ngOnInit(): void { - if (this.chart == null || - this.chart.salariesByMoneyBarChart == null) { - return; - } + chartDataLocal: SalariesChartJsObject | null = null; - const chartData = this.chart.salariesByMoneyBarChart; - const randomColor = new RandomRgbColor(); - const datasets = [ - { - type: 'line' as ChartType, - label: 'Все', - data: chartData.items.map(x => x.count), - borderWidth: 3, - borderColor: randomColor.toString(1), - backgroundColor: randomColor.toString(0.5), - }, - ]; + readonly canvasId = 'canvas_' + Math.random().toString(36).substring(7); - chartData.itemsByProfession.forEach((x, i) => { - const profession = x.profession; - const color = new RandomRgbColor(); - datasets.push({ - label: UserProfession[profession].toString(), - data: x.items.map(x => x.count), - borderWidth: 1, - borderColor: color.toString(0.6), - backgroundColor: color.toString(0.3), - type: 'bar' as ChartType, - }); - }); + constructor() {} - this.chartData = new Chart('canvas', { - type: 'scatter', - data: { - labels: chartData.labels - .map(x => { - let num = Number(x); - if (isNaN(num)) { - throw Error('Invalid label ' + x); - } + ngOnInit(): void { + // ignored + } - num = num / 1000; - return num + 'k'; - }), - datasets: datasets, - }, - options: { - maintainAspectRatio: false, - scales: { - y: { - beginAtZero: true, - }, - }, - elements: { - line: { - tension: 0.4 - }, - } - }, - }); + ngAfterViewInit() { + this.initChart(); } ngOnDestroy(): void { // ignored } + + toggleBarDatasetByProfession(profession: UserProfession): void { + this.chartDataLocal?.toggleDatasetByProfession(profession); + } + + private initChart(): void { + if (this.chart == null || this.salaries == null) { + return; + } + + this.chartDataLocal = new SalariesChartJsObject(this.canvasId, this.chart); + this.chartDataLocal.hideBarDatasets(); + + var chartEl = document.getElementById(this.canvasId); + if (chartEl != null && chartEl.parentElement != null) { + chartEl.style.height = chartEl?.parentElement.style.height ?? '100%'; + } + } } diff --git a/src/app/modules/salaries/components/salaries-by-grades-chart/salaries-chart-js-object.ts b/src/app/modules/salaries/components/salaries-by-grades-chart/salaries-chart-js-object.ts new file mode 100644 index 00000000..de698ec3 --- /dev/null +++ b/src/app/modules/salaries/components/salaries-by-grades-chart/salaries-chart-js-object.ts @@ -0,0 +1,114 @@ +import { UserProfession } from '@models/salaries/user-profession'; +import { SalariesByMoneyBarChart } from '@services/user-salaries.service'; +import { Chart, ChartType } from 'chart.js/auto'; +import { RandomRgbColor } from './random-rgb-color'; + +interface ChartDatasetType { + profession: UserProfession | null; + label: string; + data: Array; + borderWidth: number; + borderColor: string; + backgroundColor: string; + type: ChartType; +} + +export class SalariesChartJsObject extends Chart { + + private readonly datasets: Array = []; + + constructor(canvasId: string, chartData: SalariesByMoneyBarChart) { + + const ctx = document.getElementById(canvasId) as HTMLCanvasElement; + const randomColor = new RandomRgbColor(); + const datasets: Array = [ + { + profession: null, + type: 'line' as ChartType, + label: 'Все', + data: chartData.items.map(x => x.count), + borderWidth: 3, + borderColor: randomColor.toString(1), + backgroundColor: randomColor.toString(0.5), + }, + ]; + + chartData.itemsByProfession.forEach((x, i) => { + const color = new RandomRgbColor(); + datasets.push({ + profession: x.profession, + label: UserProfession[x.profession].toString(), + data: x.items.map(x => x.count), + borderWidth: 1, + borderColor: color.toString(0.6), + backgroundColor: color.toString(0.3), + type: 'bar' as ChartType, + }); + }); + + super( + canvasId, + { + type: 'scatter', + data: { + labels: chartData.labels + .map(x => { + let num = Number(x); + if (isNaN(num)) { + throw Error('Invalid label ' + x); + } + + num = num / 1000; + return num + 'k'; + }), + datasets: datasets, + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + beginAtZero: true, + }, + }, + elements: { + line: { + tension: 0.4 + }, + }, + plugins: { + legend: { + position: 'bottom', + title: { + position: 'start', + }, + } + } + }, + }); + + this.datasets = datasets; + } + + hideBarDatasets(): void { + for (let index = 0; index < this.datasets.length; index++) { + const dataset = this.datasets[index]; + if (dataset.type == 'bar') { + this.setDatasetVisibility(index, false); + } + } + + this.update(); + } + + toggleDatasetByProfession(profession: UserProfession): void { + const index = this.datasets.findIndex(x => x.profession == profession); + if (index == -1) { + return; + } + + const visibility = !this.isDatasetVisible(index); + this.setDatasetVisibility(index, visibility); + this.update(); + } +} \ No newline at end of file diff --git a/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.html b/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.html index 2b11766e..22a9f123 100644 --- a/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.html +++ b/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.html @@ -21,87 +21,86 @@
-
+
-
-
-
-
Квалификации:
-
- +
+
+
Демонстрационные данные:
+
+ +
+ + +
+
-
- -
-
-
Действия:
-
- + +
+ +
+ + + +
+
-
- +
- -
-
-
Демонстрационные данные:
-
- -
- - -
- -
- -
- -
- - - -
- -
- -
- - -
-
Рассчитано на основании {{ salariesChart.countOfRecords }} анкет(ы)
-
- -
-
Данные сгенерированы для демонстрации. - для получения актуальных данных.
+ + +
+
+ Рассчитано на основании {{ salariesChart.countOfRecords }} анкет(ы) +
- -
+
+ +
+
Данные сгенерированы для демонстрации. + для получения актуальных данных.
+
+ +
+
- По всем IT-специалистам + Графики (демонстрационные данные)
-
+
TODO: в скором времени будет добавлена возможность фильтрации данных по специальностям, грейдам и фреймворкам/языкам программирования
- - +
+ +
+ +
+
+ + +
+
+ +
+ Идея подсмотрена у сервиса Habr.Карьера, за что им большая благодарность.
- -
- Идея подсмотрена у сервиса Habr.Карьера, за что им большая благодарность. -
+
diff --git a/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.ts b/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.ts index b7e85c3e..7f536d56 100644 --- a/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.ts +++ b/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.ts @@ -16,6 +16,7 @@ import { StubSalariesChart } from './stub-salaries-chart'; export class SalariesChartComponent implements OnInit, OnDestroy { salariesChart: SalariesChart | null = null; + showDataStub = false; openAddSalaryModal = false; isAuthenticated = false; diff --git a/src/app/modules/salaries/components/salaries-chart/salaries-chart.ts b/src/app/modules/salaries/components/salaries-chart/salaries-chart.ts index 249096d9..bcc64f0f 100644 --- a/src/app/modules/salaries/components/salaries-chart/salaries-chart.ts +++ b/src/app/modules/salaries/components/salaries-chart/salaries-chart.ts @@ -1,5 +1,6 @@ import { formatNumber } from "@angular/common"; -import { SalariesByMoneyBarChart, SalariesByProfession, SalariesChartResponse } from "@services/user-salaries.service"; +import { SalariesByMoneyBarChart, SalariesChartResponse } from "@services/user-salaries.service"; +import { SalariesPerProfession } from "../salaries-per-profession"; export class SalariesChart { @@ -10,8 +11,14 @@ export class SalariesChart { readonly medianRemoteSalary: string | null; readonly countOfRecords: number; - readonly salariesByProfession: Array; + readonly salariesByMoneyBarChart: SalariesByMoneyBarChart | null; + readonly salariesByMoneyBarChartForRemote: SalariesByMoneyBarChart | null; + + readonly salariesPerProfessionForLocal: Array | null; + readonly salariesPerProfessionForRemote: Array | null; + + readonly hasRemoteSalaries: boolean; constructor(readonly data: SalariesChartResponse) { this.averageSalary = SalariesChart.formatNumber(data.averageSalary) ?? ''; @@ -21,8 +28,15 @@ export class SalariesChart { this.medianRemoteSalary = SalariesChart.formatNumber(data.medianRemoteSalary) this.countOfRecords = data.salaries.length; - this.salariesByProfession = data.salariesByProfession; + this.salariesByMoneyBarChart = data.salariesByMoneyBarChart; + this.salariesByMoneyBarChartForRemote = data.salariesByMoneyBarChartForRemote; + + const salariesPerProfession = SalariesPerProfession.from(data.salaries); + + this.salariesPerProfessionForLocal = salariesPerProfession.local; + this.salariesPerProfessionForRemote = salariesPerProfession.remote; + this.hasRemoteSalaries = this.salariesPerProfessionForRemote.length > 0; } private static formatNumber(value: number | null): string | null { diff --git a/src/app/modules/salaries/components/salaries-chart/salary-block-value/salary-block-value.component.html b/src/app/modules/salaries/components/salaries-chart/salary-block-value/salary-block-value.component.html index 9ada4904..429d17ab 100644 --- a/src/app/modules/salaries/components/salaries-chart/salary-block-value/salary-block-value.component.html +++ b/src/app/modules/salaries/components/salaries-chart/salary-block-value/salary-block-value.component.html @@ -1,7 +1,7 @@
{{ title }}
- {{ value }} + {{ value }} тг.
\ No newline at end of file diff --git a/src/app/modules/salaries/components/salaries-chart/stub-salaries-chart.ts b/src/app/modules/salaries/components/salaries-chart/stub-salaries-chart.ts index d4b30598..806b3233 100644 --- a/src/app/modules/salaries/components/salaries-chart/stub-salaries-chart.ts +++ b/src/app/modules/salaries/components/salaries-chart/stub-salaries-chart.ts @@ -41,7 +41,6 @@ export class StubSalariesChart extends SalariesChart { medianRemoteSalary: StubSalariesChart.getRandomNumber(1_200, 600) * 1000, shouldAddOwnSalary: true, salaries: salaries, - salariesByProfession: [], salariesByMoneyBarChart: { items: StubSalariesChart.salaryLabels.map((x) => { const value = parseInt(x); @@ -54,6 +53,7 @@ export class StubSalariesChart extends SalariesChart { itemsByProfession: [], labels: StubSalariesChart.salaryLabels, }, + salariesByMoneyBarChartForRemote: null, rangeStart: new Date(), rangeEnd: new Date(), }); diff --git a/src/app/modules/salaries/components/salaries-per-profession.ts b/src/app/modules/salaries/components/salaries-per-profession.ts new file mode 100644 index 00000000..ec7a572f --- /dev/null +++ b/src/app/modules/salaries/components/salaries-per-profession.ts @@ -0,0 +1,53 @@ +import { CompanyType } from "@models/salaries/company-type"; +import { UserSalary } from "@models/salaries/salary.model"; +import { UserProfession } from "@models/salaries/user-profession"; +import { SplittedByWhitespacesString } from "@shared/value-objects/splitted-by-whitespaces-string"; + +export class SalariesPerProfession { + + readonly professionName: string; + constructor( + readonly profession: UserProfession, + readonly items: Array) { + this.professionName = new SplittedByWhitespacesString(UserProfession[profession]).toString(); + } + + static from(salaries: Array): { + local: Array, + remote: Array + } { + + const localSalaries: Array = []; + const remoteSalaries: Array = []; + + for (let index = 0; index < salaries.length; index++) { + const salary = salaries[index]; + if (salary.company == CompanyType.Local) { + localSalaries.push(salary); + } else { + remoteSalaries.push(salary); + } + } + + var uniqueProfessionsForLocal = [...new Set(localSalaries.map(x => x.profession))]; + var uniqueProfessionsForRemote = [...new Set(remoteSalaries.map(x => x.profession))]; + + console.log(uniqueProfessionsForLocal); + console.log(uniqueProfessionsForRemote); + + const local = uniqueProfessionsForLocal.map(x => { + const filteredSalaries = localSalaries.filter(salary => salary.profession == x); + return new SalariesPerProfession(x, filteredSalaries); + }); + + const remote = uniqueProfessionsForRemote.map(x => { + const filteredSalaries = remoteSalaries.filter(salary => salary.profession == x); + return new SalariesPerProfession(x, filteredSalaries); + }); + + return { + local: local.sort((a, b) => a.items.length > b.items.length ? -1 : 1), + remote: remote.sort((a, b) => a.items.length > b.items.length ? -1 : 1), + }; + } +} diff --git a/src/app/services/user-salaries.service.ts b/src/app/services/user-salaries.service.ts index 18510be5..8ad95cb6 100644 --- a/src/app/services/user-salaries.service.ts +++ b/src/app/services/user-salaries.service.ts @@ -31,13 +31,8 @@ export interface SalariesChartResponse { averageRemoteSalary: number | null; medianRemoteSalary: number | null; - salariesByProfession: SalariesByProfession[]; salariesByMoneyBarChart: SalariesByMoneyBarChart | null; -} - -export interface SalariesByProfession { - profession: UserProfession; - salaries: UserSalary[]; + salariesByMoneyBarChartForRemote: SalariesByMoneyBarChart | null; } export interface SalariesByProfessionMoneyBarChartItem { diff --git a/src/app/shared/value-objects/splitted-by-whitespaces-string.ts b/src/app/shared/value-objects/splitted-by-whitespaces-string.ts index 38c37e62..4ba6f3dc 100644 --- a/src/app/shared/value-objects/splitted-by-whitespaces-string.ts +++ b/src/app/shared/value-objects/splitted-by-whitespaces-string.ts @@ -15,4 +15,8 @@ export class SplittedByWhitespacesString { this.value = ''; } + + toString(): string { + return this.value; + } } diff --git a/src/index.html b/src/index.html index 9cc238e4..0b79fc6f 100644 --- a/src/index.html +++ b/src/index.html @@ -34,7 +34,10 @@
Tech.Interview
Kazakhstan, Almaty
-
2024, © maximgorbatyuk
+
+ 2024, + © maximgorbatyuk +