Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 22 additions & 12 deletions src/app/reports/run-report/chart/chart.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,27 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->

<div class="m-b-20 layout-align-end">
<mat-button-toggle-group aria-label="Select Chart Type">
<mat-button-toggle value="Bar" (click)="setBarChart(inputData)">{{
'labels.buttons.Bar Chart' | translate
}}</mat-button-toggle>
<mat-button-toggle value="Pie" (click)="setPieChart(inputData)">{{
'labels.buttons.Pie Chart' | translate
}}</mat-button-toggle>
</mat-button-toggle-group>
</div>
<div class="p-20 m-b-20">
<div class="layout-align-end m-b-20">
<mat-button-toggle-group
[value]="selectedChartType"
[disabled]="hideOutput"
aria-label="{{ 'labels.buttons.Select Chart Type' | translate }}"
(change)="selectChart($event.value)"
>
<mat-button-toggle value="Bar" (click)="refreshChartIfSameType('Bar')">{{
'labels.buttons.Bar Chart' | translate
}}</mat-button-toggle>
<mat-button-toggle value="Pie" (click)="refreshChartIfSameType('Pie')">{{
'labels.buttons.Pie Chart' | translate
}}</mat-button-toggle>
<mat-button-toggle value="Polar" (click)="refreshChartIfSameType('Polar')">{{
'labels.buttons.Polar Area Chart' | translate
}}</mat-button-toggle>
</mat-button-toggle-group>
</div>

<div [ngStyle]="{ display: hideOutput ? 'none' : 'block' }">
<canvas id="output"></canvas>
<div [ngStyle]="{ display: hideOutput ? 'none' : 'block' }" class="chart-output-container">
<canvas id="output"></canvas>
</div>
</div>
11 changes: 11 additions & 0 deletions src/app/reports/run-report/chart/chart.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,14 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

.chart-output-container {
position: relative;
width: 100%;
height: 500px;

canvas {
width: 100% !important;
height: 100% !important;
}
}
216 changes: 203 additions & 13 deletions src/app/reports/run-report/chart/chart.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
*/

/** Angular Imports */
import { Component, OnChanges, Input, inject } from '@angular/core';
import { Component, OnChanges, OnInit, OnDestroy, Input, inject } from '@angular/core';

/** Custom Services */
import { ReportsService } from '../../reports.service';
import { ThemeStorageService } from 'app/shared/theme-picker/theme-storage.service';

