scatter
This commit is contained in:
		
							parent
							
								
									f740076d60
								
							
						
					
					
						commit
						1b17bb706d
					
				| @ -1,4 +1,4 @@ | |||||||
| <div class="doughnut-chart-container"> | <div class="chart-container"> | ||||||
|   <!-- Filter Controls Section --> |   <!-- Filter Controls Section --> | ||||||
|   <div class="filter-section" *ngIf="hasActiveFilters()"> |   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||||
|     <!-- Base Filters --> |     <!-- Base Filters --> | ||||||
| @ -245,34 +245,36 @@ | |||||||
|   </div> |   </div> | ||||||
|    |    | ||||||
|   <!-- Header row with chart title and drilldown navigation --> |   <!-- Header row with chart title and drilldown navigation --> | ||||||
|   <div class="clr-row header-row"> |   <div class="chart-header"> | ||||||
|     <div class="clr-col-6"> |     <div class="clr-row header-row"> | ||||||
|       <h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3> |       <div class="clr-col-6"> | ||||||
|  |         <h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3> | ||||||
|  |       </div> | ||||||
|  |       <div class="clr-col-6" style="text-align: right;"> | ||||||
|  |         <!-- Add drilldown navigation controls --> | ||||||
|  |         <button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()"> | ||||||
|  |           <cds-icon shape="arrow" direction="left"></cds-icon> | ||||||
|  |           Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}} | ||||||
|  |         </button> | ||||||
|  |       </div> | ||||||
|     </div> |     </div> | ||||||
|     <div class="clr-col-6" style="text-align: right;"> |      | ||||||
|       <!-- Add drilldown navigation controls --> |     <!-- Show current drilldown level --> | ||||||
|       <button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()"> |     <div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0"> | ||||||
|         <cds-icon shape="arrow" direction="left"></cds-icon> |       <div class="clr-col-12"> | ||||||
|         Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}} |         <div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;"> | ||||||
|       </button> |           <div class="alert-items"> | ||||||
|     </div> |             <div class="alert-item static"> | ||||||
|   </div> |               <div class="alert-icon-wrapper"> | ||||||
|    |                 <cds-icon class="alert-icon" shape="info-circle"></cds-icon> | ||||||
|   <!-- Show current drilldown level --> |               </div> | ||||||
|   <div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0"> |               <span class="alert-text"> | ||||||
|     <div class="clr-col-12"> |                 Drilldown Level: {{currentDrilldownLevel}}  | ||||||
|       <div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;"> |                 <span *ngIf="drilldownStack.length > 0"> | ||||||
|         <div class="alert-items"> |                   (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}}) | ||||||
|           <div class="alert-item static"> |                 </span> | ||||||
|             <div class="alert-icon-wrapper"> |  | ||||||
|               <cds-icon class="alert-icon" shape="info-circle"></cds-icon> |  | ||||||
|             </div> |  | ||||||
|             <span class="alert-text"> |  | ||||||
|               Drilldown Level: {{currentDrilldownLevel}}  |  | ||||||
|               <span *ngIf="drilldownStack.length > 0"> |  | ||||||
|                 (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}}) |  | ||||||
|               </span> |               </span> | ||||||
|             </span> |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
| @ -280,7 +282,7 @@ | |||||||
|   </div> |   </div> | ||||||
|    |    | ||||||
|   <div class="chart-wrapper"> |   <div class="chart-wrapper"> | ||||||
|     <div class="chart-content" [class.loading]="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable"> |     <div class="chart-content" [class.loading]="isLoading"> | ||||||
|       <!-- Show no data message --> |       <!-- Show no data message --> | ||||||
|       <div class="no-data-message" *ngIf="noDataAvailable"> |       <div class="no-data-message" *ngIf="noDataAvailable"> | ||||||
|         <p>No chart data available</p> |         <p>No chart data available</p> | ||||||
| @ -289,7 +291,7 @@ | |||||||
|       <!-- Show chart when data is available --> |       <!-- Show chart when data is available --> | ||||||
|       <canvas baseChart  |       <canvas baseChart  | ||||||
|         *ngIf="!noDataAvailable && doughnutChartLabels.length > 0 && doughnutChartData.length > 0" |         *ngIf="!noDataAvailable && doughnutChartLabels.length > 0 && doughnutChartData.length > 0" | ||||||
|         [data]="doughnutChartData"  |         [datasets]="doughnutChartData"  | ||||||
|         [labels]="doughnutChartLabels"  |         [labels]="doughnutChartLabels"  | ||||||
|         [type]="doughnutChartType" |         [type]="doughnutChartType" | ||||||
|         [options]="doughnutChartOptions" |         [options]="doughnutChartOptions" | ||||||
| @ -298,7 +300,7 @@ | |||||||
|       </canvas> |       </canvas> | ||||||
|        |        | ||||||
|       <!-- Loading overlay --> |       <!-- Loading overlay --> | ||||||
|       <div class="loading-overlay" *ngIf="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable"> |       <div class="loading-overlay" *ngIf="isLoading"> | ||||||
|         <div class="shimmer-donut"></div> |         <div class="shimmer-donut"></div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| @ -308,7 +310,7 @@ | |||||||
|     <div class="legend-item" *ngFor="let label of doughnutChartLabels; let i = index"> |     <div class="legend-item" *ngFor="let label of doughnutChartLabels; let i = index"> | ||||||
|       <span class="legend-color" [style.background-color]="getLegendColor(i)"></span> |       <span class="legend-color" [style.background-color]="getLegendColor(i)"></span> | ||||||
|       <span class="legend-label">{{ label }}</span> |       <span class="legend-label">{{ label }}</span> | ||||||
|       <span class="legend-value">{{ doughnutChartData && doughnutChartData[i] !== undefined ? doughnutChartData[i] : 0 }}</span> |       <span class="legend-value">{{ doughnutChartData && doughnutChartData[0] && doughnutChartData[0].data && doughnutChartData[0].data[i] !== undefined ? doughnutChartData[0].data[i] : 0 }}</span> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
| @ -1,239 +1,292 @@ | |||||||
| .doughnut-chart-container { | // Chart container structure - simplified to match shield dashboard | ||||||
|  | .chart-container { | ||||||
|  |   height: 100%; | ||||||
|  |   min-height: 400px; // Ensure minimum height | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   height: 400px; |  | ||||||
|   min-height: 400px; |  | ||||||
|   padding: 20px; |  | ||||||
|   background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); |  | ||||||
|   border-radius: 12px; |  | ||||||
|   box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); |  | ||||||
|   font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |  | ||||||
|   transition: all 0.3s ease; |  | ||||||
|   border: 1px solid #eaeaea; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .doughnut-chart-container:hover { |  | ||||||
|   box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2); |  | ||||||
|   transform: translateY(-2px); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .compact-filters-container { |  | ||||||
|   display: flex; |  | ||||||
|   flex-wrap: wrap; |  | ||||||
|   gap: 5px; |  | ||||||
|   margin-bottom: 10px; |  | ||||||
|   padding: 5px; |  | ||||||
|   background: #ffffff; |  | ||||||
|   border: 1px solid #e9ecef; |  | ||||||
|   border-radius: 6px; |  | ||||||
|   min-height: 40px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .drilldown-indicator { |  | ||||||
|   background-color: #e0e0e0; |  | ||||||
|   padding: 10px; |  | ||||||
|   margin-bottom: 15px; |  | ||||||
|   border-radius: 8px; |  | ||||||
|   text-align: center; |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: center; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 10px; |  | ||||||
|   box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .drilldown-text { |  | ||||||
|   font-weight: bold; |  | ||||||
|   color: #333; |  | ||||||
|   font-size: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .btn { |  | ||||||
|   padding: 6px 12px; |  | ||||||
|   border: none; |  | ||||||
|   border-radius: 4px; |  | ||||||
|   cursor: pointer; |  | ||||||
|   font-size: 14px; |  | ||||||
|   transition: all 0.3s ease; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .btn:hover { |  | ||||||
|   transform: translateY(-2px); |  | ||||||
|   box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .btn-sm { |  | ||||||
|   padding: 4px 8px; |  | ||||||
|   font-size: 12px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .btn-secondary { |  | ||||||
|   background-color: #007cba; |  | ||||||
|   color: white; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .btn-danger { |  | ||||||
|   background-color: #dc3545; |  | ||||||
|   color: white; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .chart-header { |  | ||||||
|   margin-bottom: 20px; |  | ||||||
|    |    | ||||||
|   .chart-title { |   // Filter section styling | ||||||
|     font-size: 22px; |   .filter-section { | ||||||
|     font-weight: 600; |     margin-bottom: 20px; | ||||||
|     color: #0a192f; |     padding: 15px; | ||||||
|     margin: 0; |     border: 1px solid #ddd; | ||||||
|     text-align: center; |     border-radius: 4px; | ||||||
|     padding-bottom: 10px; |     background-color: #f9f9f9; | ||||||
|     border-bottom: 2px solid #3498db; |  | ||||||
|   } |   } | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| .chart-wrapper { |   .filter-group { | ||||||
|   position: relative; |     margin-bottom: 15px; | ||||||
|   flex: 1; |  | ||||||
|   min-height: 250px; |  | ||||||
|   margin: 15px 0; |  | ||||||
|   background: #f8f9fa; |  | ||||||
|   border: 1px solid #e9ecef; |  | ||||||
|   border-radius: 8px; |  | ||||||
|   padding: 10px; |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .chart-wrapper canvas { |  | ||||||
|   max-width: 100%; |  | ||||||
|   max-height: 100%; |  | ||||||
|   filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1)); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .chart-wrapper canvas:hover { |  | ||||||
|   filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15)); |  | ||||||
|   transform: scale(1.02); |  | ||||||
|   transition: all 0.3s ease; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .chart-content { |  | ||||||
|   position: relative; |  | ||||||
|   height: 100%; |  | ||||||
|   width: 100%; |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: center; |  | ||||||
|    |  | ||||||
|   &.loading { |  | ||||||
|     opacity: 0.7; |  | ||||||
|      |      | ||||||
|     canvas { |     h4 { | ||||||
|       filter: blur(2px); |       margin-top: 0; | ||||||
|  |       margin-bottom: 10px; | ||||||
|  |       color: #333; | ||||||
|  |       font-weight: 600; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|    | 
 | ||||||
|   .no-data-message { |   .filter-controls { | ||||||
|     text-align: center; |  | ||||||
|     padding: 30px; |  | ||||||
|     color: #666; |  | ||||||
|     font-size: 18px; |  | ||||||
|     font-style: italic; |  | ||||||
|     display: flex; |     display: flex; | ||||||
|     flex-direction: column; |     flex-wrap: wrap; | ||||||
|     align-items: center; |     gap: 15px; | ||||||
|     justify-content: center; |   } | ||||||
|     height: 100%; | 
 | ||||||
|  |   .filter-item { | ||||||
|  |     flex: 1 1 300px; | ||||||
|  |     min-width: 250px; | ||||||
|  |     padding: 10px; | ||||||
|  |     background: white; | ||||||
|  |     border: 1px solid #e0e0e0; | ||||||
|  |     border-radius: 4px; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .filter-label { | ||||||
|  |     font-weight: 500; | ||||||
|  |     margin-bottom: 8px; | ||||||
|  |     color: #555; | ||||||
|  |     font-size: 14px; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .filter-input { | ||||||
|     width: 100%; |     width: 100%; | ||||||
|  |      | ||||||
|  |     .filter-text-input, | ||||||
|  |     .filter-select, | ||||||
|  |     .filter-date { | ||||||
|  |       width: 100%; | ||||||
|  |       padding: 6px 12px; | ||||||
|  |       border: 1px solid #ccc; | ||||||
|  |       border-radius: 4px; | ||||||
|  |       font-size: 14px; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .filter-select { | ||||||
|  |       height: 34px; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|    | 
 | ||||||
|   .no-data-message p { |   .multiselect-container { | ||||||
|     margin: 0; |     position: relative; | ||||||
|   } |   } | ||||||
|    | 
 | ||||||
|   .loading-overlay { |   .multiselect-display { | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: space-between; | ||||||
|  |     padding: 6px 12px; | ||||||
|  |     border: 1px solid #ccc; | ||||||
|  |     border-radius: 4px; | ||||||
|  |     background: white; | ||||||
|  |     cursor: pointer; | ||||||
|  |     min-height: 34px; | ||||||
|  |      | ||||||
|  |     .multiselect-label { | ||||||
|  |       flex: 1; | ||||||
|  |       font-size: 14px; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .multiselect-value { | ||||||
|  |       color: #666; | ||||||
|  |       font-size: 12px; | ||||||
|  |       margin-right: 8px; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .dropdown-icon { | ||||||
|  |       flex-shrink: 0; | ||||||
|  |       transition: transform 0.2s ease; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     &:hover { | ||||||
|  |       border-color: #999; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .multiselect-dropdown { | ||||||
|     position: absolute; |     position: absolute; | ||||||
|     top: 0; |     top: 100%; | ||||||
|     left: 0; |     left: 0; | ||||||
|     right: 0; |     right: 0; | ||||||
|     bottom: 0; |     z-index: 1000; | ||||||
|     display: flex; |     background: white; | ||||||
|     align-items: center; |     border: 1px solid #ccc; | ||||||
|     justify-content: center; |     border-top: none; | ||||||
|     background: rgba(255, 255, 255, 0.8); |     border-radius: 0 0 4px 4px; | ||||||
|  |     box-shadow: 0 2px 4px rgba(0,0,0,0.1); | ||||||
|  |     max-height: 200px; | ||||||
|  |     overflow-y: auto; | ||||||
|      |      | ||||||
|     .shimmer-donut { |     .checkbox-group { | ||||||
|       width: 120px; |       padding: 8px; | ||||||
|       height: 120px; |        | ||||||
|       border-radius: 50%; |       .checkbox-item { | ||||||
|       background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); |         display: flex; | ||||||
|       background-size: 200% 100%; |         align-items: center; | ||||||
|       animation: shimmer 1.5s infinite; |         gap: 8px; | ||||||
|  |         padding: 4px 0; | ||||||
|  |          | ||||||
|  |         .checkbox-label { | ||||||
|  |           margin: 0; | ||||||
|  |           font-size: 14px; | ||||||
|  |           cursor: pointer; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| .chart-legend { |   .date-range { | ||||||
|   display: flex; |     .date-input-group { | ||||||
|   flex-wrap: wrap; |       display: flex; | ||||||
|   justify-content: center; |       align-items: center; | ||||||
|   gap: 15px; |       gap: 8px; | ||||||
|   margin-top: 20px; |     } | ||||||
|   padding: 20px; |      | ||||||
|   background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); |     .date-separator { | ||||||
|   border-radius: 8px; |       margin: 0 5px; | ||||||
|   border: 1px solid #dee2e6; |       color: #777; | ||||||
|   box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); |     } | ||||||
| } |   } | ||||||
| 
 | 
 | ||||||
| .legend-item { |   .toggle { | ||||||
|   display: flex; |     display: flex; | ||||||
|   align-items: center; |     align-items: center; | ||||||
|   padding: 12px 20px; |     gap: 8px; | ||||||
|   background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); |      | ||||||
|   border-radius: 25px; |     .toggle-label { | ||||||
|   box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); |       margin: 0; | ||||||
|   transition: all 0.3s ease; |       font-size: 14px; | ||||||
|   border: 1px solid #eaeaea; |       cursor: pointer; | ||||||
|   cursor: pointer; |     } | ||||||
| } |   } | ||||||
| 
 | 
 | ||||||
| .legend-item:hover { |   .filter-actions { | ||||||
|   transform: translateY(-3px); |     margin-top: 15px; | ||||||
|   box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); |     padding-top: 15px; | ||||||
|   border-color: #3498db; |     border-top: 1px solid #eee; | ||||||
|   background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); |      | ||||||
| } |     .btn { | ||||||
| 
 |       font-size: 13px; | ||||||
| .legend-color { |     } | ||||||
|   width: 20px; |   } | ||||||
|   height: 20px; |    | ||||||
|   border-radius: 50%; |   // Chart header styling | ||||||
|   margin-right: 12px; |   .chart-header { | ||||||
|   display: inline-block; |     margin-bottom: 20px; | ||||||
|   border: 2px solid white; |      | ||||||
|   box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); |     .header-row { | ||||||
| } |       margin-bottom: 15px; | ||||||
| 
 |       padding-bottom: 10px; | ||||||
| .legend-label { |       border-bottom: 1px solid #eee; | ||||||
|   font-size: 16px; |        | ||||||
|   font-weight: 600; |       .chart-title { | ||||||
|   color: #2c3e50; |         margin: 0; | ||||||
|   margin-right: 15px; |         font-size: 18px; | ||||||
|   white-space: nowrap; |         font-weight: 600; | ||||||
|   text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); |         color: #0a192f; | ||||||
| } |       } | ||||||
| 
 |     } | ||||||
| .legend-value { |   } | ||||||
|   font-size: 16px; |    | ||||||
|   font-weight: 700; |   // Chart wrapper and content - simplified to match shield dashboard | ||||||
|   color: #3498db; |   .chart-wrapper { | ||||||
|   background: linear-gradient(135deg, #e9ecef 0%, #dde1e5 100%); |     flex: 1; | ||||||
|   padding: 6px 12px; |     position: relative; | ||||||
|   border-radius: 12px; |      | ||||||
|   box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); |     .chart-content { | ||||||
|   min-width: 40px; |       position: relative; | ||||||
|   text-align: center; |       height: 100%; | ||||||
|  |       min-height: 300px; | ||||||
|  |        | ||||||
|  |       &.loading { | ||||||
|  |         opacity: 0.7; | ||||||
|  |          | ||||||
|  |         canvas { | ||||||
|  |           filter: blur(2px); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .no-data-message { | ||||||
|  |         text-align: center; | ||||||
|  |         padding: 20px; | ||||||
|  |         color: #666; | ||||||
|  |         font-style: italic; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       canvas { | ||||||
|  |         max-width: 100%; | ||||||
|  |         max-height: calc(100% - 40px); // Leave space for legend | ||||||
|  |         transition: filter 0.3s ease; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .loading-overlay { | ||||||
|  |         position: absolute; | ||||||
|  |         top: 0; | ||||||
|  |         left: 0; | ||||||
|  |         right: 0; | ||||||
|  |         bottom: 0; | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         justify-content: center; | ||||||
|  |         background: rgba(255, 255, 255, 0.8); | ||||||
|  |          | ||||||
|  |         .shimmer-donut { | ||||||
|  |           width: 120px; | ||||||
|  |           height: 120px; | ||||||
|  |           border-radius: 50%; | ||||||
|  |           background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); | ||||||
|  |           background-size: 200% 100%; | ||||||
|  |           animation: shimmer 1.5s infinite; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Chart legend - simplified | ||||||
|  |   .chart-legend { | ||||||
|  |     display: flex; | ||||||
|  |     flex-wrap: wrap; | ||||||
|  |     justify-content: center; | ||||||
|  |     gap: 15px; | ||||||
|  |     margin: 20px 0; | ||||||
|  |     padding: 15px; | ||||||
|  |      | ||||||
|  |     .legend-item { | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       padding: 8px 15px; | ||||||
|  |       background: #f8f9fa; | ||||||
|  |       border-radius: 20px; | ||||||
|  |       border: 1px solid #e9ecef; | ||||||
|  |       min-width: 120px; | ||||||
|  |       justify-content: space-between; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .legend-color { | ||||||
|  |       width: 15px; | ||||||
|  |       height: 15px; | ||||||
|  |       border-radius: 50%; | ||||||
|  |       margin-right: 8px; | ||||||
|  |       display: inline-block; | ||||||
|  |       flex-shrink: 0; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .legend-label { | ||||||
|  |       font-size: 14px; | ||||||
|  |       font-weight: 500; | ||||||
|  |       color: #2c3e50; | ||||||
|  |       margin-right: 10px; | ||||||
|  |       white-space: nowrap; | ||||||
|  |       overflow: hidden; | ||||||
|  |       text-overflow: ellipsis; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .legend-value { | ||||||
|  |       font-size: 14px; | ||||||
|  |       font-weight: 600; | ||||||
|  |       color: #3498db; | ||||||
|  |       min-width: 30px; | ||||||
|  |       text-align: right; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @keyframes shimmer { | @keyframes shimmer { | ||||||
| @ -245,240 +298,46 @@ | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Filter section styles | // Responsive design | ||||||
| .filter-section { | @media (max-width: 768px) { | ||||||
|   margin-bottom: 20px; |   .chart-container { | ||||||
|   padding: 15px; |     .filter-controls { | ||||||
|   border: 1px solid #ddd; |       flex-direction: column; | ||||||
|   border-radius: 4px; |     } | ||||||
|   background-color: #f9f9f9; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .filter-group { |  | ||||||
|   margin-bottom: 15px; |  | ||||||
|    |  | ||||||
|   h4 { |  | ||||||
|     margin-top: 0; |  | ||||||
|     margin-bottom: 10px; |  | ||||||
|     color: #333; |  | ||||||
|     font-weight: 600; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .filter-controls { |  | ||||||
|   display: flex; |  | ||||||
|   flex-wrap: wrap; |  | ||||||
|   gap: 15px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .filter-item { |  | ||||||
|   flex: 1 1 300px; |  | ||||||
|   min-width: 250px; |  | ||||||
|   padding: 10px; |  | ||||||
|   background: white; |  | ||||||
|   border: 1px solid #e0e0e0; |  | ||||||
|   border-radius: 4px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .filter-label { |  | ||||||
|   font-weight: 500; |  | ||||||
|   margin-bottom: 8px; |  | ||||||
|   color: #555; |  | ||||||
|   font-size: 14px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .filter-input { |  | ||||||
|   width: 100%; |  | ||||||
|    |  | ||||||
|   .filter-text-input, |  | ||||||
|   .filter-select, |  | ||||||
|   .filter-date { |  | ||||||
|     width: 100%; |  | ||||||
|     padding: 6px 12px; |  | ||||||
|     border: 1px solid #ccc; |  | ||||||
|     border-radius: 4px; |  | ||||||
|     font-size: 14px; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .filter-select { |  | ||||||
|     height: 34px; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .multiselect-container { |  | ||||||
|   position: relative; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .multiselect-display { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   padding: 6px 12px; |  | ||||||
|   border: 1px solid #ccc; |  | ||||||
|   border-radius: 4px; |  | ||||||
|   background: white; |  | ||||||
|   cursor: pointer; |  | ||||||
|   min-height: 34px; |  | ||||||
|    |  | ||||||
|   .multiselect-label { |  | ||||||
|     flex: 1; |  | ||||||
|     font-size: 14px; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .multiselect-value { |  | ||||||
|     color: #666; |  | ||||||
|     font-size: 12px; |  | ||||||
|     margin-right: 8px; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .dropdown-icon { |  | ||||||
|     flex-shrink: 0; |  | ||||||
|     transition: transform 0.2s ease; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   &:hover { |  | ||||||
|     border-color: #999; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .multiselect-dropdown { |  | ||||||
|   position: absolute; |  | ||||||
|   top: 100%; |  | ||||||
|   left: 0; |  | ||||||
|   right: 0; |  | ||||||
|   z-index: 1000; |  | ||||||
|   background: white; |  | ||||||
|   border: 1px solid #ccc; |  | ||||||
|   border-top: none; |  | ||||||
|   border-radius: 0 0 4px 4px; |  | ||||||
|   box-shadow: 0 2px 4px rgba(0,0,0,0.1); |  | ||||||
|   max-height: 200px; |  | ||||||
|   overflow-y: auto; |  | ||||||
|    |  | ||||||
|   .checkbox-group { |  | ||||||
|     padding: 8px; |  | ||||||
|      |      | ||||||
|     .checkbox-item { |     .filter-item { | ||||||
|       display: flex; |       min-width: 100%; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .chart-header { | ||||||
|  |       .header-row { | ||||||
|  |         .chart-title { | ||||||
|  |           font-size: 16px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .chart-content { | ||||||
|  |       min-height: 250px; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .chart-legend { | ||||||
|  |       flex-direction: column; | ||||||
|       align-items: center; |       align-items: center; | ||||||
|       gap: 8px; |  | ||||||
|       padding: 4px 0; |  | ||||||
|        |        | ||||||
|       .checkbox-label { |       .legend-item { | ||||||
|         margin: 0; |         width: 100%; | ||||||
|         font-size: 14px; |         max-width: 300px; | ||||||
|         cursor: pointer; |         justify-content: space-between; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .chart-content { | ||||||
|  |       min-height: 250px; | ||||||
|  |        | ||||||
|  |       canvas { | ||||||
|  |         max-height: calc(100% - 60px); // More space for legend on mobile | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .date-range { |  | ||||||
|   .date-input-group { |  | ||||||
|     display: flex; |  | ||||||
|     align-items: center; |  | ||||||
|     gap: 8px; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .date-separator { |  | ||||||
|     margin: 0 5px; |  | ||||||
|     color: #777; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .toggle { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
|    |  | ||||||
|   .toggle-label { |  | ||||||
|     margin: 0; |  | ||||||
|     font-size: 14px; |  | ||||||
|     cursor: pointer; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .filter-actions { |  | ||||||
|   margin-top: 15px; |  | ||||||
|   padding-top: 15px; |  | ||||||
|   border-top: 1px solid #eee; |  | ||||||
|    |  | ||||||
|   .btn { |  | ||||||
|     font-size: 13px; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // New header row styling |  | ||||||
| .header-row { |  | ||||||
|   margin-bottom: 15px; |  | ||||||
|   padding-bottom: 10px; |  | ||||||
|   border-bottom: 1px solid #eee; |  | ||||||
|    |  | ||||||
|   .chart-title { |  | ||||||
|     margin: 0; |  | ||||||
|     font-size: 18px; |  | ||||||
|     font-weight: 600; |  | ||||||
|     color: #333; |  | ||||||
|     text-align: left; |  | ||||||
|     padding-bottom: 0; |  | ||||||
|     border-bottom: none; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /* Responsive design */ |  | ||||||
| @media (max-width: 768px) { |  | ||||||
|   .doughnut-chart-container { |  | ||||||
|     padding: 15px; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .chart-header .chart-title { |  | ||||||
|     font-size: 18px; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .drilldown-indicator { |  | ||||||
|     flex-direction: column; |  | ||||||
|     gap: 5px; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .drilldown-text { |  | ||||||
|     font-size: 14px; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .chart-wrapper { |  | ||||||
|     min-height: 200px; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .chart-legend { |  | ||||||
|     flex-direction: column; |  | ||||||
|     align-items: center; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .legend-item { |  | ||||||
|     width: 100%; |  | ||||||
|     max-width: 300px; |  | ||||||
|     justify-content: space-between; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .no-data-message { |  | ||||||
|     font-size: 16px; |  | ||||||
|     padding: 20px; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .compact-filters-container { |  | ||||||
|     flex-wrap: wrap; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .filter-controls { |  | ||||||
|     flex-direction: column; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .filter-item { |  | ||||||
|     min-width: 100%; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .header-row { |  | ||||||
|     .chart-title { |  | ||||||
|       font-size: 16px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| @ -36,7 +36,21 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|   @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
 |   @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
 | ||||||
| 
 | 
 | ||||||
|   public doughnutChartLabels: string[] = ["Category A", "Category B", "Category C"]; |   public doughnutChartLabels: string[] = ["Category A", "Category B", "Category C"]; | ||||||
|   public doughnutChartData: number[] = [30, 50, 20]; |   public doughnutChartData: any[] = [ | ||||||
|  |     { | ||||||
|  |       data: [30, 50, 20], | ||||||
|  |       backgroundColor: [ | ||||||
|  |         '#FF6384', | ||||||
|  |         '#36A2EB', | ||||||
|  |         '#FFCE56' | ||||||
|  |       ], | ||||||
|  |       hoverBackgroundColor: [ | ||||||
|  |         '#FF6384', | ||||||
|  |         '#36A2EB', | ||||||
|  |         '#FFCE56' | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|   public doughnutChartType: string = "doughnut"; |   public doughnutChartType: string = "doughnut"; | ||||||
|   public doughnutChartOptions: any = { |   public doughnutChartOptions: any = { | ||||||
|     responsive: true, |     responsive: true, | ||||||
| @ -72,6 +86,14 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|         borderWidth: 2, |         borderWidth: 2, | ||||||
|         borderColor: '#fff' |         borderColor: '#fff' | ||||||
|       } |       } | ||||||
|  |     }, | ||||||
|  |     layout: { | ||||||
|  |       padding: { | ||||||
|  |         top: 20, | ||||||
|  |         bottom: 20, | ||||||
|  |         left: 20, | ||||||
|  |         right: 20 | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|    |    | ||||||
| @ -96,6 +118,9 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|   // No data state
 |   // No data state
 | ||||||
|   noDataAvailable: boolean = false; |   noDataAvailable: boolean = false; | ||||||
|    |    | ||||||
|  |   // Loading state
 | ||||||
|  |   isLoading: boolean = false; | ||||||
|  |    | ||||||
|   // Flag to prevent infinite loops
 |   // Flag to prevent infinite loops
 | ||||||
|   private isFetchingData: boolean = false; |   private isFetchingData: boolean = false; | ||||||
|    |    | ||||||
| @ -143,17 +168,33 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|     if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) { |     if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) { | ||||||
|       // Add default data to ensure chart visibility
 |       // Add default data to ensure chart visibility
 | ||||||
|       this.doughnutChartLabels = ["Category A", "Category B", "Category C"]; |       this.doughnutChartLabels = ["Category A", "Category B", "Category C"]; | ||||||
|       this.doughnutChartData = [30, 50, 20]; |       this.doughnutChartData = [ | ||||||
|  |         { | ||||||
|  |           data: [30, 50, 20], | ||||||
|  |           backgroundColor: [ | ||||||
|  |             '#FF6384', | ||||||
|  |             '#36A2EB', | ||||||
|  |             '#FFCE56' | ||||||
|  |           ], | ||||||
|  |           hoverBackgroundColor: [ | ||||||
|  |             '#FF6384', | ||||||
|  |             '#36A2EB', | ||||||
|  |             '#FFCE56' | ||||||
|  |           ] | ||||||
|  |         } | ||||||
|  |       ]; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     // Ensure we have matching arrays
 |     // Ensure we have matching arrays
 | ||||||
|     if (this.doughnutChartLabels.length !== this.doughnutChartData.length) { |     if (this.doughnutChartLabels.length !== (this.doughnutChartData[0]?.data?.length || 0)) { | ||||||
|       const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length); |       const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData[0]?.data?.length || 0); | ||||||
|       while (this.doughnutChartLabels.length < maxLength) { |       while (this.doughnutChartLabels.length < maxLength) { | ||||||
|         this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`); |         this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`); | ||||||
|       } |       } | ||||||
|       while (this.doughnutChartData.length < maxLength) { |       if (this.doughnutChartData[0]) { | ||||||
|         this.doughnutChartData.push(0); |         while (this.doughnutChartData[0].data.length < maxLength) { | ||||||
|  |           this.doughnutChartData[0].data.push(0); | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -529,14 +570,18 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|   } |   } | ||||||
|    |    | ||||||
|   fetchChartData(): void { |   fetchChartData(): void { | ||||||
|  |     // Set loading state
 | ||||||
|  |     this.isLoading = true; | ||||||
|  |      | ||||||
|     // Set flag to prevent recursive calls
 |     // Set flag to prevent recursive calls
 | ||||||
|     this.isFetchingData = true; |     this.isFetchingData = true; | ||||||
|      |      | ||||||
|     // If we're in drilldown mode, fetch the appropriate drilldown data
 |     // If we're in drilldown mode, fetch the appropriate drilldown data
 | ||||||
|     if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) { |     if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) { | ||||||
|       this.fetchDrilldownData(); |       this.fetchDrilldownData(); | ||||||
|       // Reset flag after fetching
 |       // Reset flags after fetching
 | ||||||
|       this.isFetchingData = false; |       this.isFetchingData = false; | ||||||
|  |       this.isLoading = false; | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|      |      | ||||||
| @ -618,8 +663,9 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|             this.noDataAvailable = true; |             this.noDataAvailable = true; | ||||||
|             this.doughnutChartLabels = []; |             this.doughnutChartLabels = []; | ||||||
|             this.doughnutChartData = []; |             this.doughnutChartData = []; | ||||||
|             // Reset flag after fetching
 |             // Reset flags after fetching
 | ||||||
|             this.isFetchingData = false; |             this.isFetchingData = false; | ||||||
|  |             this.isLoading = false; | ||||||
|             return; |             return; | ||||||
|           } |           } | ||||||
|            |            | ||||||
| @ -628,34 +674,44 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|             // Backend has already filtered the data, just display it
 |             // Backend has already filtered the data, just display it
 | ||||||
|             this.noDataAvailable = data.chartLabels.length === 0; |             this.noDataAvailable = data.chartLabels.length === 0; | ||||||
|             this.doughnutChartLabels = data.chartLabels; |             this.doughnutChartLabels = data.chartLabels; | ||||||
|             this.doughnutChartData = data.chartData; |             this.doughnutChartData = [ | ||||||
|             // Trigger change detection
 |               { | ||||||
|             this.doughnutChartData = [...this.doughnutChartData]; |                 data: data.chartData, | ||||||
|  |                 backgroundColor: this.chartColors.slice(0, data.chartData.length), | ||||||
|  |                 hoverBackgroundColor: this.chartColors.slice(0, data.chartData.length) | ||||||
|  |               } | ||||||
|  |             ]; | ||||||
|             console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); |             console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); | ||||||
|           } else if (data && data.labels && data.datasets) { |           } else if (data && data.labels && data.datasets) { | ||||||
|             // Backend has already filtered the data, just display it
 |             // Backend has already filtered the data, just display it
 | ||||||
|             this.noDataAvailable = data.labels.length === 0; |             this.noDataAvailable = data.labels.length === 0; | ||||||
|             this.doughnutChartLabels = data.labels; |             this.doughnutChartLabels = data.labels; | ||||||
|             this.doughnutChartData = data.datasets[0]?.data || []; |             this.doughnutChartData = data.datasets; | ||||||
|             // Trigger change detection
 |  | ||||||
|             this.doughnutChartData = [...this.doughnutChartData]; |  | ||||||
|             console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); |             console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); | ||||||
|           } else { |           } else { | ||||||
|             console.warn('Received data does not have expected structure', data); |             console.warn('Received data does not have expected structure', data); | ||||||
|             this.noDataAvailable = true; |             this.noDataAvailable = true; | ||||||
|             this.doughnutChartLabels = []; |             this.doughnutChartLabels = []; | ||||||
|             this.doughnutChartData = []; |             this.doughnutChartData = [ | ||||||
|  |               { | ||||||
|  |                 data: [], | ||||||
|  |                 backgroundColor: [], | ||||||
|  |                 hoverBackgroundColor: [] | ||||||
|  |               } | ||||||
|  |             ]; | ||||||
|           } |           } | ||||||
|           // Reset flag after fetching
 |           // Reset flags after fetching
 | ||||||
|           this.isFetchingData = false; |           this.isFetchingData = false; | ||||||
|  |           this.isLoading = false; | ||||||
|         }, |         }, | ||||||
|         (error) => { |         (error) => { | ||||||
|           console.error('Error fetching doughnut chart data:', error); |           console.error('Error fetching doughnut chart data:', error); | ||||||
|           this.noDataAvailable = true; |           this.noDataAvailable = true; | ||||||
|           this.doughnutChartLabels = []; |           this.doughnutChartLabels = []; | ||||||
|           this.doughnutChartData = []; |           this.doughnutChartData = []; | ||||||
|           // Reset flag after fetching
 |           // Reset flags after fetching
 | ||||||
|           this.isFetchingData = false; |           this.isFetchingData = false; | ||||||
|  |           this.isLoading = false; | ||||||
|           // Keep default data in case of error
 |           // Keep default data in case of error
 | ||||||
|         } |         } | ||||||
|       ); |       ); | ||||||
| @ -664,8 +720,9 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|       this.noDataAvailable = true; |       this.noDataAvailable = true; | ||||||
|       this.doughnutChartLabels = []; |       this.doughnutChartLabels = []; | ||||||
|       this.doughnutChartData = []; |       this.doughnutChartData = []; | ||||||
|       // Reset flag after fetching
 |       // Reset flags after fetching
 | ||||||
|       this.isFetchingData = false; |       this.isFetchingData = false; | ||||||
|  |       this.isLoading = false; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|    |    | ||||||
| @ -812,23 +869,37 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|           // Backend has already filtered the data, just display it
 |           // Backend has already filtered the data, just display it
 | ||||||
|           this.noDataAvailable = data.chartLabels.length === 0; |           this.noDataAvailable = data.chartLabels.length === 0; | ||||||
|           this.doughnutChartLabels = data.chartLabels; |           this.doughnutChartLabels = data.chartLabels; | ||||||
|           this.doughnutChartData = data.chartData; |           this.doughnutChartData = [ | ||||||
|           // Trigger change detection
 |             { | ||||||
|           this.doughnutChartData = [...this.doughnutChartData]; |               data: data.chartData, | ||||||
|  |               backgroundColor: this.chartColors.slice(0, data.chartData.length), | ||||||
|  |               hoverBackgroundColor: this.chartColors.slice(0, data.chartData.length) | ||||||
|  |             } | ||||||
|  |           ]; | ||||||
|           console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); |           console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); | ||||||
|  |           // Set loading state to false
 | ||||||
|  |           this.isLoading = false; | ||||||
|         } else if (data && data.labels && data.datasets) { |         } else if (data && data.labels && data.datasets) { | ||||||
|           // Backend has already filtered the data, just display it
 |           // Backend has already filtered the data, just display it
 | ||||||
|           this.noDataAvailable = data.labels.length === 0; |           this.noDataAvailable = data.labels.length === 0; | ||||||
|           this.doughnutChartLabels = data.labels; |           this.doughnutChartLabels = data.labels; | ||||||
|           this.doughnutChartData = data.datasets[0]?.data || []; |           this.doughnutChartData = data.datasets; | ||||||
|           // Trigger change detection
 |  | ||||||
|           this.doughnutChartData = [...this.doughnutChartData]; |  | ||||||
|           console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); |           console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); | ||||||
|  |           // Set loading state to false
 | ||||||
|  |           this.isLoading = false; | ||||||
|         } else { |         } else { | ||||||
|           console.warn('Drilldown received data does not have expected structure', data); |           console.warn('Drilldown received data does not have expected structure', data); | ||||||
|           this.noDataAvailable = true; |           this.noDataAvailable = true; | ||||||
|           this.doughnutChartLabels = []; |           this.doughnutChartLabels = []; | ||||||
|           this.doughnutChartData = []; |           this.doughnutChartData = [ | ||||||
|  |             { | ||||||
|  |               data: [], | ||||||
|  |               backgroundColor: [], | ||||||
|  |               hoverBackgroundColor: [] | ||||||
|  |             } | ||||||
|  |           ]; | ||||||
|  |           // Set loading state to false
 | ||||||
|  |           this.isLoading = false; | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       (error) => { |       (error) => { | ||||||
| @ -836,9 +907,14 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|         this.noDataAvailable = true; |         this.noDataAvailable = true; | ||||||
|         this.doughnutChartLabels = []; |         this.doughnutChartLabels = []; | ||||||
|         this.doughnutChartData = []; |         this.doughnutChartData = []; | ||||||
|  |         // Set loading state to false
 | ||||||
|  |         this.isLoading = false; | ||||||
|         // Keep current data in case of error
 |         // Keep current data in case of error
 | ||||||
|       } |       } | ||||||
|     ); |     ); | ||||||
|  |      | ||||||
|  |     // Set loading state
 | ||||||
|  |     this.isLoading = true; | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   // Reset to original data (go back to base level)
 |   // Reset to original data (go back to base level)
 | ||||||
| @ -855,7 +931,7 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|       console.log('Restored original labels'); |       console.log('Restored original labels'); | ||||||
|     } |     } | ||||||
|     if (this.originalDoughnutChartData.length > 0) { |     if (this.originalDoughnutChartData.length > 0) { | ||||||
|       this.doughnutChartData = [...this.originalDoughnutChartData]; |       this.doughnutChartData = JSON.parse(JSON.stringify(this.originalDoughnutChartData)); | ||||||
|       console.log('Restored original data'); |       console.log('Restored original data'); | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  | |||||||
| @ -288,6 +288,7 @@ | |||||||
|   <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);"> |   <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);"> | ||||||
|     <canvas baseChart |     <canvas baseChart | ||||||
|       [datasets]="scatterChartData" |       [datasets]="scatterChartData" | ||||||
|  |       [options]="scatterChartOptions" | ||||||
|       [type]="scatterChartType" |       [type]="scatterChartType" | ||||||
|       (chartHover)="chartHovered($event)" |       (chartHover)="chartHovered($event)" | ||||||
|       (chartClick)="chartClicked($event)"> |       (chartClick)="chartClicked($event)"> | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||||
| import { ChartData,ChartDataset } from 'chart.js'; | import { ChartData,ChartDataset,ChartOptions } from 'chart.js'; | ||||||
| import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | ||||||
| import { FilterService } from '../../common-filter/filter.service'; | import { FilterService } from '../../common-filter/filter.service'; | ||||||
| import { Subscription } from 'rxjs'; | import { Subscription } from 'rxjs'; | ||||||
| @ -113,6 +113,81 @@ export class ScatterChartComponent implements OnInit, OnChanges { | |||||||
|       ], |       ], | ||||||
|     }, |     }, | ||||||
|   ]; |   ]; | ||||||
|  |    | ||||||
|  |   public scatterChartOptions: any = { | ||||||
|  |     responsive: true, | ||||||
|  |     maintainAspectRatio: false, | ||||||
|  |     scales: { | ||||||
|  |       x: { | ||||||
|  |         type: 'linear', | ||||||
|  |         position: 'bottom', | ||||||
|  |         title: { | ||||||
|  |           display: true, | ||||||
|  |           text: 'X Axis' | ||||||
|  |         }, | ||||||
|  |         ticks: { | ||||||
|  |           autoSkip: true, | ||||||
|  |           maxTicksLimit: 10, | ||||||
|  |           callback: function(value: any) { | ||||||
|  |             if (typeof value === 'number') { | ||||||
|  |               // Format large numbers for better readability
 | ||||||
|  |               if (Math.abs(value) >= 1000000) { | ||||||
|  |                 return (value / 1000000).toFixed(1) + 'M'; | ||||||
|  |               } else if (Math.abs(value) >= 1000) { | ||||||
|  |                 return (value / 1000).toFixed(1) + 'K'; | ||||||
|  |               } | ||||||
|  |               return value.toString(); | ||||||
|  |             } | ||||||
|  |             return value; | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         grid: { | ||||||
|  |           display: true, | ||||||
|  |           color: 'rgba(0, 0, 0, 0.1)' | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       y: { | ||||||
|  |         title: { | ||||||
|  |           display: true, | ||||||
|  |           text: 'Y Axis' | ||||||
|  |         }, | ||||||
|  |         ticks: { | ||||||
|  |           autoSkip: true, | ||||||
|  |           maxTicksLimit: 10, | ||||||
|  |           callback: function(value: any) { | ||||||
|  |             if (typeof value === 'number') { | ||||||
|  |               // Format large numbers for better readability
 | ||||||
|  |               if (Math.abs(value) >= 1000000) { | ||||||
|  |                 return (value / 1000000).toFixed(1) + 'M'; | ||||||
|  |               } else if (Math.abs(value) >= 1000) { | ||||||
|  |                 return (value / 1000).toFixed(1) + 'K'; | ||||||
|  |               } | ||||||
|  |               return value.toString(); | ||||||
|  |             } | ||||||
|  |             return value; | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         grid: { | ||||||
|  |           display: true, | ||||||
|  |           color: 'rgba(0, 0, 0, 0.1)' | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     plugins: { | ||||||
|  |       legend: { | ||||||
|  |         display: true, | ||||||
|  |         position: 'top', | ||||||
|  |       }, | ||||||
|  |       tooltip: { | ||||||
|  |         callbacks: { | ||||||
|  |           label: function(context: any) { | ||||||
|  |             return `(${context.parsed.x}, ${context.parsed.y})`; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   public scatterChartType: string = 'scatter'; |   public scatterChartType: string = 'scatter'; | ||||||
|    |    | ||||||
|   // Multi-layer drilldown state tracking
 |   // Multi-layer drilldown state tracking
 | ||||||
| @ -457,6 +532,56 @@ export class ScatterChartComponent implements OnInit, OnChanges { | |||||||
|       return data; |       return data; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     // Transform the data properly for scatter chart
 | ||||||
|  |     // Assuming labels are x-values and data[0].data are y-values
 | ||||||
|  |     if (labels && data && data.length > 0 && data[0].data) { | ||||||
|  |       const yValues = data[0].data; | ||||||
|  |       const label = data[0].label || 'Dataset 1'; | ||||||
|  |        | ||||||
|  |       // Create scatter points from labels (x) and data (y)
 | ||||||
|  |       const scatterPoints = []; | ||||||
|  |       const minLength = Math.min(labels.length, yValues.length); | ||||||
|  |        | ||||||
|  |       for (let i = 0; i < minLength; i++) { | ||||||
|  |         // Convert to numbers if they're strings
 | ||||||
|  |         const x = typeof labels[i] === 'string' ? parseFloat(labels[i]) : labels[i]; | ||||||
|  |         const y = typeof yValues[i] === 'string' ? parseFloat(yValues[i]) : yValues[i]; | ||||||
|  |          | ||||||
|  |         // Only add valid points
 | ||||||
|  |         if (!isNaN(x) && !isNaN(y)) { | ||||||
|  |           scatterPoints.push({ x, y }); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // Generate different colors for each point to avoid all points showing the same color
 | ||||||
|  |       const backgroundColors = []; | ||||||
|  |       const borderColors = []; | ||||||
|  |        | ||||||
|  |       for (let i = 0; i < scatterPoints.length; i++) { | ||||||
|  |         // Generate a color based on the point index for variety
 | ||||||
|  |         const hue = (i * 137.508) % 360; // Use golden angle to spread colors
 | ||||||
|  |         backgroundColors.push(`hsla(${hue}, 70%, 50%, 0.6)`); | ||||||
|  |         borderColors.push(`hsla(${hue}, 70%, 40%, 1)`); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // Create a single dataset with all scatter points
 | ||||||
|  |       const scatterDatasets: ChartDataset[] = [ | ||||||
|  |         { | ||||||
|  |           data: scatterPoints, | ||||||
|  |           label: label, | ||||||
|  |           pointRadius: 8, | ||||||
|  |           pointHoverRadius: 10, | ||||||
|  |           backgroundColor: backgroundColors, | ||||||
|  |           borderColor: borderColors, | ||||||
|  |           borderWidth: 1, | ||||||
|  |           pointHoverBackgroundColor: 'rgba(255, 99, 132, 1)', | ||||||
|  |         } | ||||||
|  |       ]; | ||||||
|  |        | ||||||
|  |       console.log('Transformed scatter data:', scatterDatasets); | ||||||
|  |       return scatterDatasets; | ||||||
|  |     } | ||||||
|  |      | ||||||
|     // Otherwise, create a default scatter dataset
 |     // Otherwise, create a default scatter dataset
 | ||||||
|     const scatterDatasets: ChartDataset[] = [ |     const scatterDatasets: ChartDataset[] = [ | ||||||
|       { |       { | ||||||
| @ -587,11 +712,19 @@ export class ScatterChartComponent implements OnInit, OnChanges { | |||||||
|             // Scatter charts expect data in the format: {x: number, y: number}
 |             // Scatter charts expect data in the format: {x: number, y: number}
 | ||||||
|             this.noDataAvailable = data.chartLabels.length === 0; |             this.noDataAvailable = data.chartLabels.length === 0; | ||||||
|             this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData); |             this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData); | ||||||
|  |              | ||||||
|  |             // Update chart options with axis titles
 | ||||||
|  |             this.updateChartOptionsWithAxisTitles(); | ||||||
|  |              | ||||||
|             console.log('Updated scatter chart with data:', this.scatterChartData); |             console.log('Updated scatter chart with data:', this.scatterChartData); | ||||||
|           } else if (data && data.labels && data.datasets) { |           } else if (data && data.labels && data.datasets) { | ||||||
|             // Handle the original expected format as fallback
 |             // Handle the original expected format as fallback
 | ||||||
|             this.noDataAvailable = data.labels.length === 0; |             this.noDataAvailable = data.labels.length === 0; | ||||||
|             this.scatterChartData = data.datasets; |             this.scatterChartData = data.datasets; | ||||||
|  |              | ||||||
|  |             // Update chart options with axis titles
 | ||||||
|  |             this.updateChartOptionsWithAxisTitles(); | ||||||
|  |              | ||||||
|             console.log('Updated scatter chart with legacy data format:', this.scatterChartData); |             console.log('Updated scatter chart with legacy data format:', this.scatterChartData); | ||||||
|           } else { |           } else { | ||||||
|             console.warn('Scatter chart received data does not have expected structure', data); |             console.warn('Scatter chart received data does not have expected structure', data); | ||||||
| @ -619,6 +752,20 @@ export class ScatterChartComponent implements OnInit, OnChanges { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|    |    | ||||||
|  |   // Update chart options with axis titles
 | ||||||
|  |   private updateChartOptionsWithAxisTitles(): void { | ||||||
|  |     // Update X axis title
 | ||||||
|  |     if (this.scatterChartOptions.scales && this.scatterChartOptions.scales.x) { | ||||||
|  |       this.scatterChartOptions.scales.x.title.text = this.xAxis || 'X Axis'; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Update Y axis title
 | ||||||
|  |     if (this.scatterChartOptions.scales && this.scatterChartOptions.scales.y) { | ||||||
|  |       const yAxisLabel = Array.isArray(this.yAxis) ? this.yAxis[0] : this.yAxis; | ||||||
|  |       this.scatterChartOptions.scales.y.title.text = yAxisLabel || 'Y Axis'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|   // Fetch drilldown data based on current drilldown level
 |   // Fetch drilldown data based on current drilldown level
 | ||||||
|   fetchDrilldownData(): void { |   fetchDrilldownData(): void { | ||||||
|     console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel); |     console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel); | ||||||
| @ -928,4 +1075,4 @@ export class ScatterChartComponent implements OnInit, OnChanges { | |||||||
|     // Clean up document click handler
 |     // Clean up document click handler
 | ||||||
|     this.removeDocumentClickHandler(); |     this.removeDocumentClickHandler(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user