Skip to content

Commit 93f6bc6

Browse files
Mutugiiidogi
andauthored
manager: smoother reports charts loading (fixes #9183) (#9184)
Co-authored-by: dogi <[email protected]>
1 parent 679f256 commit 93f6bc6

File tree

4 files changed

+90
-42
lines changed

4 files changed

+90
-42
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"name": "planet",
33
"license": "AGPL-3.0",
4-
"version": "0.20.48",
4+
"version": "0.20.49",
55
"myplanet": {
6-
"latest": "v0.34.32",
7-
"min": "v0.33.32"
6+
"latest": "v0.34.54",
7+
"min": "v0.33.54"
88
},
99
"scripts": {
1010
"ng": "ng",

src/app/manager-dashboard/reports/reports-detail.component.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
55
import { Location } from '@angular/common';
66
import { combineLatest, Subject, of } from 'rxjs';
77
import { takeUntil, take } from 'rxjs/operators';
8-
import { Chart, ChartConfiguration, BarController, CategoryScale, LinearScale, BarElement, Title, Legend, Tooltip } from 'chart.js';
8+
import type { Chart as ChartJs, ChartConfiguration } from 'chart.js';
9+
import { loadChart } from '../../shared/chart-utils';
910
import { ReportsService } from './reports.service';
1011
import { StateService } from '../../shared/state.service';
1112
import { styleVariables, formatDate } from '../../shared/utils';
@@ -28,7 +29,7 @@ import { UserProfileDialogComponent } from '../../users/users-profile/users-prof
2829
import { findDocuments } from '../../shared/mangoQueries';
2930
import { DeviceInfoService, DeviceType } from '../../shared/device-info.service';
3031

31-
Chart.register(BarController, CategoryScale, LinearScale, BarElement, Title, Legend, Tooltip);
32+
type ChartModule = typeof import('chart.js');
3233

3334
@Component({
3435
templateUrl: './reports-detail.component.html',
@@ -43,7 +44,7 @@ export class ReportsDetailComponent implements OnInit, OnDestroy {
4344
planetCode = '';
4445
planetName = '';
4546
reports: any = {};
46-
charts: Chart[] = [];
47+
charts: ChartJs[] = [];
4748
users: any[] = [];
4849
onDestroy$ = new Subject<void>();
4950
filter: ReportDetailFilter = { app: '', members: [], startDate: new Date(0), endDate: new Date() };
@@ -89,6 +90,7 @@ export class ReportsDetailComponent implements OnInit, OnDestroy {
8990
week2Label = $localize`Week 2`;
9091
comparisonData1: any = {};
9192
comparisonData2: any = {};
93+
private chartModule: ChartModule | null = null;
9294

9395
constructor(
9496
private activityService: ReportsService,
@@ -156,6 +158,8 @@ export class ReportsDetailComponent implements OnInit, OnDestroy {
156158
ngOnDestroy() {
157159
this.onDestroy$.next();
158160
this.onDestroy$.complete();
161+
this.charts.forEach((chart) => chart.destroy());
162+
this.charts = [];
159163
}
160164

161165
@HostListener('window:resize')
@@ -460,22 +464,26 @@ export class ReportsDetailComponent implements OnInit, OnDestroy {
460464
});
461465
}
462466

463-
setChart({ data, labels, chartName }) {
464-
const updateChart = this.charts.find(chart => chart.canvas.id === chartName);
467+
async setChart({ data, labels, chartName }) {
468+
const { Chart } = await loadChart([
469+
'BarController', 'DoughnutController', 'CategoryScale', 'LinearScale', 'BarElement', 'Title', 'Legend', 'Tooltip'
470+
]);
471+
const updateChart = this.charts.find(newChart => newChart.canvas.id === chartName);
465472
if (updateChart) {
466473
updateChart.data = { ...data, labels };
467-
updateChart.update();
474+
updateChart.update('none');
468475
return;
469476
}
470477
const chartConfig: ChartConfiguration<'bar'> = {
471478
type: 'bar',
472-
data,
479+
data: { ...data, labels },
473480
options: {
474481
plugins: {
475482
title: { display: true, text: titleOfChartName(chartName), font: { size: 16 } },
476483
legend: { position: 'bottom' }
477484
},
478485
maintainAspectRatio: false,
486+
animation: false,
479487
scales: {
480488
x: { type: 'category' },
481489
y: {

src/app/shared/chart-utils.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { Chart } from 'chart.js';
2+
3+
type ChartJsModule = typeof import('chart.js');
4+
type RegisterableKey = 'ArcElement' | 'BarController' | 'BarElement' | 'CategoryScale' | 'DoughnutController'
5+
| 'Legend' | 'LinearScale' | 'Title' | 'Tooltip' | 'LineController' | 'PointElement' | 'LineElement';
6+
7+
let chartJsPromise: Promise<ChartJsModule> | null = null;
8+
const registeredKeys = new Set<RegisterableKey>();
9+
10+
function registerOnce(module: ChartJsModule, keys: RegisterableKey[]): void {
11+
const pending = keys.filter((key) => {
12+
if (registeredKeys.has(key)) {
13+
return false;
14+
}
15+
if (!(key in module)) {
16+
console.warn(`Chart.js registerable "${key}" is not available on the module export.`);
17+
return false;
18+
}
19+
registeredKeys.add(key);
20+
return true;
21+
}).map((key) => module[key]);
22+
23+
if (pending.length) {
24+
module.Chart.register(...pending);
25+
}
26+
}
27+
28+
export async function loadChart(keys: RegisterableKey[] = []): Promise<ChartJsModule> {
29+
if (!chartJsPromise) {
30+
chartJsPromise = import('chart.js').catch((error) => {
31+
chartJsPromise = null;
32+
throw error;
33+
});
34+
}
35+
36+
const module = await chartJsPromise;
37+
if (keys.length) {
38+
registerOnce(module, keys);
39+
}
40+
return module;
41+
}
42+
43+
export type ChartJs = Chart;
44+
45+
export function createChartCanvas(width = 300, height = 400): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D | null } {
46+
const canvas = document.createElement('canvas');
47+
canvas.width = width;
48+
canvas.height = height;
49+
return { canvas, ctx: canvas.getContext('2d') };
50+
}
51+
52+
export function renderNoDataPlaceholder(ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, message = 'No data available'): string {
53+
ctx.fillStyle = '#666666';
54+
ctx.textAlign = 'center';
55+
ctx.textBaseline = 'middle';
56+
ctx.font = '16px sans-serif';
57+
ctx.fillText(message, canvas.width / 2, canvas.height / 2);
58+
return canvas.toDataURL('image/png');
59+
}
60+
61+
export const CHART_COLORS = [
62+
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
63+
'#FF9F40', '#C9CBCF', '#8DD4F2', '#A8E6CF', '#DCE775'
64+
];

src/app/submissions/submissions.service.ts

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Injectable } from '@angular/core';
22
import { Subject, of, forkJoin, throwError } from 'rxjs';
33
import { catchError, map, switchMap, tap } from 'rxjs/operators';
4-
import type { Chart as ChartType, ChartConfiguration } from 'chart.js';
4+
import type { ChartConfiguration } from 'chart.js';
55
import htmlToPdfmake from 'html-to-pdfmake';
66
import { findDocuments } from '../shared/mangoQueries';
77
import { CouchService } from '../shared/couchdb.service';
@@ -17,23 +17,7 @@ import { attachNamesToPlanets, codeToPlanetName, fullLabel } from '../manager-da
1717
import { TeamsService } from '../teams/teams.service';
1818
import { ChatService } from '../shared/chat.service';
1919
import { surveyAnalysisPrompt } from '../shared/ai-prompts.constants';
20-
21-
type ChartJsModule = typeof import('chart.js');
22-
23-
let chartJsPromise: Promise<ChartJsModule> | null = null;
24-
25-
async function loadChart(): Promise<ChartJsModule> {
26-
if (!chartJsPromise) {
27-
chartJsPromise = import('chart.js').then((module) => {
28-
const { Chart, BarController, DoughnutController, BarElement, ArcElement, LinearScale, CategoryScale } = module;
29-
Chart.register(BarController, DoughnutController, BarElement, ArcElement, LinearScale, CategoryScale);
30-
31-
return module;
32-
});
33-
}
34-
35-
return chartJsPromise;
36-
}
20+
import { loadChart, createChartCanvas, renderNoDataPlaceholder, CHART_COLORS } from '../shared/chart-utils';
3721

3822
pdfMake.vfs = pdfFonts.pdfMake.vfs;
3923

@@ -571,24 +555,18 @@ export class SubmissionsService {
571555
}
572556

573557
async generateChartImage(data: any): Promise<string> {
574-
const { Chart } = await loadChart();
575-
const canvas = document.createElement('canvas');
576-
canvas.width = 300;
577-
canvas.height = 400;
558+
const { Chart } = await loadChart([
559+
'BarController', 'DoughnutController', 'BarElement', 'ArcElement', 'LinearScale', 'CategoryScale', 'Legend', 'Tooltip', 'Title'
560+
]);
561+
const { canvas, ctx } = createChartCanvas(300, 400);
578562
const isBar = data.chartType === 'bar';
579563
const isRatingScale = data.isRatingScale || false;
580-
const ctx = canvas.getContext('2d');
581564

582565
if (!ctx) { return ''; }
583566
const hasData = Array.isArray(data.data) && data.data.some((value: number) => Number(value) > 0);
584567

585568
if (!hasData) {
586-
ctx.fillStyle = '#666666';
587-
ctx.textAlign = 'center';
588-
ctx.textBaseline = 'middle';
589-
ctx.font = '16px sans-serif';
590-
ctx.fillText('No data available', canvas.width / 2, canvas.height / 2);
591-
return canvas.toDataURL('image/png');
569+
renderNoDataPlaceholder(ctx, canvas, 'No data available');
592570
}
593571

594572
const maxCount = Math.max(...data.data);
@@ -599,9 +577,7 @@ export class SubmissionsService {
599577
datasets: [ {
600578
data: data.data,
601579
label: isRatingScale ? 'selection/choices(1-9)' : (isBar ? '% of responders/selection' : undefined),
602-
backgroundColor: [
603-
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#C9CBCF', '#8DD4F2', '#A8E6CF', '#DCE775'
604-
],
580+
backgroundColor: CHART_COLORS
605581
} ]
606582
},
607583
options: {

0 commit comments

Comments
 (0)