/** Custom Models */
import { ChartData } from '../../common-models/chart-data.model';
Expand Down Expand Up @@ -38,8 +39,11 @@ Chart.register(...registerables);
NgStyle
]
})
export class ChartComponent implements OnChanges {
export class ChartComponent implements OnChanges, OnInit, OnDestroy {
private reportsService = inject(ReportsService);
private themeStorageService = inject(ThemeStorageService);
private resizeTimeoutId: ReturnType<typeof setTimeout> | undefined;
private initialRenderTimeoutId: ReturnType<typeof setTimeout> | undefined;

/** Run Report Data */
@Input() dataObject: any;
Expand All @@ -50,6 +54,52 @@ export class ChartComponent implements OnChanges {
hideOutput = true;
/** Data object for witching charts in view. */
inputData: ChartData;
/** Tracks the currently selected chart type */
selectedChartType: string = 'Pie';
/** Resize listener */
private readonly resizeListener = () => this.resizeChart();

/**
* Initialize component and add resize listener.
*/
ngOnInit() {
window.addEventListener('resize', this.resizeListener);
}

/**
* Clean up on component destroy.
*/
ngOnDestroy() {
window.removeEventListener('resize', this.resizeListener);
if (this.resizeTimeoutId !== undefined) {
clearTimeout(this.resizeTimeoutId);
this.resizeTimeoutId = undefined;
}
if (this.initialRenderTimeoutId !== undefined) {
clearTimeout(this.initialRenderTimeoutId);
this.initialRenderTimeoutId = undefined;
}
if (this.chart) {
Comment on lines +65 to +82
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

window.addEventListener() returns void, so assigning its return value to resizeListener means removeEventListener() in ngOnDestroy() will never remove the handler (leaking the listener and continuing to call resizeChart() after destroy). Store the actual callback function (or use an AbortController/RxJS fromEvent with teardown) and consider debouncing/throttling resize events to avoid queuing many setTimeout() calls during continuous resize.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

this.chart.destroy();
this.chart = undefined;
}
}

/**
* Resize and redraw chart when window size changes.
*/
resizeChart() {
if (this.chart) {
if (this.resizeTimeoutId !== undefined) {
clearTimeout(this.resizeTimeoutId);
}
// Debounce resize calls to avoid queuing multiple chart operations.
this.resizeTimeoutId = setTimeout(() => {
this.chart?.resize();
this.resizeTimeoutId = undefined;
}, 100);
}
}

/**
* Fetches run report data post changes in run report form.
Expand All @@ -63,19 +113,59 @@ export class ChartComponent implements OnChanges {
.getChartRunReportData(this.dataObject.report.name, this.dataObject.formData)
.subscribe((response: ChartData) => {
this.inputData = response;
this.setPieChart(this.inputData);
this.selectedChartType = 'Pie';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this always a Pie Chart? or is this just the default?

Copy link
Contributor Author

@Omar-Nabil2 Omar-Nabil2 Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API response does not provide a chart type field, so the component chooses Pie as the initial default for first render to keep behavior unchanged from the previous implementation. It is not always Pie: users can switch to Bar or Polar using the chart toggle after data loads. If we want backend-driven chart selection later, we can add a chartType attribute in the API contract and use that instead of a UI default.

{
  "columnHeaders": [
    {
      "columnName": "office_name",
      "columnType": "VARCHAR",
      "columnDisplayType": "STRING",
      "isColumnNullable": false,
      "isColumnPrimaryKey": false,
      "isColumnUnique": false,
      "isColumnIndexed": false,
      "columnValues": []
    },
    {
      "columnName": "total_loans",
      "columnType": "BIGINT",
      "columnDisplayType": "INTEGER",
      "isColumnNullable": false,
      "isColumnPrimaryKey": false,
      "isColumnUnique": false,
      "isColumnIndexed": false,
      "columnValues": []
    },
    {
      "columnName": "total_principal",
      "columnType": "DECIMAL",
      "columnDisplayType": "DECIMAL",
      "isColumnNullable": false,
      "isColumnPrimaryKey": false,
      "isColumnUnique": false,
      "isColumnIndexed": false,
      "columnValues": []
    },
    {
      "columnName": "active_loans",
      "columnType": "BIGINT",
      "columnDisplayType": "INTEGER",
      "isColumnNullable": false,
      "isColumnPrimaryKey": false,
      "isColumnUnique": false,
      "isColumnIndexed": false,
      "columnValues": []
    }
  ],
  "data": [
    {
      "row": [
        "New Branch Name",
        2,
        28000,
        2
      ]
    },
    {
      "row": [
        "West Sub-Branch",
        1,
        12000,
        1
      ]
    },
    {
      "row": [
        "East Sub-Branch",
        1,
        4000,
        1
      ]
    }
  ]
}

this.hideOutput = false;
if (this.initialRenderTimeoutId !== undefined) {
clearTimeout(this.initialRenderTimeoutId);
}
this.initialRenderTimeoutId = setTimeout(() => {
this.setPieChart(response);
this.initialRenderTimeoutId = undefined;
});
});
}

/**
* Handles chart type selection and renders the selected chart.
* @param {string} chartType The type of chart to display
*/
selectChart(chartType: string) {
if (!this.inputData) {
return;
}
const chartColors = this.randomColorArray(this.inputData.values.length);
this.selectedChartType = chartType;
switch (chartType) {
case 'Bar':
this.setBarChart(this.inputData, chartColors);
break;
case 'Pie':
this.setPieChart(this.inputData, chartColors);
break;
case 'Polar':
this.setPolarAreaChart(this.inputData, chartColors);
break;
}
}

/**
* Refreshes colors when user clicks the currently selected toggle.
*/
refreshChartIfSameType(chartType: string) {
if (chartType === this.selectedChartType) {
this.selectChart(chartType);
}
}

/**
* Creates instance of chart.js pie chart.
* Refer: https://www.chartjs.org/docs/latest/charts/doughnut.html for configuration details.
*/
setPieChart(inputData: ChartData) {
setPieChart(inputData: ChartData, chartColors?: string[]) {
if (this.chart) {
this.chart.destroy();
}
const colors = chartColors ?? this.randomColorArray(inputData.values.length);
this.chart = new Chart('output', {
type: 'pie',
data: {
Expand All @@ -84,11 +174,13 @@ export class ChartComponent implements OnChanges {
{
label: inputData.valuesLabel,
data: inputData.values,
backgroundColor: this.randomColorArray(inputData.values.length)
backgroundColor: colors
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
Expand All @@ -103,10 +195,11 @@ export class ChartComponent implements OnChanges {
* Creates instance of chart.js bar chart.
* Refer: https://www.chartjs.org/docs/latest/charts/bar.html for configuration details.
*/
setBarChart(inputData: ChartData) {
setBarChart(inputData: ChartData, chartColors?: string[]) {
if (this.chart) {
this.chart.destroy();
}
const colors = chartColors ?? this.randomColorArray(inputData.values.length);
this.chart = new Chart('output', {
type: 'bar',
data: {
Expand All @@ -115,11 +208,13 @@ export class ChartComponent implements OnChanges {
{
label: inputData.valuesLabel,
data: inputData.values,
backgroundColor: this.randomColorArray(inputData.values.length)
backgroundColor: colors
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
Expand All @@ -138,12 +233,53 @@ export class ChartComponent implements OnChanges {
});
}

/**
* Creates instance of chart.js polar area chart.
* Refer: https://www.chartjs.org/docs/latest/charts/polar.html for configuration details.
*/
setPolarAreaChart(inputData: ChartData, chartColors?: string[]) {
if (this.chart) {
this.chart.destroy();
}
const colors = chartColors ?? this.randomColorArray(inputData.values.length);
this.chart = new Chart('output', {
type: 'polarArea',
data: {
labels: inputData.keys,
datasets: [
{
label: inputData.valuesLabel,
data: inputData.values,
backgroundColor: colors,
borderColor: colors
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: inputData.keysLabel
},
legend: { display: true }
},
scales: {
r: {
min: 0
}
}
}
});
}

/**
* Generates bar/pie-slice colors array for dynamic charts.
* @param {number} length Length of dataset array.
*/
randomColorArray(length: number) {
const colorArray: any[] = [];
const colorArray: string[] = [];
while (length--) {
const color = this.randomColor();
colorArray.push(color);
Expand All @@ -152,12 +288,66 @@ export class ChartComponent implements OnChanges {
}

/**
* Returns a random rgb color.
* Returns a semi-random color based on the active theme palette.
*/
randomColor() {
const r = Math.floor(Math.random() * 255);
const g = Math.floor(Math.random() * 255);
const b = Math.floor(Math.random() * 255);
return `rgb(${r},${g},${b},0.6)`;
const baseColors = this.getThemeBaseColors();
const baseColor = baseColors[Math.floor(Math.random() * baseColors.length)];
const variation = Math.floor(Math.random() * 61) - 30;
const [
r,
g,
b
] = this.hexToRgb(baseColor).map((channel) => this.clamp(channel + variation));
return `rgba(${r},${g},${b},0.6)`;
}

/**
* Derives chart base colors from the user's selected theme and current dark mode.
*/
private getThemeBaseColors(): string[] {
const savedTheme = this.themeStorageService.getTheme();
const primary = savedTheme?.primary || '#1074B9';
const accent = savedTheme?.accent || '#B4D575';
const isDark = document.body.classList.contains('dark-theme');

if (isDark) {
return [
primary,
accent,
'#5BA2EC',
'#83A447',
'#C4C6D0'
];
}

return [
primary,
accent,
'#004989',
'#E7FFA5',
'#6E8A3B'
];
}

private hexToRgb(hexColor: string): number[] {
const hex = hexColor.replace('#', '');
const normalized =
hex.length === 3
? hex
.split('')
.map((char) => char + char)
.join('')
: hex;
const numeric = parseInt(normalized, 16);
return [
(numeric >> 16) & 255,
(numeric >> 8) & 255,
numeric & 255
];
}

private clamp(value: number): number {
return Math.max(0, Math.min(255, value));
}
}
2 changes: 1 addition & 1 deletion src/app/reports/run-report/run-report.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@

<div class="container output" *ngIf="isCollapsed">
<mat-card>
<div class="m-b-20">
<div class="m-b-20 p-20">
<button mat-raised-button color="primary" (click)="isCollapsed = false">
{{ 'labels.buttons.Parameters' | translate }}
</button>
Expand Down
2 changes: 2 additions & 0 deletions src/assets/translations/cs-CS.json
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@
"Payments": "Platby",
"Permissions": "Oprávnění",
"Pie Chart": "Koláčový graf",
"Polar Area Chart": "Polární plošný graf",
"Post Dividend": "Vyplácet dividendu",
"Previous": "Předchozí",
"Preview": "Náhled",
Expand Down Expand Up @@ -562,6 +563,7 @@
"Setup Products": "Nastavení produktů",
"Setup System": "Systém nastavení",
"Select All": "Vybrat vše",
"Select Chart Type": "Vybrat typ grafu",
"Show more": "Zobrazit více",
"Show less": "Ukaž méně",
"Signing in...": "Přihlašování",
Expand Down
Loading