11import { Injectable } from '@angular/core' ;
22import { Subject , of , forkJoin , throwError } from 'rxjs' ;
33import { 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' ;
55import htmlToPdfmake from 'html-to-pdfmake' ;
66import { findDocuments } from '../shared/mangoQueries' ;
77import { CouchService } from '../shared/couchdb.service' ;
@@ -18,8 +18,24 @@ import { TeamsService } from '../teams/teams.service';
1818import { ChatService } from '../shared/chat.service' ;
1919import { 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+
2138pdfMake . 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