Skip to content

Commit d16f589

Browse files
dogiMutugiii
andauthored
manager: smoother submission chart library loading (fixes #9179) (#9158)
Co-authored-by: mutugiii <[email protected]>
1 parent 340e3c1 commit d16f589

File tree

2 files changed

+95
-63
lines changed

2 files changed

+95
-63
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "planet",
33
"license": "AGPL-3.0",
4-
"version": "0.20.39",
4+
"version": "0.20.40",
55
"myplanet": {
66
"latest": "v0.33.52",
77
"min": "v0.32.52"

src/app/submissions/submissions.service.ts

Lines changed: 94 additions & 62 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 { Chart, ChartConfiguration, BarController, DoughnutController, BarElement, ArcElement, LinearScale, CategoryScale } from 'chart.js';
4+
import type { Chart as ChartType, ChartConfiguration } from 'chart.js';
55
import htmlToPdfmake from 'html-to-pdfmake';
66
import { findDocuments } from '../shared/mangoQueries';
77
import { CouchService } from '../shared/couchdb.service';
@@ -18,8 +18,24 @@ import { TeamsService } from '../teams/teams.service';
1818
import { ChatService } from '../shared/chat.service';
1919
import { surveyAnalysisPrompt } from '../shared/ai-prompts.constants';
2020

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+
}
37+
2138
pdfMake.vfs = pdfFonts.pdfMake.vfs;
22-
Chart.register(BarController, DoughnutController, BarElement, ArcElement, LinearScale, CategoryScale);
2339

2440
@Injectable({
2541
providedIn: 'root'
@@ -552,75 +568,91 @@ export class SubmissionsService {
552568
}
553569

554570
async generateChartImage(data: any): Promise<string> {
571+
const { Chart } = await loadChart();
555572
const canvas = document.createElement('canvas');
556573
canvas.width = 300;
557574
canvas.height = 400;
558575
const isBar = data.chartType === 'bar';
559576
const isRatingScale = data.isRatingScale || false;
560577
const ctx = canvas.getContext('2d');
561578

562-
return new Promise<string>((resolve) => {
563-
const maxCount = Math.max(...data.data);
564-
const chartConfig: ChartConfiguration<'bar' | 'doughnut'> = {
565-
type: isBar ? 'bar' : 'doughnut',
566-
data: {
567-
labels: data.labels,
568-
datasets: [ {
569-
data: data.data,
570-
label: isRatingScale ? 'selection/choices(1-9)' : (isBar ? '% of responders/selection' : undefined),
571-
backgroundColor: [
572-
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#C9CBCF', '#8DD4F2', '#A8E6CF', '#DCE775'
573-
],
574-
} ]
575-
},
576-
options: {
577-
responsive: false,
578-
maintainAspectRatio: false,
579-
indexAxis: 'x',
580-
plugins: {
581-
legend: {
582-
display: true,
583-
labels: {
584-
boxWidth: isBar ? 0 : 50,
585-
boxHeight: isBar ? 0 : 20
586-
}
587-
}
588-
},
589-
scales: isBar ? {
590-
y: {
591-
type: 'linear',
592-
beginAtZero: true,
593-
max: isRatingScale ? maxCount > 0 ? Math.ceil(maxCount / 10) * 10 : 10 : 100,
594-
ticks: { precision: 0, stepSize: 2 }
595-
}
596-
} : {},
597-
animation: {
598-
onComplete: function() {
599-
if (isBar && data.userCounts) {
600-
this.getDatasetMeta(0).data.forEach((bar, index) => {
601-
const count = data.userCounts[index];
602-
if (count > 0) {
603-
ctx.fillText(`${count}`, bar.x - 2.5 , bar.y);
604-
}
605-
});
606-
} else {
607-
const total = data.data.reduce((sum, val) => sum + val, 0);
608-
this.getDatasetMeta(0).data.forEach((element, index) => {
609-
const count = data.data[index];
610-
const percentage = total > 0 ? ((count / total) * 100).toFixed(1) : '0';
611-
if (count > 0) {
612-
const pos = element.tooltipPosition();
613-
ctx.fillText(`${count}(${percentage}%)`, pos.x - 15, pos.y);
614-
}
615-
});
616-
}
617-
resolve(this.toBase64Image());
579+
if (!ctx) { return ''; }
580+
const hasData = Array.isArray(data.data) && data.data.some((value: number) => Number(value) > 0);
581+
582+
if (!hasData) {
583+
ctx.fillStyle = '#666666';
584+
ctx.textAlign = 'center';
585+
ctx.textBaseline = 'middle';
586+
ctx.font = '16px sans-serif';
587+
ctx.fillText('No data available', canvas.width / 2, canvas.height / 2);
588+
return canvas.toDataURL('image/png');
589+
}
590+
591+
const maxCount = Math.max(...data.data);
592+
const chartConfig: ChartConfiguration<'bar' | 'doughnut', number[], string> = {
593+
type: isBar ? 'bar' : 'doughnut',
594+
data: {
595+
labels: data.labels,
596+
datasets: [ {
597+
data: data.data,
598+
label: isRatingScale ? 'selection/choices(1-9)' : (isBar ? '% of responders/selection' : undefined),
599+
backgroundColor: [
600+
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#C9CBCF', '#8DD4F2', '#A8E6CF', '#DCE775'
601+
],
602+
} ]
603+
},
604+
options: {
605+
responsive: false,
606+
maintainAspectRatio: false,
607+
indexAxis: 'x',
608+
plugins: {
609+
legend: {
610+
display: true,
611+
labels: {
612+
boxWidth: isBar ? 0 : 50,
613+
boxHeight: isBar ? 0 : 20
618614
}
619615
}
620-
}
621-
};
622-
return new Chart(ctx, chartConfig);
623-
});
616+
},
617+
scales: isBar ? {
618+
y: {
619+
type: 'linear',
620+
beginAtZero: true,
621+
max: isRatingScale ? maxCount > 0 ? Math.ceil(maxCount / 10) * 10 : 10 : 100,
622+
ticks: { precision: 0, stepSize: 2 }
623+
}
624+
} : {},
625+
animation: false
626+
}
627+
};
628+
629+
const chart = new Chart<'bar' | 'doughnut', number[], string>(ctx, chartConfig);
630+
try {
631+
chart.update();
632+
633+
if (isBar && data.userCounts) {
634+
chart.getDatasetMeta(0).data.forEach((bar, index) => {
635+
const count = data.userCounts[index];
636+
if (count > 0) {
637+
ctx.fillText(`${count}`, bar.x - 2.5 , bar.y);
638+
}
639+
});
640+
} else {
641+
const total = data.data.reduce((sum, val) => sum + val, 0);
642+
chart.getDatasetMeta(0).data.forEach((element, index) => {
643+
const count = data.data[index];
644+
const percentage = total > 0 ? ((count / total) * 100).toFixed(1) : '0';
645+
if (count > 0) {
646+
const pos = element.tooltipPosition();
647+
ctx.fillText(`${count}(${percentage}%)`, pos.x - 15, pos.y);
648+
}
649+
});
650+
}
651+
652+
return chart.toBase64Image();
653+
} finally {
654+
chart.destroy();
655+
}
624656
}
625657

626658
calculateAverageRating(question, submissions): number {

0 commit comments

Comments
 (0)