chart
This commit is contained in:
		
							parent
							
								
									87810acc9e
								
							
						
					
					
						commit
						bedcc0822d
					
				| @ -1,14 +1,283 @@ | ||||
| <div style="display:block"> | ||||
|   <!-- Drilldown mode indicator --> | ||||
|   <div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> | ||||
|     <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> | ||||
|     <button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> | ||||
|       Back to Level {{currentDrilldownLevel - 1}} | ||||
|     </button> | ||||
|     <button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> | ||||
|       Back to Main View | ||||
| <div style="display:block; height: 100%; width: 100%;"> | ||||
|   <!-- Filter Controls Section --> | ||||
|   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||
|     <!-- Base Filters --> | ||||
|     <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0"> | ||||
|       <h4>Base Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of baseFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'base-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Drilldown Filters --> | ||||
|     <div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0"> | ||||
|       <h4>Drilldown Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of drilldownFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'drilldown-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Layer Filters --> | ||||
|     <div class="filter-group" *ngIf="hasActiveLayerFilters()"> | ||||
|       <h4>Layer Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of getActiveLayerFilters()" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'layer-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Clear Filters Button --> | ||||
|     <div class="filter-actions"> | ||||
|       <button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Header row with chart title and drilldown navigation --> | ||||
|   <div class="clr-row header-row"> | ||||
|     <div class="clr-col-6"> | ||||
|       <h3 class="chart-title">{{charttitle || 'Bubble Chart'}}</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> | ||||
|    | ||||
|   <!-- Show current drilldown level --> | ||||
|   <div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0"> | ||||
|     <div class="clr-col-12"> | ||||
|       <div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;"> | ||||
|         <div class="alert-items"> | ||||
|           <div class="alert-item static"> | ||||
|             <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> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <!-- No data message --> | ||||
|   <div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;"> | ||||
| @ -16,7 +285,7 @@ | ||||
|   </div> | ||||
|    | ||||
|   <!-- Chart display --> | ||||
|   <div *ngIf="!noDataAvailable"> | ||||
|   <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);"> | ||||
|     <canvas baseChart | ||||
|     [datasets]="bubbleChartData" | ||||
|     [type]="bubbleChartType" | ||||
|  | ||||
| @ -0,0 +1,192 @@ | ||||
| .filter-section { | ||||
|   margin-bottom: 20px; | ||||
|   padding: 15px; | ||||
|   border: 1px solid #ddd; | ||||
|   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 { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       gap: 8px; | ||||
|       padding: 4px 0; | ||||
|        | ||||
|       .checkbox-label { | ||||
|         margin: 0; | ||||
|         font-size: 14px; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .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; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Responsive design | ||||
| @media (max-width: 768px) { | ||||
|   .filter-controls { | ||||
|     flex-direction: column; | ||||
|   } | ||||
|    | ||||
|   .filter-item { | ||||
|     min-width: 100%; | ||||
|   } | ||||
|    | ||||
|   .header-row { | ||||
|     .chart-title { | ||||
|       font-size: 16px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,6 +1,8 @@ | ||||
| import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||
| import { ChartConfiguration, ChartDataset, ChartOptions } from 'chart.js'; | ||||
| import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | ||||
| import { FilterService } from '../../common-filter/filter.service'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-bubble-chart', | ||||
| @ -96,15 +98,40 @@ export class BubbleChartComponent implements OnInit, OnChanges { | ||||
|   // Flag to prevent infinite loops
 | ||||
|   private isFetchingData: boolean = false; | ||||
|    | ||||
|   constructor(private dashboardService: Dashboard3Service) { } | ||||
|   // Subscriptions to unsubscribe on destroy
 | ||||
|   private subscriptions: Subscription[] = []; | ||||
| 
 | ||||
|   // Add properties for filter functionality
 | ||||
|   private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
 | ||||
|   private documentClickHandler: ((event: MouseEvent) => void) | null = null; | ||||
|   private filtersInitialized: boolean = false; | ||||
| 
 | ||||
|   constructor( | ||||
|     private dashboardService: Dashboard3Service, | ||||
|     private filterService: FilterService | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     // Subscribe to filter changes
 | ||||
|     this.subscriptions.push( | ||||
|       this.filterService.filterState$.subscribe(filters => { | ||||
|         // When filters change, refresh the chart data
 | ||||
|         this.fetchChartData(); | ||||
|       }) | ||||
|     ); | ||||
|      | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     console.log('BubbleChartComponent input changes:', changes); | ||||
|      | ||||
|     // Initialize filter values if they haven't been initialized yet
 | ||||
|     if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { | ||||
|       this.initializeFilterValues(); | ||||
|       this.filtersInitialized = true; | ||||
|     } | ||||
|      | ||||
|     // Check if any of the key properties have changed
 | ||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||
| @ -128,6 +155,349 @@ export class BubbleChartComponent implements OnInit, OnChanges { | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Initialize filter values with proper default values based on type
 | ||||
|   private initializeFilterValues(): void { | ||||
|     console.log('Initializing filter values'); | ||||
|      | ||||
|     // Initialize base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.value === undefined || filter.value === null) { | ||||
|               switch (filter.type) { | ||||
|                 case 'multiselect': | ||||
|                   filter.value = []; | ||||
|                   break; | ||||
|                 case 'date-range': | ||||
|                   filter.value = { start: null, end: null }; | ||||
|                   break; | ||||
|                 case 'toggle': | ||||
|                   filter.value = false; | ||||
|                   break; | ||||
|                 default: | ||||
|                   filter.value = ''; | ||||
|               } | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     console.log('Filter values initialized:', { | ||||
|       baseFilters: this.baseFilters, | ||||
|       drilldownFilters: this.drilldownFilters, | ||||
|       drilldownLayers: this.drilldownLayers | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active filters
 | ||||
|   hasActiveFilters(): boolean { | ||||
|     return (this.baseFilters && this.baseFilters.length > 0) ||  | ||||
|            (this.drilldownFilters && this.drilldownFilters.length > 0) ||  | ||||
|            this.hasActiveLayerFilters(); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active layer filters for current drilldown level
 | ||||
|   hasActiveLayerFilters(): boolean { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       return layerIndex < this.drilldownLayers.length &&  | ||||
|              this.drilldownLayers[layerIndex].filters &&  | ||||
|              this.drilldownLayers[layerIndex].filters.length > 0; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   // Get active layer filters for current drilldown level
 | ||||
|   getActiveLayerFilters(): any[] { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       if (layerIndex < this.drilldownLayers.length &&  | ||||
|           this.drilldownLayers[layerIndex].filters) { | ||||
|         return this.drilldownLayers[layerIndex].filters; | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Get filter options for dropdown/multiselect filters
 | ||||
|   getFilterOptions(filter: any): string[] { | ||||
|     if (filter.options) { | ||||
|       if (Array.isArray(filter.options)) { | ||||
|         return filter.options; | ||||
|       } else if (typeof filter.options === 'string') { | ||||
|         return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt); | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Check if an option is selected for multiselect filters
 | ||||
|   isOptionSelected(filter: any, option: string): boolean { | ||||
|     if (!filter.value) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.includes(option); | ||||
|     } | ||||
|      | ||||
|     return filter.value === option; | ||||
|   } | ||||
| 
 | ||||
|   // Handle base filter changes
 | ||||
|   onBaseFilterChange(filter: any): void { | ||||
|     console.log('Base filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle drilldown filter changes
 | ||||
|   onDrilldownFilterChange(filter: any): void { | ||||
|     console.log('Drilldown filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle layer filter changes
 | ||||
|   onLayerFilterChange(filter: any): void { | ||||
|     console.log('Layer filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle multiselect changes
 | ||||
|   onMultiSelectChange(filter: any, option: string, event: any): void { | ||||
|     const checked = event.target.checked; | ||||
|      | ||||
|     // Initialize filter.value as array if it's not already
 | ||||
|     if (!Array.isArray(filter.value)) { | ||||
|       filter.value = []; | ||||
|     } | ||||
|      | ||||
|     if (checked) { | ||||
|       // Add option to array if not already present
 | ||||
|       if (!filter.value.includes(option)) { | ||||
|         filter.value.push(option); | ||||
|       } | ||||
|     } else { | ||||
|       // Remove option from array
 | ||||
|       filter.value = filter.value.filter((item: string) => item !== option); | ||||
|     } | ||||
|      | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle date range changes
 | ||||
|   onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void { | ||||
|     filter.value = dateRange; | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle toggle changes
 | ||||
|   onToggleChange(filter: any, checked: boolean): void { | ||||
|     filter.value = checked; | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Toggle multiselect dropdown visibility
 | ||||
|   toggleMultiselect(filter: any, context: string): void { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     if (this.isMultiselectOpen(filter, context)) { | ||||
|       this.openMultiselects.delete(filterId); | ||||
|     } else { | ||||
|       // Close all other multiselects first
 | ||||
|       this.openMultiselects.clear(); | ||||
|       this.openMultiselects.set(filterId, context); | ||||
|        | ||||
|       // Add document click handler to close dropdown when clicking outside
 | ||||
|       this.addDocumentClickHandler(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Add document click handler to close dropdowns when clicking outside
 | ||||
|   private addDocumentClickHandler(): void { | ||||
|     if (!this.documentClickHandler) { | ||||
|       this.documentClickHandler = (event: MouseEvent) => { | ||||
|         const target = event.target as HTMLElement; | ||||
|         // Check if click is outside any multiselect dropdown
 | ||||
|         if (!target.closest('.multiselect-container')) { | ||||
|           this.openMultiselects.clear(); | ||||
|           this.removeDocumentClickHandler(); | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
 | ||||
|       setTimeout(() => { | ||||
|         document.addEventListener('click', this.documentClickHandler!); | ||||
|       }, 0); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Remove document click handler
 | ||||
|   private removeDocumentClickHandler(): void { | ||||
|     if (this.documentClickHandler) { | ||||
|       document.removeEventListener('click', this.documentClickHandler); | ||||
|       this.documentClickHandler = null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Check if multiselect dropdown is open
 | ||||
|   isMultiselectOpen(filter: any, context: string): boolean { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     return this.openMultiselects.has(filterId); | ||||
|   } | ||||
| 
 | ||||
|   // Get count of selected options for a multiselect filter
 | ||||
|   getSelectedOptionsCount(filter: any): number { | ||||
|     if (!filter.value) { | ||||
|       return 0; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.length; | ||||
|     } | ||||
|      | ||||
|     return 0; | ||||
|   } | ||||
| 
 | ||||
|   // Clear all filters
 | ||||
|   clearAllFilters(): void { | ||||
|     // Clear base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.type === 'multiselect') { | ||||
|               filter.value = []; | ||||
|             } else if (filter.type === 'date-range') { | ||||
|               filter.value = { start: null, end: null }; | ||||
|             } else if (filter.type === 'toggle') { | ||||
|               filter.value = false; | ||||
|             } else { | ||||
|               filter.value = ''; | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Close all multiselect dropdowns
 | ||||
|     this.openMultiselects.clear(); | ||||
|      | ||||
|     // Refresh data
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
|    | ||||
|   // Transform data to bubble chart format
 | ||||
|   private transformToBubbleData(labels: any[], data: any[]): ChartDataset[] { | ||||
|     // For bubble charts, we need to transform the data into bubble format
 | ||||
|     // Bubble charts expect data in the format: {x: number, y: number, r: number}
 | ||||
|     console.log('Transforming data to bubble format:', { labels, data }); | ||||
|      | ||||
|     // If we have the expected bubble data format, return it as is
 | ||||
|     if (data && data.length > 0 && data[0].data && data[0].data.length > 0 &&  | ||||
|         typeof data[0].data[0] === 'object' && data[0].data[0].hasOwnProperty('x') &&  | ||||
|         data[0].data[0].hasOwnProperty('y') && data[0].data[0].hasOwnProperty('r')) { | ||||
|       return data; | ||||
|     } | ||||
|      | ||||
|     // Otherwise, create a default bubble dataset
 | ||||
|     const bubbleDatasets: ChartDataset[] = [ | ||||
|       { | ||||
|         data: [ | ||||
|           { x: 10, y: 10, r: 10 }, | ||||
|           { x: 15, y: 5, r: 15 }, | ||||
|           { x: 26, y: 12, r: 23 }, | ||||
|           { x: 7, y: 8, r: 8 }, | ||||
|         ], | ||||
|         label: 'Dataset 1', | ||||
|         backgroundColor: 'rgba(255, 0, 0, 0.6)', | ||||
|         borderColor: 'blue', | ||||
|         hoverBackgroundColor: 'purple', | ||||
|         hoverBorderColor: 'red', | ||||
|       } | ||||
|     ]; | ||||
|      | ||||
|     return bubbleDatasets; | ||||
|   } | ||||
| 
 | ||||
|   fetchChartData(): void { | ||||
|     // Set flag to prevent recursive calls
 | ||||
|     this.isFetchingData = true; | ||||
| @ -160,7 +530,49 @@ export class BubbleChartComponent implements OnInit, OnChanges { | ||||
|           filterParams = JSON.stringify(filterObj); | ||||
|         } | ||||
|       } | ||||
|       console.log('Base filter parameters:', filterParams); | ||||
|        | ||||
|       // Add common filters to filter parameters
 | ||||
|       const commonFilters = this.filterService.getFilterValues(); | ||||
|       console.log('Common filters from service:', commonFilters); | ||||
|        | ||||
|       if (Object.keys(commonFilters).length > 0) { | ||||
|         // Merge common filters with base filters
 | ||||
|         const mergedFilterObj = {}; | ||||
|          | ||||
|         // Add base filters first
 | ||||
|         if (filterParams) { | ||||
|           try { | ||||
|             const baseFilterObj = JSON.parse(filterParams); | ||||
|             Object.assign(mergedFilterObj, baseFilterObj); | ||||
|           } catch (e) { | ||||
|             console.warn('Failed to parse base filter parameters:', e); | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         // Add common filters using the field name as the key, not the filter id
 | ||||
|         Object.keys(commonFilters).forEach(filterId => { | ||||
|           const filterValue = commonFilters[filterId]; | ||||
|           // Find the filter definition to get the field name
 | ||||
|           const filterDef = this.filterService.getFilters().find(f => f.id === filterId); | ||||
|           if (filterDef && filterDef.field) { | ||||
|             const fieldName = filterDef.field; | ||||
|             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||
|               mergedFilterObj[fieldName] = filterValue; | ||||
|             } | ||||
|           } else { | ||||
|             // Fallback to using filterId as field name if no field is defined
 | ||||
|             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||
|               mergedFilterObj[filterId] = filterValue; | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
|          | ||||
|         if (Object.keys(mergedFilterObj).length > 0) { | ||||
|           filterParams = JSON.stringify(mergedFilterObj); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       console.log('Final filter parameters:', filterParams); | ||||
|        | ||||
|       // Log the URL that will be called
 | ||||
|       const url = `chart/getdashjson/bubble?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||
| @ -206,6 +618,7 @@ export class BubbleChartComponent implements OnInit, OnChanges { | ||||
|           this.bubbleChartData = []; | ||||
|           // Reset flag after fetching
 | ||||
|           this.isFetchingData = false; | ||||
|           // Keep default data in case of error
 | ||||
|         } | ||||
|       ); | ||||
|     } else { | ||||
| @ -307,6 +720,35 @@ export class BubbleChartComponent implements OnInit, OnChanges { | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Add common filters to drilldown filter parameters
 | ||||
|     const commonFilters = this.filterService.getFilterValues(); | ||||
|     if (Object.keys(commonFilters).length > 0) { | ||||
|       // Merge common filters with drilldown filters
 | ||||
|       const mergedFilterObj = {}; | ||||
|        | ||||
|       // Add drilldown filters first
 | ||||
|       if (filterParams) { | ||||
|         try { | ||||
|           const drilldownFilterObj = JSON.parse(filterParams); | ||||
|           Object.assign(mergedFilterObj, drilldownFilterObj); | ||||
|         } catch (e) { | ||||
|           console.warn('Failed to parse drilldown filter parameters:', e); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Add common filters
 | ||||
|       Object.keys(commonFilters).forEach(key => { | ||||
|         const value = commonFilters[key]; | ||||
|         if (value !== undefined && value !== null && value !== '') { | ||||
|           mergedFilterObj[key] = value; | ||||
|         } | ||||
|       }); | ||||
|        | ||||
|       if (Object.keys(mergedFilterObj).length > 0) { | ||||
|         filterParams = JSON.stringify(mergedFilterObj); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Log the URL that will be called
 | ||||
|     const url = `chart/getdashjson/bubble?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||
|     console.log('Drilldown data URL:', url); | ||||
| @ -326,7 +768,6 @@ export class BubbleChartComponent implements OnInit, OnChanges { | ||||
|         // Handle the actual data structure returned by the API
 | ||||
|         if (data && data.chartLabels && data.chartData) { | ||||
|           // For bubble charts, we need to transform the data into bubble format
 | ||||
|           // Bubble charts expect data in the format: {x: number, y: number, r: number}
 | ||||
|           this.noDataAvailable = data.chartLabels.length === 0; | ||||
|           this.bubbleChartData = this.transformToBubbleData(data.chartLabels, data.chartData); | ||||
|           console.log('Updated bubble chart with drilldown data:', this.bubbleChartData); | ||||
| @ -345,39 +786,11 @@ export class BubbleChartComponent implements OnInit, OnChanges { | ||||
|         console.error('Error fetching drilldown data:', error); | ||||
|         this.noDataAvailable = true; | ||||
|         this.bubbleChartData = []; | ||||
|         // Keep current data in case of error
 | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   // Transform chart data to bubble chart format
 | ||||
|   private transformToBubbleData(labels: string[], datasets: any[]): ChartDataset[] { | ||||
|     // For bubble charts, we need to transform the data into bubble format
 | ||||
|     // Bubble charts expect data in the format: {x: number, y: number, r: number}
 | ||||
|      | ||||
|     // This is a simple transformation - in a real implementation, you might want to
 | ||||
|     // create a more sophisticated mapping based on your data structure
 | ||||
|     return datasets.map((dataset, index) => { | ||||
|       // Create bubble data points
 | ||||
|       const bubbleData = labels.map((label, i) => { | ||||
|         // Use x-axis data as x coordinate, y-axis data as y coordinate, and a fixed radius
 | ||||
|         const xValue = dataset.data[i] || 0; | ||||
|         const yValue = i < datasets.length ? (datasets[i].data[index] || 0) : 0; | ||||
|         const radius = 10; // Fixed radius for now
 | ||||
|          | ||||
|         return { x: xValue, y: yValue, r: radius }; | ||||
|       }); | ||||
|        | ||||
|       return { | ||||
|         data: bubbleData, | ||||
|         label: dataset.label || `Dataset ${index + 1}`, | ||||
|         backgroundColor: dataset.backgroundColor || `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.6)`, | ||||
|         borderColor: dataset.borderColor || 'rgba(0, 0, 0, 1)', | ||||
|         hoverBackgroundColor: dataset.hoverBackgroundColor || 'rgba(255, 255, 255, 0.8)', | ||||
|         hoverBorderColor: dataset.hoverBorderColor || 'rgba(0, 0, 0, 1)' | ||||
|       }; | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   // Reset to original data (go back to base level)
 | ||||
|   resetToOriginalData(): void { | ||||
|     console.log('Resetting to original data'); | ||||
| @ -438,16 +851,18 @@ export class BubbleChartComponent implements OnInit, OnChanges { | ||||
|       // Get the index of the clicked element
 | ||||
|       const clickedIndex = e.active[0].index; | ||||
|        | ||||
|       // Get the label of the clicked element
 | ||||
|       // For bubble charts, we might not have labels in the same way as other charts
 | ||||
|       const clickedLabel = `Bubble ${clickedIndex}`; | ||||
|       // Get the dataset index
 | ||||
|       const datasetIndex = e.active[0].datasetIndex; | ||||
|        | ||||
|       console.log('Clicked on bubble:', { index: clickedIndex, label: clickedLabel }); | ||||
|       // Get the data point
 | ||||
|       const dataPoint = this.bubbleChartData[datasetIndex].data[clickedIndex]; | ||||
|        | ||||
|       console.log('Clicked on bubble:', { datasetIndex, clickedIndex, dataPoint }); | ||||
|        | ||||
|       // If we're not at the base level, store original data
 | ||||
|       if (this.currentDrilldownLevel === 0) { | ||||
|         // Store original data before entering drilldown mode
 | ||||
|         this.originalBubbleChartData = [...this.bubbleChartData]; | ||||
|         this.originalBubbleChartData = JSON.parse(JSON.stringify(this.bubbleChartData)); | ||||
|         console.log('Stored original data for drilldown'); | ||||
|       } | ||||
|        | ||||
| @ -489,9 +904,10 @@ export class BubbleChartComponent implements OnInit, OnChanges { | ||||
|         // Add this click to the drilldown stack
 | ||||
|         const stackEntry = { | ||||
|           level: nextDrilldownLevel, | ||||
|           datasetIndex: datasetIndex, | ||||
|           clickedIndex: clickedIndex, | ||||
|           clickedLabel: clickedLabel, | ||||
|           clickedValue: clickedLabel // Using label as value for now
 | ||||
|           dataPoint: dataPoint, | ||||
|           clickedValue: dataPoint // Using data point as value for now
 | ||||
|         }; | ||||
|          | ||||
|         this.drilldownStack.push(stackEntry); | ||||
| @ -515,6 +931,6 @@ export class BubbleChartComponent implements OnInit, OnChanges { | ||||
|   } | ||||
| 
 | ||||
|   public chartHovered(e: any): void { | ||||
|     console.log(e); | ||||
|     console.log('Bubble chart hovered:', e); | ||||
|   } | ||||
| } | ||||
| @ -1,27 +1,283 @@ | ||||
| <div class="doughnut-chart-container"> | ||||
|   <!-- Compact Filters --> | ||||
|   <div class="compact-filters-container" *ngIf="baseFilters && baseFilters.length > 0"> | ||||
|     <app-compact-filter  | ||||
|       *ngFor="let filter of baseFilters"  | ||||
|       [filterKey]="filter.field" | ||||
|       (filterChange)="onFilterChange($event)"> | ||||
|     </app-compact-filter> | ||||
|   <!-- Filter Controls Section --> | ||||
|   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||
|     <!-- Base Filters --> | ||||
|     <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0"> | ||||
|       <h4>Base Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of baseFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|   <!-- Drilldown mode indicator --> | ||||
|   <div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator"> | ||||
|     <span class="drilldown-text">Drilldown Level: {{currentDrilldownLevel}}</span> | ||||
|     <button class="btn btn-secondary btn-sm" (click)="navigateBack()"> | ||||
|       Back to Level {{currentDrilldownLevel - 1}} | ||||
|     </button> | ||||
|     <button class="btn btn-danger btn-sm" (click)="resetToOriginalData()"> | ||||
|       Back to Main View | ||||
|     </button> | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|   <div class="chart-header"> | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'base-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Drilldown Filters --> | ||||
|     <div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0"> | ||||
|       <h4>Drilldown Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of drilldownFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'drilldown-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Layer Filters --> | ||||
|     <div class="filter-group" *ngIf="hasActiveLayerFilters()"> | ||||
|       <h4>Layer Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of getActiveLayerFilters()" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'layer-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Clear Filters Button --> | ||||
|     <div class="filter-actions"> | ||||
|       <button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Header row with chart title and drilldown navigation --> | ||||
|   <div class="clr-row header-row"> | ||||
|     <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> | ||||
|    | ||||
|   <!-- Show current drilldown level --> | ||||
|   <div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0"> | ||||
|     <div class="clr-col-12"> | ||||
|       <div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;"> | ||||
|         <div class="alert-items"> | ||||
|           <div class="alert-item static"> | ||||
|             <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> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <div class="chart-wrapper"> | ||||
|     <div class="chart-content" [class.loading]="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable"> | ||||
|  | ||||
| @ -245,6 +245,186 @@ | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Filter section styles | ||||
| .filter-section { | ||||
|   margin-bottom: 20px; | ||||
|   padding: 15px; | ||||
|   border: 1px solid #ddd; | ||||
|   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 { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       gap: 8px; | ||||
|       padding: 4px 0; | ||||
|        | ||||
|       .checkbox-label { | ||||
|         margin: 0; | ||||
|         font-size: 14px; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .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 { | ||||
| @ -287,4 +467,18 @@ | ||||
|   .compact-filters-container { | ||||
|     flex-wrap: wrap; | ||||
|   } | ||||
|    | ||||
|   .filter-controls { | ||||
|     flex-direction: column; | ||||
|   } | ||||
|    | ||||
|   .filter-item { | ||||
|     min-width: 100%; | ||||
|   } | ||||
|    | ||||
|   .header-row { | ||||
|     .chart-title { | ||||
|       font-size: 16px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -8,7 +8,7 @@ import { Subscription } from 'rxjs'; | ||||
|   templateUrl: './doughnut-chart.component.html', | ||||
|   styleUrls: ['./doughnut-chart.component.scss'] | ||||
| }) | ||||
| export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked { | ||||
| export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy { | ||||
|   @Input() xAxis: string; | ||||
|   @Input() yAxis: string | string[]; | ||||
|   @Input() table: string; | ||||
| @ -102,6 +102,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | ||||
|   // Subscriptions to unsubscribe on destroy
 | ||||
|   private subscriptions: Subscription[] = []; | ||||
| 
 | ||||
|   // Add properties for filter functionality
 | ||||
|   private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
 | ||||
|   private documentClickHandler: ((event: MouseEvent) => void) | null = null; | ||||
|   private filtersInitialized: boolean = false; | ||||
| 
 | ||||
|   constructor( | ||||
|     private dashboardService: Dashboard3Service, | ||||
|     private filterService: FilterService | ||||
| @ -164,6 +169,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     console.log('DoughnutChartComponent input changes:', changes); | ||||
|      | ||||
|     // Initialize filter values if they haven't been initialized yet
 | ||||
|     if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { | ||||
|       this.initializeFilterValues(); | ||||
|       this.filtersInitialized = true; | ||||
|     } | ||||
|      | ||||
|     // Check if any of the key properties have changed
 | ||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||
| @ -198,12 +209,318 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     this.subscriptions.forEach(sub => sub.unsubscribe()); | ||||
|     // Clean up document click handler
 | ||||
|     this.removeDocumentClickHandler(); | ||||
|   } | ||||
|    | ||||
|   // Handle filter changes from compact filters
 | ||||
|   onFilterChange(event: { filterId: string, value: any }): void { | ||||
|     console.log('Compact filter changed:', event); | ||||
|     // The filter service will automatically trigger chart updates through the subscription
 | ||||
|   // Initialize filter values with proper default values based on type
 | ||||
|   private initializeFilterValues(): void { | ||||
|     console.log('Initializing filter values'); | ||||
|      | ||||
|     // Initialize base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.value === undefined || filter.value === null) { | ||||
|               switch (filter.type) { | ||||
|                 case 'multiselect': | ||||
|                   filter.value = []; | ||||
|                   break; | ||||
|                 case 'date-range': | ||||
|                   filter.value = { start: null, end: null }; | ||||
|                   break; | ||||
|                 case 'toggle': | ||||
|                   filter.value = false; | ||||
|                   break; | ||||
|                 default: | ||||
|                   filter.value = ''; | ||||
|               } | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     console.log('Filter values initialized:', { | ||||
|       baseFilters: this.baseFilters, | ||||
|       drilldownFilters: this.drilldownFilters, | ||||
|       drilldownLayers: this.drilldownLayers | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active filters
 | ||||
|   hasActiveFilters(): boolean { | ||||
|     return (this.baseFilters && this.baseFilters.length > 0) ||  | ||||
|            (this.drilldownFilters && this.drilldownFilters.length > 0) ||  | ||||
|            this.hasActiveLayerFilters(); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active layer filters for current drilldown level
 | ||||
|   hasActiveLayerFilters(): boolean { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       return layerIndex < this.drilldownLayers.length &&  | ||||
|              this.drilldownLayers[layerIndex].filters &&  | ||||
|              this.drilldownLayers[layerIndex].filters.length > 0; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   // Get active layer filters for current drilldown level
 | ||||
|   getActiveLayerFilters(): any[] { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       if (layerIndex < this.drilldownLayers.length &&  | ||||
|           this.drilldownLayers[layerIndex].filters) { | ||||
|         return this.drilldownLayers[layerIndex].filters; | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Get filter options for dropdown/multiselect filters
 | ||||
|   getFilterOptions(filter: any): string[] { | ||||
|     if (filter.options) { | ||||
|       if (Array.isArray(filter.options)) { | ||||
|         return filter.options; | ||||
|       } else if (typeof filter.options === 'string') { | ||||
|         return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt); | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Check if an option is selected for multiselect filters
 | ||||
|   isOptionSelected(filter: any, option: string): boolean { | ||||
|     if (!filter.value) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.includes(option); | ||||
|     } | ||||
|      | ||||
|     return filter.value === option; | ||||
|   } | ||||
| 
 | ||||
|   // Handle base filter changes
 | ||||
|   onBaseFilterChange(filter: any): void { | ||||
|     console.log('Base filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle drilldown filter changes
 | ||||
|   onDrilldownFilterChange(filter: any): void { | ||||
|     console.log('Drilldown filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle layer filter changes
 | ||||
|   onLayerFilterChange(filter: any): void { | ||||
|     console.log('Layer filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle multiselect changes
 | ||||
|   onMultiSelectChange(filter: any, option: string, event: any): void { | ||||
|     const checked = event.target.checked; | ||||
|      | ||||
|     // Initialize filter.value as array if it's not already
 | ||||
|     if (!Array.isArray(filter.value)) { | ||||
|       filter.value = []; | ||||
|     } | ||||
|      | ||||
|     if (checked) { | ||||
|       // Add option to array if not already present
 | ||||
|       if (!filter.value.includes(option)) { | ||||
|         filter.value.push(option); | ||||
|       } | ||||
|     } else { | ||||
|       // Remove option from array
 | ||||
|       filter.value = filter.value.filter((item: string) => item !== option); | ||||
|     } | ||||
|      | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle date range changes
 | ||||
|   onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void { | ||||
|     filter.value = dateRange; | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle toggle changes
 | ||||
|   onToggleChange(filter: any, checked: boolean): void { | ||||
|     filter.value = checked; | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Toggle multiselect dropdown visibility
 | ||||
|   toggleMultiselect(filter: any, context: string): void { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     if (this.isMultiselectOpen(filter, context)) { | ||||
|       this.openMultiselects.delete(filterId); | ||||
|     } else { | ||||
|       // Close all other multiselects first
 | ||||
|       this.openMultiselects.clear(); | ||||
|       this.openMultiselects.set(filterId, context); | ||||
|        | ||||
|       // Add document click handler to close dropdown when clicking outside
 | ||||
|       this.addDocumentClickHandler(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Add document click handler to close dropdowns when clicking outside
 | ||||
|   private addDocumentClickHandler(): void { | ||||
|     if (!this.documentClickHandler) { | ||||
|       this.documentClickHandler = (event: MouseEvent) => { | ||||
|         const target = event.target as HTMLElement; | ||||
|         // Check if click is outside any multiselect dropdown
 | ||||
|         if (!target.closest('.multiselect-container')) { | ||||
|           this.openMultiselects.clear(); | ||||
|           this.removeDocumentClickHandler(); | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
 | ||||
|       setTimeout(() => { | ||||
|         document.addEventListener('click', this.documentClickHandler!); | ||||
|       }, 0); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Remove document click handler
 | ||||
|   private removeDocumentClickHandler(): void { | ||||
|     if (this.documentClickHandler) { | ||||
|       document.removeEventListener('click', this.documentClickHandler); | ||||
|       this.documentClickHandler = null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Check if multiselect dropdown is open
 | ||||
|   isMultiselectOpen(filter: any, context: string): boolean { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     return this.openMultiselects.has(filterId); | ||||
|   } | ||||
| 
 | ||||
|   // Get count of selected options for a multiselect filter
 | ||||
|   getSelectedOptionsCount(filter: any): number { | ||||
|     if (!filter.value) { | ||||
|       return 0; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.length; | ||||
|     } | ||||
|      | ||||
|     return 0; | ||||
|   } | ||||
| 
 | ||||
|   // Clear all filters
 | ||||
|   clearAllFilters(): void { | ||||
|     // Clear base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.type === 'multiselect') { | ||||
|               filter.value = []; | ||||
|             } else if (filter.type === 'date-range') { | ||||
|               filter.value = { start: null, end: null }; | ||||
|             } else if (filter.type === 'toggle') { | ||||
|               filter.value = false; | ||||
|             } else { | ||||
|               filter.value = ''; | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Close all multiselect dropdowns
 | ||||
|     this.openMultiselects.clear(); | ||||
|      | ||||
|     // Refresh data
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
|    | ||||
|   // Public method to refresh data when filters change
 | ||||
| @ -289,7 +606,7 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | ||||
|        | ||||
|       // Log the URL that will be called
 | ||||
|       const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||
|       console.log('Doughnut chart data URL:', url); | ||||
|       console.log('Chart data URL:', url); | ||||
|        | ||||
|       // Fetch data from the dashboard service with parameter field and value
 | ||||
|       // For base level, we pass empty parameter and value, but now also pass filters
 | ||||
| @ -297,12 +614,10 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | ||||
|         (data: any) => { | ||||
|           console.log('Received doughnut chart data:', data); | ||||
|           if (data === null) { | ||||
|             console.warn('Doughnut chart API returned null data. Check if the API endpoint is working correctly.'); | ||||
|             console.warn('API returned null data. Check if the API endpoint is working correctly.'); | ||||
|             this.noDataAvailable = true; | ||||
|             this.doughnutChartLabels = []; | ||||
|             this.doughnutChartData = []; | ||||
|             // Validate and sanitize data to show default data
 | ||||
|             this.validateChartData(); | ||||
|             // Reset flag after fetching
 | ||||
|             this.isFetchingData = false; | ||||
|             return; | ||||
| @ -310,50 +625,26 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | ||||
|            | ||||
|           // Handle the actual data structure returned by the API
 | ||||
|           if (data && data.chartLabels && data.chartData) { | ||||
|             // For doughnut charts, we need to extract the data differently
 | ||||
|             // The first dataset's data array contains the values for the doughnut chart
 | ||||
|             // Backend has already filtered the data, just display it
 | ||||
|             this.noDataAvailable = data.chartLabels.length === 0; | ||||
|             this.doughnutChartLabels = data.chartLabels || []; | ||||
|             if (data.chartData && data.chartData.length > 0) { | ||||
|               this.doughnutChartData = data.chartData[0].data.map(value => { | ||||
|                 // Convert to number if it's not already
 | ||||
|                 const numValue = Number(value); | ||||
|                 return isNaN(numValue) ? 0 : numValue; | ||||
|               }); | ||||
|             } else { | ||||
|               this.doughnutChartData = []; | ||||
|             } | ||||
|             // Ensure labels and data arrays have the same length
 | ||||
|             this.syncLabelAndDataArrays(); | ||||
|             // Validate and sanitize data
 | ||||
|             this.validateChartData(); | ||||
|             this.doughnutChartLabels = data.chartLabels; | ||||
|             this.doughnutChartData = data.chartData; | ||||
|             // Trigger change detection
 | ||||
|             this.doughnutChartData = [...this.doughnutChartData]; | ||||
|             console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); | ||||
|           } else if (data && data.labels && data.data) { | ||||
|             // Handle the original expected format as fallback
 | ||||
|           } else if (data && data.labels && data.datasets) { | ||||
|             // Backend has already filtered the data, just display it
 | ||||
|             this.noDataAvailable = data.labels.length === 0; | ||||
|             this.doughnutChartLabels = data.labels || []; | ||||
|             this.doughnutChartData = data.data.map(value => { | ||||
|               // Convert to number if it's not already
 | ||||
|               const numValue = Number(value); | ||||
|               return isNaN(numValue) ? 0 : numValue; | ||||
|             }); | ||||
|             // Ensure labels and data arrays have the same length
 | ||||
|             this.syncLabelAndDataArrays(); | ||||
|             // Validate and sanitize data
 | ||||
|             this.validateChartData(); | ||||
|             this.doughnutChartLabels = data.labels; | ||||
|             this.doughnutChartData = data.datasets[0]?.data || []; | ||||
|             // Trigger change detection
 | ||||
|             this.doughnutChartData = [...this.doughnutChartData]; | ||||
|             console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); | ||||
|           } else { | ||||
|             console.warn('Doughnut chart received data does not have expected structure', data); | ||||
|             // Reset to default data
 | ||||
|             console.warn('Received data does not have expected structure', data); | ||||
|             this.noDataAvailable = true; | ||||
|             this.doughnutChartLabels = []; | ||||
|             this.doughnutChartData = []; | ||||
|             // Validate and sanitize data to show default data
 | ||||
|             this.validateChartData(); | ||||
|           } | ||||
|           // Reset flag after fetching
 | ||||
|           this.isFetchingData = false; | ||||
| @ -363,21 +654,16 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | ||||
|           this.noDataAvailable = true; | ||||
|           this.doughnutChartLabels = []; | ||||
|           this.doughnutChartData = []; | ||||
|           // Validate and sanitize data to show default data
 | ||||
|           this.validateChartData(); | ||||
|           // Reset flag after fetching
 | ||||
|           this.isFetchingData = false; | ||||
|           // Keep default data in case of error
 | ||||
|         } | ||||
|       ); | ||||
|     } else { | ||||
|       console.log('Missing required data for doughnut chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); | ||||
|       // Don't set noDataAvailable to true when there's no required data
 | ||||
|       // This allows static data to be displayed
 | ||||
|       this.noDataAvailable = false; | ||||
|       // Validate the chart data to ensure we have some data to display
 | ||||
|       this.validateChartData(); | ||||
|       // Force a redraw to ensure the chart displays
 | ||||
|       this.doughnutChartData = [...this.doughnutChartData]; | ||||
|       console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); | ||||
|       this.noDataAvailable = true; | ||||
|       this.doughnutChartLabels = []; | ||||
|       this.doughnutChartData = []; | ||||
|       // Reset flag after fetching
 | ||||
|       this.isFetchingData = false; | ||||
|     } | ||||
| @ -475,6 +761,35 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Add common filters to drilldown filter parameters
 | ||||
|     const commonFilters = this.filterService.getFilterValues(); | ||||
|     if (Object.keys(commonFilters).length > 0) { | ||||
|       // Merge common filters with drilldown filters
 | ||||
|       const mergedFilterObj = {}; | ||||
|        | ||||
|       // Add drilldown filters first
 | ||||
|       if (filterParams) { | ||||
|         try { | ||||
|           const drilldownFilterObj = JSON.parse(filterParams); | ||||
|           Object.assign(mergedFilterObj, drilldownFilterObj); | ||||
|         } catch (e) { | ||||
|           console.warn('Failed to parse drilldown filter parameters:', e); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Add common filters
 | ||||
|       Object.keys(commonFilters).forEach(key => { | ||||
|         const value = commonFilters[key]; | ||||
|         if (value !== undefined && value !== null && value !== '') { | ||||
|           mergedFilterObj[key] = value; | ||||
|         } | ||||
|       }); | ||||
|        | ||||
|       if (Object.keys(mergedFilterObj).length > 0) { | ||||
|         filterParams = JSON.stringify(mergedFilterObj); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Log the URL that will be called
 | ||||
|     const url = `chart/getdashjson/doughnut?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||
|     console.log('Drilldown data URL:', url); | ||||
| @ -494,39 +809,18 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | ||||
|          | ||||
|         // Handle the actual data structure returned by the API
 | ||||
|         if (data && data.chartLabels && data.chartData) { | ||||
|           // For doughnut charts, we need to extract the data differently
 | ||||
|           // The first dataset's data array contains the values for the doughnut chart
 | ||||
|           // Backend has already filtered the data, just display it
 | ||||
|           this.noDataAvailable = data.chartLabels.length === 0; | ||||
|           this.doughnutChartLabels = data.chartLabels || []; | ||||
|           if (data.chartData && data.chartData.length > 0) { | ||||
|             this.doughnutChartData = data.chartData[0].data.map(value => { | ||||
|               // Convert to number if it's not already
 | ||||
|               const numValue = Number(value); | ||||
|               return isNaN(numValue) ? 0 : numValue; | ||||
|             }); | ||||
|           } else { | ||||
|             this.doughnutChartData = []; | ||||
|           } | ||||
|           // Ensure labels and data arrays have the same length
 | ||||
|           this.syncLabelAndDataArrays(); | ||||
|           // Validate and sanitize data
 | ||||
|           this.validateChartData(); | ||||
|           this.doughnutChartLabels = data.chartLabels; | ||||
|           this.doughnutChartData = data.chartData; | ||||
|           // Trigger change detection
 | ||||
|           this.doughnutChartData = [...this.doughnutChartData]; | ||||
|           console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); | ||||
|         } else if (data && data.labels && data.data) { | ||||
|           // Handle the original expected format as fallback
 | ||||
|         } else if (data && data.labels && data.datasets) { | ||||
|           // Backend has already filtered the data, just display it
 | ||||
|           this.noDataAvailable = data.labels.length === 0; | ||||
|           this.doughnutChartLabels = data.labels || []; | ||||
|           this.doughnutChartData = data.data.map(value => { | ||||
|             // Convert to number if it's not already
 | ||||
|             const numValue = Number(value); | ||||
|             return isNaN(numValue) ? 0 : numValue; | ||||
|           }); | ||||
|           // Ensure labels and data arrays have the same length
 | ||||
|           this.syncLabelAndDataArrays(); | ||||
|           // Validate and sanitize data
 | ||||
|           this.validateChartData(); | ||||
|           this.doughnutChartLabels = data.labels; | ||||
|           this.doughnutChartData = data.datasets[0]?.data || []; | ||||
|           // Trigger change detection
 | ||||
|           this.doughnutChartData = [...this.doughnutChartData]; | ||||
|           console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); | ||||
| @ -535,8 +829,6 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | ||||
|           this.noDataAvailable = true; | ||||
|           this.doughnutChartLabels = []; | ||||
|           this.doughnutChartData = []; | ||||
|           // Validate and sanitize data
 | ||||
|           this.validateChartData(); | ||||
|         } | ||||
|       }, | ||||
|       (error) => { | ||||
| @ -605,44 +897,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Get color for legend item | ||||
|    * @param index Index of the legend item | ||||
|    */ | ||||
|   public getLegendColor(index: number): string { | ||||
|   // Get legend color for a specific index
 | ||||
|   getLegendColor(index: number): string { | ||||
|     return this.chartColors[index % this.chartColors.length]; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Ensure labels and data arrays have the same length | ||||
|    */ | ||||
|   private syncLabelAndDataArrays(): void { | ||||
|     // Handle empty arrays
 | ||||
|     if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length); | ||||
|      | ||||
|     // Pad the shorter array with default values
 | ||||
|     while (this.doughnutChartLabels.length < maxLength) { | ||||
|       this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`); | ||||
|     } | ||||
|      | ||||
|     while (this.doughnutChartData.length < maxLength) { | ||||
|       this.doughnutChartData.push(0); | ||||
|     } | ||||
|      | ||||
|     // Truncate the longer array if needed
 | ||||
|     if (this.doughnutChartLabels.length > maxLength) { | ||||
|       this.doughnutChartLabels = this.doughnutChartLabels.slice(0, maxLength); | ||||
|     } | ||||
|      | ||||
|     if (this.doughnutChartData.length > maxLength) { | ||||
|       this.doughnutChartData = this.doughnutChartData.slice(0, maxLength); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // events
 | ||||
|   public chartClicked(e: any): void { | ||||
|     console.log('Doughnut chart clicked:', e); | ||||
| @ -729,6 +988,6 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | ||||
|   } | ||||
| 
 | ||||
|   public chartHovered(e: any): void { | ||||
|     console.log(e); | ||||
|     console.log('Doughnut chart hovered:', e); | ||||
|   } | ||||
| } | ||||
| @ -1,4 +1,285 @@ | ||||
| <div class="dynamic-chart-container"> | ||||
|   <!-- Filter Controls Section --> | ||||
|   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||
|     <!-- Base Filters --> | ||||
|     <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0"> | ||||
|       <h4>Base Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of baseFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'base-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Drilldown Filters --> | ||||
|     <div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0"> | ||||
|       <h4>Drilldown Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of drilldownFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'drilldown-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Layer Filters --> | ||||
|     <div class="filter-group" *ngIf="hasActiveLayerFilters()"> | ||||
|       <h4>Layer Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of getActiveLayerFilters()" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'layer-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Clear Filters Button --> | ||||
|     <div class="filter-actions"> | ||||
|       <button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Header row with chart title and drilldown navigation --> | ||||
|   <div class="clr-row header-row"> | ||||
|     <div class="clr-col-6"> | ||||
|       <h3 class="chart-title">{{charttitle || 'Dynamic Chart'}}</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> | ||||
|    | ||||
|   <!-- Show current drilldown level --> | ||||
|   <div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0"> | ||||
|     <div class="clr-col-12"> | ||||
|       <div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;"> | ||||
|         <div class="alert-items"> | ||||
|           <div class="alert-item static"> | ||||
|             <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> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Existing content --> | ||||
|   <!-- Drilldown mode indicator --> | ||||
|   <div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> | ||||
|     <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> | ||||
|  | ||||
| @ -0,0 +1,192 @@ | ||||
| .filter-section { | ||||
|   margin-bottom: 20px; | ||||
|   padding: 15px; | ||||
|   border: 1px solid #ddd; | ||||
|   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 { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       gap: 8px; | ||||
|       padding: 4px 0; | ||||
|        | ||||
|       .checkbox-label { | ||||
|         margin: 0; | ||||
|         font-size: 14px; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .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; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Responsive design | ||||
| @media (max-width: 768px) { | ||||
|   .filter-controls { | ||||
|     flex-direction: column; | ||||
|   } | ||||
|    | ||||
|   .filter-item { | ||||
|     min-width: 100%; | ||||
|   } | ||||
|    | ||||
|   .header-row { | ||||
|     .chart-title { | ||||
|       font-size: 16px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -2,6 +2,8 @@ import { Component, OnInit, ViewChild, Input, OnChanges, SimpleChanges } from '@ | ||||
| import { ChartConfiguration, ChartData, ChartDataset } from 'chart.js'; | ||||
| import { BaseChartDirective } from 'ng2-charts'; | ||||
| import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | ||||
| import { FilterService } from '../../common-filter/filter.service'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-dynamic-chart', | ||||
| @ -37,9 +39,20 @@ export class DynamicChartComponent implements OnInit, OnChanges { | ||||
| 
 | ||||
|   @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined; | ||||
| 
 | ||||
|   constructor(private dashboardService: Dashboard3Service) { } | ||||
|   constructor( | ||||
|     private dashboardService: Dashboard3Service, | ||||
|     private filterService: FilterService | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     // Subscribe to filter changes
 | ||||
|     this.subscriptions.push( | ||||
|       this.filterService.filterState$.subscribe(filters => { | ||||
|         // When filters change, refresh the chart data
 | ||||
|         this.fetchChartData(); | ||||
|       }) | ||||
|     ); | ||||
|      | ||||
|     // Initialize with default data
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| @ -47,6 +60,12 @@ export class DynamicChartComponent implements OnInit, OnChanges { | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     console.log('DynamicChartComponent input changes:', changes); | ||||
|      | ||||
|     // Initialize filter values if they haven't been initialized yet
 | ||||
|     if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { | ||||
|       this.initializeFilterValues(); | ||||
|       this.filtersInitialized = true; | ||||
|     } | ||||
|      | ||||
|     // Check if any of the key properties have changed
 | ||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||
| @ -107,6 +126,14 @@ export class DynamicChartComponent implements OnInit, OnChanges { | ||||
|   // Flag to prevent infinite loops
 | ||||
|   private isFetchingData: boolean = false; | ||||
|    | ||||
|   // Add properties for filter functionality
 | ||||
|   private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
 | ||||
|   private documentClickHandler: ((event: MouseEvent) => void) | null = null; | ||||
|   private filtersInitialized: boolean = false; | ||||
|    | ||||
|   // Subscriptions to unsubscribe on destroy
 | ||||
|   private subscriptions: Subscription[] = []; | ||||
| 	 | ||||
| 	fetchChartData(): void { | ||||
|     // Set flag to prevent recursive calls
 | ||||
|     this.isFetchingData = true; | ||||
| @ -139,7 +166,49 @@ export class DynamicChartComponent implements OnInit, OnChanges { | ||||
|           filterParams = JSON.stringify(filterObj); | ||||
|         } | ||||
|       } | ||||
|       console.log('Base filter parameters:', filterParams); | ||||
|        | ||||
|       // Add common filters to filter parameters
 | ||||
|       const commonFilters = this.filterService.getFilterValues(); | ||||
|       console.log('Common filters from service:', commonFilters); | ||||
|        | ||||
|       if (Object.keys(commonFilters).length > 0) { | ||||
|         // Merge common filters with base filters
 | ||||
|         const mergedFilterObj = {}; | ||||
|          | ||||
|         // Add base filters first
 | ||||
|         if (filterParams) { | ||||
|           try { | ||||
|             const baseFilterObj = JSON.parse(filterParams); | ||||
|             Object.assign(mergedFilterObj, baseFilterObj); | ||||
|           } catch (e) { | ||||
|             console.warn('Failed to parse base filter parameters:', e); | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         // Add common filters using the field name as the key, not the filter id
 | ||||
|         Object.keys(commonFilters).forEach(filterId => { | ||||
|           const filterValue = commonFilters[filterId]; | ||||
|           // Find the filter definition to get the field name
 | ||||
|           const filterDef = this.filterService.getFilters().find(f => f.id === filterId); | ||||
|           if (filterDef && filterDef.field) { | ||||
|             const fieldName = filterDef.field; | ||||
|             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||
|               mergedFilterObj[fieldName] = filterValue; | ||||
|             } | ||||
|           } else { | ||||
|             // Fallback to using filterId as field name if no field is defined
 | ||||
|             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||
|               mergedFilterObj[filterId] = filterValue; | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
|          | ||||
|         if (Object.keys(mergedFilterObj).length > 0) { | ||||
|           filterParams = JSON.stringify(mergedFilterObj); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       console.log('Final filter parameters:', filterParams); | ||||
|        | ||||
|       // Log the URL that will be called
 | ||||
|       const url = `chart/getdashjson/dynamic?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||
| @ -504,4 +573,322 @@ export class DynamicChartComponent implements OnInit, OnChanges { | ||||
|     } | ||||
|     this.dynamicChartData = _dynamicChartData; | ||||
|   } | ||||
|    | ||||
|   ngOnDestroy(): void { | ||||
|     // Unsubscribe from all subscriptions to prevent memory leaks
 | ||||
|     this.subscriptions.forEach(subscription => subscription.unsubscribe()); | ||||
|      | ||||
|     // Remove document click handler if it exists
 | ||||
|     this.removeDocumentClickHandler(); | ||||
|   } | ||||
|    | ||||
|   // Initialize filter values with proper default values based on type
 | ||||
|   private initializeFilterValues(): void { | ||||
|     console.log('Initializing filter values'); | ||||
|      | ||||
|     // Initialize base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.value === undefined || filter.value === null) { | ||||
|               switch (filter.type) { | ||||
|                 case 'multiselect': | ||||
|                   filter.value = []; | ||||
|                   break; | ||||
|                 case 'date-range': | ||||
|                   filter.value = { start: null, end: null }; | ||||
|                   break; | ||||
|                 case 'toggle': | ||||
|                   filter.value = false; | ||||
|                   break; | ||||
|                 default: | ||||
|                   filter.value = ''; | ||||
|               } | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     console.log('Filter values initialized:', { | ||||
|       baseFilters: this.baseFilters, | ||||
|       drilldownFilters: this.drilldownFilters, | ||||
|       drilldownLayers: this.drilldownLayers | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active filters
 | ||||
|   hasActiveFilters(): boolean { | ||||
|     return (this.baseFilters && this.baseFilters.length > 0) ||  | ||||
|            (this.drilldownFilters && this.drilldownFilters.length > 0) ||  | ||||
|            this.hasActiveLayerFilters(); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active layer filters for current drilldown level
 | ||||
|   hasActiveLayerFilters(): boolean { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       return layerIndex < this.drilldownLayers.length &&  | ||||
|              this.drilldownLayers[layerIndex].filters &&  | ||||
|              this.drilldownLayers[layerIndex].filters.length > 0; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   // Get active layer filters for current drilldown level
 | ||||
|   getActiveLayerFilters(): any[] { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       if (layerIndex < this.drilldownLayers.length &&  | ||||
|           this.drilldownLayers[layerIndex].filters) { | ||||
|         return this.drilldownLayers[layerIndex].filters; | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Get filter options for dropdown/multiselect filters
 | ||||
|   getFilterOptions(filter: any): string[] { | ||||
|     if (filter.options) { | ||||
|       if (Array.isArray(filter.options)) { | ||||
|         return filter.options; | ||||
|       } else if (typeof filter.options === 'string') { | ||||
|         return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt); | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Check if an option is selected for multiselect filters
 | ||||
|   isOptionSelected(filter: any, option: string): boolean { | ||||
|     if (!filter.value) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.includes(option); | ||||
|     } | ||||
|      | ||||
|     return filter.value === option; | ||||
|   } | ||||
| 
 | ||||
|   // Handle base filter changes
 | ||||
|   onBaseFilterChange(filter: any): void { | ||||
|     console.log('Base filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle drilldown filter changes
 | ||||
|   onDrilldownFilterChange(filter: any): void { | ||||
|     console.log('Drilldown filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle layer filter changes
 | ||||
|   onLayerFilterChange(filter: any): void { | ||||
|     console.log('Layer filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle multiselect changes
 | ||||
|   onMultiSelectChange(filter: any, option: string, event: any): void { | ||||
|     const checked = event.target.checked; | ||||
|      | ||||
|     // Initialize filter.value as array if it's not already
 | ||||
|     if (!Array.isArray(filter.value)) { | ||||
|       filter.value = []; | ||||
|     } | ||||
|      | ||||
|     if (checked) { | ||||
|       // Add option to array if not already present
 | ||||
|       if (!filter.value.includes(option)) { | ||||
|         filter.value.push(option); | ||||
|       } | ||||
|     } else { | ||||
|       // Remove option from array
 | ||||
|       filter.value = filter.value.filter((item: string) => item !== option); | ||||
|     } | ||||
|      | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle date range changes
 | ||||
|   onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void { | ||||
|     filter.value = dateRange; | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle toggle changes
 | ||||
|   onToggleChange(filter: any, checked: boolean): void { | ||||
|     filter.value = checked; | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Toggle multiselect dropdown visibility
 | ||||
|   toggleMultiselect(filter: any, context: string): void { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     if (this.isMultiselectOpen(filter, context)) { | ||||
|       this.openMultiselects.delete(filterId); | ||||
|     } else { | ||||
|       // Close all other multiselects first
 | ||||
|       this.openMultiselects.clear(); | ||||
|       this.openMultiselects.set(filterId, context); | ||||
|        | ||||
|       // Add document click handler to close dropdown when clicking outside
 | ||||
|       this.addDocumentClickHandler(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Add document click handler to close dropdowns when clicking outside
 | ||||
|   private addDocumentClickHandler(): void { | ||||
|     if (!this.documentClickHandler) { | ||||
|       this.documentClickHandler = (event: MouseEvent) => { | ||||
|         const target = event.target as HTMLElement; | ||||
|         // Check if click is outside any multiselect dropdown
 | ||||
|         if (!target.closest('.multiselect-container')) { | ||||
|           this.openMultiselects.clear(); | ||||
|           this.removeDocumentClickHandler(); | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
 | ||||
|       setTimeout(() => { | ||||
|         document.addEventListener('click', this.documentClickHandler!); | ||||
|       }, 0); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Remove document click handler
 | ||||
|   private removeDocumentClickHandler(): void { | ||||
|     if (this.documentClickHandler) { | ||||
|       document.removeEventListener('click', this.documentClickHandler); | ||||
|       this.documentClickHandler = null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Check if multiselect dropdown is open
 | ||||
|   isMultiselectOpen(filter: any, context: string): boolean { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     return this.openMultiselects.has(filterId); | ||||
|   } | ||||
| 
 | ||||
|   // Get count of selected options for a multiselect filter
 | ||||
|   getSelectedOptionsCount(filter: any): number { | ||||
|     if (!filter.value) { | ||||
|       return 0; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.length; | ||||
|     } | ||||
|      | ||||
|     return 0; | ||||
|   } | ||||
| 
 | ||||
|   // Clear all filters
 | ||||
|   clearAllFilters(): void { | ||||
|     // Clear base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.type === 'multiselect') { | ||||
|               filter.value = []; | ||||
|             } else if (filter.type === 'date-range') { | ||||
|               filter.value = { start: null, end: null }; | ||||
|             } else if (filter.type === 'toggle') { | ||||
|               filter.value = false; | ||||
|             } else { | ||||
|               filter.value = ''; | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Close all multiselect dropdowns
 | ||||
|     this.openMultiselects.clear(); | ||||
|      | ||||
|     // Refresh data
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| } | ||||
| @ -1,4 +1,285 @@ | ||||
| <div class="financial-chart-container"> | ||||
|   <!-- Filter Controls Section --> | ||||
|   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||
|     <!-- Base Filters --> | ||||
|     <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0"> | ||||
|       <h4>Base Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of baseFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'base-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Drilldown Filters --> | ||||
|     <div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0"> | ||||
|       <h4>Drilldown Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of drilldownFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'drilldown-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Layer Filters --> | ||||
|     <div class="filter-group" *ngIf="hasActiveLayerFilters()"> | ||||
|       <h4>Layer Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of getActiveLayerFilters()" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'layer-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Clear Filters Button --> | ||||
|     <div class="filter-actions"> | ||||
|       <button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Header row with chart title and drilldown navigation --> | ||||
|   <div class="clr-row header-row"> | ||||
|     <div class="clr-col-6"> | ||||
|       <h3 class="chart-title">{{charttitle || 'Financial Chart'}}</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> | ||||
|    | ||||
|   <!-- Show current drilldown level --> | ||||
|   <div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0"> | ||||
|     <div class="clr-col-12"> | ||||
|       <div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;"> | ||||
|         <div class="alert-items"> | ||||
|           <div class="alert-item static"> | ||||
|             <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> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Existing content --> | ||||
|   <!-- Drilldown mode indicator --> | ||||
|   <div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> | ||||
|     <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> | ||||
|  | ||||
| @ -1,108 +1,192 @@ | ||||
| .financial-chart-container { | ||||
|   display: flex; | ||||
|   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; | ||||
| } | ||||
| 
 | ||||
| .financial-chart-container:hover { | ||||
|   box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2); | ||||
|   transform: translateY(-2px); | ||||
| } | ||||
| 
 | ||||
| .chart-title { | ||||
|   font-size: 26px; | ||||
|   font-weight: 700; | ||||
|   color: #2c3e50; | ||||
| .filter-section { | ||||
|   margin-bottom: 20px; | ||||
|   text-align: center; | ||||
|   padding-bottom: 15px; | ||||
|   border-bottom: 2px solid #3498db; | ||||
|   text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); | ||||
| } | ||||
| 
 | ||||
| .chart-wrapper { | ||||
|   position: relative; | ||||
|   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; | ||||
| } | ||||
| 
 | ||||
| .loading-indicator, .no-data-message { | ||||
|   text-align: center; | ||||
|   padding: 30px; | ||||
|   color: #666; | ||||
|   font-size: 18px; | ||||
|   font-style: italic; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   height: 100%; | ||||
| } | ||||
| 
 | ||||
| .loading-indicator p, .no-data-message p { | ||||
|   margin: 10px 0 0 0; | ||||
| } | ||||
| 
 | ||||
| .spinner { | ||||
|   border: 4px solid #f3f3f3; | ||||
|   border-top: 4px solid #3498db; | ||||
|   border-radius: 50%; | ||||
|   width: 40px; | ||||
|   height: 40px; | ||||
|   animation: spin 1s linear infinite; | ||||
|   margin-bottom: 10px; | ||||
| } | ||||
| 
 | ||||
| @keyframes spin { | ||||
|   0% { transform: rotate(0deg); } | ||||
|   100% { transform: rotate(360deg); } | ||||
| } | ||||
| 
 | ||||
| /* Responsive design */ | ||||
| @media (max-width: 768px) { | ||||
|   .financial-chart-container { | ||||
|   padding: 15px; | ||||
|   border: 1px solid #ddd; | ||||
|   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 { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       gap: 8px; | ||||
|       padding: 4px 0; | ||||
|        | ||||
|       .checkbox-label { | ||||
|         margin: 0; | ||||
|         font-size: 14px; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .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 { | ||||
|     font-size: 20px; | ||||
|     margin-bottom: 15px; | ||||
|   } | ||||
|    | ||||
|   .chart-wrapper { | ||||
|     min-height: 200px; | ||||
|   } | ||||
|    | ||||
|   .no-data-message { | ||||
|     font-size: 16px; | ||||
|     padding: 20px; | ||||
|     margin: 0; | ||||
|     font-size: 18px; | ||||
|     font-weight: 600; | ||||
|     color: #333; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Responsive design | ||||
| @media (max-width: 768px) { | ||||
|   .filter-controls { | ||||
|     flex-direction: column; | ||||
|   } | ||||
|    | ||||
|   .filter-item { | ||||
|     min-width: 100%; | ||||
|   } | ||||
|    | ||||
|   .header-row { | ||||
|     .chart-title { | ||||
|       font-size: 16px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,5 +1,7 @@ | ||||
| import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||
| import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | ||||
| import { FilterService } from '../../common-filter/filter.service'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-financial-chart', | ||||
| @ -33,9 +35,20 @@ export class FinancialChartComponent implements OnInit, OnChanges { | ||||
|   // Multi-layer drilldown configuration inputs
 | ||||
|   @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
 | ||||
| 
 | ||||
|   constructor(private dashboardService: Dashboard3Service) { } | ||||
|   constructor( | ||||
|     private dashboardService: Dashboard3Service, | ||||
|     private filterService: FilterService | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     // Subscribe to filter changes
 | ||||
|     this.subscriptions.push( | ||||
|       this.filterService.filterState$.subscribe(filters => { | ||||
|         // When filters change, refresh the chart data
 | ||||
|         this.fetchChartData(); | ||||
|       }) | ||||
|     ); | ||||
|      | ||||
|     // Initialize with default data
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| @ -43,6 +56,12 @@ export class FinancialChartComponent implements OnInit, OnChanges { | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     console.log('FinancialChartComponent input changes:', changes); | ||||
|      | ||||
|     // Initialize filter values if they haven't been initialized yet
 | ||||
|     if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { | ||||
|       this.initializeFilterValues(); | ||||
|       this.filtersInitialized = true; | ||||
|     } | ||||
|      | ||||
|     // Check if any of the key properties have changed
 | ||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||
| @ -86,6 +105,14 @@ export class FinancialChartComponent implements OnInit, OnChanges { | ||||
|   // Flag to prevent infinite loops
 | ||||
|   private isFetchingData: boolean = false; | ||||
|    | ||||
|   // Add properties for filter functionality
 | ||||
|   private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
 | ||||
|   private documentClickHandler: ((event: MouseEvent) => void) | null = null; | ||||
|   private filtersInitialized: boolean = false; | ||||
|    | ||||
|   // Subscriptions to unsubscribe on destroy
 | ||||
|   private subscriptions: Subscription[] = []; | ||||
|    | ||||
|   fetchChartData(): void { | ||||
|     // Set flag to prevent recursive calls
 | ||||
|     this.isFetchingData = true; | ||||
| @ -118,7 +145,49 @@ export class FinancialChartComponent implements OnInit, OnChanges { | ||||
|           filterParams = JSON.stringify(filterObj); | ||||
|         } | ||||
|       } | ||||
|       console.log('Base filter parameters:', filterParams); | ||||
|        | ||||
|       // Add common filters to filter parameters
 | ||||
|       const commonFilters = this.filterService.getFilterValues(); | ||||
|       console.log('Common filters from service:', commonFilters); | ||||
|        | ||||
|       if (Object.keys(commonFilters).length > 0) { | ||||
|         // Merge common filters with base filters
 | ||||
|         const mergedFilterObj = {}; | ||||
|          | ||||
|         // Add base filters first
 | ||||
|         if (filterParams) { | ||||
|           try { | ||||
|             const baseFilterObj = JSON.parse(filterParams); | ||||
|             Object.assign(mergedFilterObj, baseFilterObj); | ||||
|           } catch (e) { | ||||
|             console.warn('Failed to parse base filter parameters:', e); | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         // Add common filters using the field name as the key, not the filter id
 | ||||
|         Object.keys(commonFilters).forEach(filterId => { | ||||
|           const filterValue = commonFilters[filterId]; | ||||
|           // Find the filter definition to get the field name
 | ||||
|           const filterDef = this.filterService.getFilters().find(f => f.id === filterId); | ||||
|           if (filterDef && filterDef.field) { | ||||
|             const fieldName = filterDef.field; | ||||
|             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||
|               mergedFilterObj[fieldName] = filterValue; | ||||
|             } | ||||
|           } else { | ||||
|             // Fallback to using filterId as field name if no field is defined
 | ||||
|             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||
|               mergedFilterObj[filterId] = filterValue; | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
|          | ||||
|         if (Object.keys(mergedFilterObj).length > 0) { | ||||
|           filterParams = JSON.stringify(mergedFilterObj); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       console.log('Final filter parameters:', filterParams); | ||||
|        | ||||
|       // Log the URL that will be called
 | ||||
|       const url = `chart/getdashjson/financial?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||
| @ -496,4 +565,322 @@ export class FinancialChartComponent implements OnInit, OnChanges { | ||||
|   public chartHovered(e: any): void { | ||||
|     console.log(e); | ||||
|   } | ||||
|    | ||||
|   // Initialize filter values with proper default values based on type
 | ||||
|   private initializeFilterValues(): void { | ||||
|     console.log('Initializing filter values'); | ||||
|      | ||||
|     // Initialize base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.value === undefined || filter.value === null) { | ||||
|               switch (filter.type) { | ||||
|                 case 'multiselect': | ||||
|                   filter.value = []; | ||||
|                   break; | ||||
|                 case 'date-range': | ||||
|                   filter.value = { start: null, end: null }; | ||||
|                   break; | ||||
|                 case 'toggle': | ||||
|                   filter.value = false; | ||||
|                   break; | ||||
|                 default: | ||||
|                   filter.value = ''; | ||||
|               } | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     console.log('Filter values initialized:', { | ||||
|       baseFilters: this.baseFilters, | ||||
|       drilldownFilters: this.drilldownFilters, | ||||
|       drilldownLayers: this.drilldownLayers | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active filters
 | ||||
|   hasActiveFilters(): boolean { | ||||
|     return (this.baseFilters && this.baseFilters.length > 0) ||  | ||||
|            (this.drilldownFilters && this.drilldownFilters.length > 0) ||  | ||||
|            this.hasActiveLayerFilters(); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active layer filters for current drilldown level
 | ||||
|   hasActiveLayerFilters(): boolean { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       return layerIndex < this.drilldownLayers.length &&  | ||||
|              this.drilldownLayers[layerIndex].filters &&  | ||||
|              this.drilldownLayers[layerIndex].filters.length > 0; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   // Get active layer filters for current drilldown level
 | ||||
|   getActiveLayerFilters(): any[] { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       if (layerIndex < this.drilldownLayers.length &&  | ||||
|           this.drilldownLayers[layerIndex].filters) { | ||||
|         return this.drilldownLayers[layerIndex].filters; | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Get filter options for dropdown/multiselect filters
 | ||||
|   getFilterOptions(filter: any): string[] { | ||||
|     if (filter.options) { | ||||
|       if (Array.isArray(filter.options)) { | ||||
|         return filter.options; | ||||
|       } else if (typeof filter.options === 'string') { | ||||
|         return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt); | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Check if an option is selected for multiselect filters
 | ||||
|   isOptionSelected(filter: any, option: string): boolean { | ||||
|     if (!filter.value) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.includes(option); | ||||
|     } | ||||
|      | ||||
|     return filter.value === option; | ||||
|   } | ||||
| 
 | ||||
|   // Handle base filter changes
 | ||||
|   onBaseFilterChange(filter: any): void { | ||||
|     console.log('Base filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle drilldown filter changes
 | ||||
|   onDrilldownFilterChange(filter: any): void { | ||||
|     console.log('Drilldown filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle layer filter changes
 | ||||
|   onLayerFilterChange(filter: any): void { | ||||
|     console.log('Layer filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle multiselect changes
 | ||||
|   onMultiSelectChange(filter: any, option: string, event: any): void { | ||||
|     const checked = event.target.checked; | ||||
|      | ||||
|     // Initialize filter.value as array if it's not already
 | ||||
|     if (!Array.isArray(filter.value)) { | ||||
|       filter.value = []; | ||||
|     } | ||||
|      | ||||
|     if (checked) { | ||||
|       // Add option to array if not already present
 | ||||
|       if (!filter.value.includes(option)) { | ||||
|         filter.value.push(option); | ||||
|       } | ||||
|     } else { | ||||
|       // Remove option from array
 | ||||
|       filter.value = filter.value.filter((item: string) => item !== option); | ||||
|     } | ||||
|      | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle date range changes
 | ||||
|   onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void { | ||||
|     filter.value = dateRange; | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle toggle changes
 | ||||
|   onToggleChange(filter: any, checked: boolean): void { | ||||
|     filter.value = checked; | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Toggle multiselect dropdown visibility
 | ||||
|   toggleMultiselect(filter: any, context: string): void { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     if (this.isMultiselectOpen(filter, context)) { | ||||
|       this.openMultiselects.delete(filterId); | ||||
|     } else { | ||||
|       // Close all other multiselects first
 | ||||
|       this.openMultiselects.clear(); | ||||
|       this.openMultiselects.set(filterId, context); | ||||
|        | ||||
|       // Add document click handler to close dropdown when clicking outside
 | ||||
|       this.addDocumentClickHandler(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Add document click handler to close dropdowns when clicking outside
 | ||||
|   private addDocumentClickHandler(): void { | ||||
|     if (!this.documentClickHandler) { | ||||
|       this.documentClickHandler = (event: MouseEvent) => { | ||||
|         const target = event.target as HTMLElement; | ||||
|         // Check if click is outside any multiselect dropdown
 | ||||
|         if (!target.closest('.multiselect-container')) { | ||||
|           this.openMultiselects.clear(); | ||||
|           this.removeDocumentClickHandler(); | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
 | ||||
|       setTimeout(() => { | ||||
|         document.addEventListener('click', this.documentClickHandler!); | ||||
|       }, 0); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Remove document click handler
 | ||||
|   private removeDocumentClickHandler(): void { | ||||
|     if (this.documentClickHandler) { | ||||
|       document.removeEventListener('click', this.documentClickHandler); | ||||
|       this.documentClickHandler = null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Check if multiselect dropdown is open
 | ||||
|   isMultiselectOpen(filter: any, context: string): boolean { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     return this.openMultiselects.has(filterId); | ||||
|   } | ||||
| 
 | ||||
|   // Get count of selected options for a multiselect filter
 | ||||
|   getSelectedOptionsCount(filter: any): number { | ||||
|     if (!filter.value) { | ||||
|       return 0; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.length; | ||||
|     } | ||||
|      | ||||
|     return 0; | ||||
|   } | ||||
| 
 | ||||
|   // Clear all filters
 | ||||
|   clearAllFilters(): void { | ||||
|     // Clear base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.type === 'multiselect') { | ||||
|               filter.value = []; | ||||
|             } else if (filter.type === 'date-range') { | ||||
|               filter.value = { start: null, end: null }; | ||||
|             } else if (filter.type === 'toggle') { | ||||
|               filter.value = false; | ||||
|             } else { | ||||
|               filter.value = ''; | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Close all multiselect dropdowns
 | ||||
|     this.openMultiselects.clear(); | ||||
|      | ||||
|     // Refresh data
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
|    | ||||
|   ngOnDestroy(): void { | ||||
|     // Unsubscribe from all subscriptions to prevent memory leaks
 | ||||
|     this.subscriptions.forEach(subscription => subscription.unsubscribe()); | ||||
|      | ||||
|     // Remove document click handler if it exists
 | ||||
|     this.removeDocumentClickHandler(); | ||||
|   } | ||||
| } | ||||
| @ -1,16 +1,284 @@ | ||||
| <div class="pie-chart-container"> | ||||
|   <!-- Drilldown mode indicator --> | ||||
|   <div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> | ||||
|     <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> | ||||
|     <button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> | ||||
|       Back to Level {{currentDrilldownLevel - 1}} | ||||
|     </button> | ||||
|     <button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> | ||||
|       Back to Main View | ||||
|     </button> | ||||
|   <!-- Filter Controls Section --> | ||||
|   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||
|     <!-- Base Filters --> | ||||
|     <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0"> | ||||
|       <h4>Base Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of baseFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'base-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Drilldown Filters --> | ||||
|     <div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0"> | ||||
|       <h4>Drilldown Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of drilldownFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'drilldown-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Layer Filters --> | ||||
|     <div class="filter-group" *ngIf="hasActiveLayerFilters()"> | ||||
|       <h4>Layer Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of getActiveLayerFilters()" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'layer-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Clear Filters Button --> | ||||
|     <div class="filter-actions"> | ||||
|       <button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Header row with chart title and drilldown navigation --> | ||||
|   <div class="clr-row header-row"> | ||||
|     <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> | ||||
|    | ||||
|   <!-- Show current drilldown level --> | ||||
|   <div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0"> | ||||
|     <div class="clr-col-12"> | ||||
|       <div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;"> | ||||
|         <div class="alert-items"> | ||||
|           <div class="alert-item static"> | ||||
|             <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> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <div class="chart-wrapper"> | ||||
|     <!-- Show loading indicator --> | ||||
|     <div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable"> | ||||
|  | ||||
| @ -149,10 +149,192 @@ | ||||
|   100% { transform: rotate(360deg); } | ||||
| } | ||||
| 
 | ||||
| /* Responsive design */ | ||||
| // Filter section styles | ||||
| .filter-section { | ||||
|   margin-bottom: 20px; | ||||
|   padding: 15px; | ||||
|   border: 1px solid #ddd; | ||||
|   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 { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       gap: 8px; | ||||
|       padding: 4px 0; | ||||
|        | ||||
|       .checkbox-label { | ||||
|         margin: 0; | ||||
|         font-size: 14px; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .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) { | ||||
|   .pie-chart-container { | ||||
|     padding: 15px; | ||||
|     height: auto; | ||||
|     min-height: 300px; | ||||
|   } | ||||
|    | ||||
|   .chart-title { | ||||
| @ -179,4 +361,18 @@ | ||||
|     font-size: 16px; | ||||
|     padding: 20px; | ||||
|   } | ||||
|    | ||||
|   .filter-controls { | ||||
|     flex-direction: column; | ||||
|   } | ||||
|    | ||||
|   .filter-item { | ||||
|     min-width: 100%; | ||||
|   } | ||||
|    | ||||
|   .header-row { | ||||
|     .chart-title { | ||||
|       font-size: 16px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -8,7 +8,7 @@ import { Subscription } from 'rxjs'; | ||||
|   templateUrl: './pie-chart.component.html', | ||||
|   styleUrls: ['./pie-chart.component.scss'] | ||||
| }) | ||||
| export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | ||||
| export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy { | ||||
|   @Input() xAxis: string; | ||||
|   @Input() yAxis: string | string[]; | ||||
|   @Input() table: string; | ||||
| @ -101,6 +101,11 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | ||||
|   // Subscriptions to unsubscribe on destroy
 | ||||
|   private subscriptions: Subscription[] = []; | ||||
| 
 | ||||
|   // Add properties for filter functionality
 | ||||
|   private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
 | ||||
|   private documentClickHandler: ((event: MouseEvent) => void) | null = null; | ||||
|   private filtersInitialized: boolean = false; | ||||
| 
 | ||||
|   constructor( | ||||
|     private dashboardService: Dashboard3Service, | ||||
|     private filterService: FilterService | ||||
| @ -133,6 +138,12 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     console.log('PieChartComponent input changes:', changes); | ||||
|      | ||||
|     // Initialize filter values if they haven't been initialized yet
 | ||||
|     if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { | ||||
|       this.initializeFilterValues(); | ||||
|       this.filtersInitialized = true; | ||||
|     } | ||||
|      | ||||
|     // Check if any of the key properties have changed
 | ||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||
| @ -158,6 +169,318 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     this.subscriptions.forEach(sub => sub.unsubscribe()); | ||||
|     // Clean up document click handler
 | ||||
|     this.removeDocumentClickHandler(); | ||||
|   } | ||||
|    | ||||
|   // Initialize filter values with proper default values based on type
 | ||||
|   private initializeFilterValues(): void { | ||||
|     console.log('Initializing filter values'); | ||||
|      | ||||
|     // Initialize base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.value === undefined || filter.value === null) { | ||||
|               switch (filter.type) { | ||||
|                 case 'multiselect': | ||||
|                   filter.value = []; | ||||
|                   break; | ||||
|                 case 'date-range': | ||||
|                   filter.value = { start: null, end: null }; | ||||
|                   break; | ||||
|                 case 'toggle': | ||||
|                   filter.value = false; | ||||
|                   break; | ||||
|                 default: | ||||
|                   filter.value = ''; | ||||
|               } | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     console.log('Filter values initialized:', { | ||||
|       baseFilters: this.baseFilters, | ||||
|       drilldownFilters: this.drilldownFilters, | ||||
|       drilldownLayers: this.drilldownLayers | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active filters
 | ||||
|   hasActiveFilters(): boolean { | ||||
|     return (this.baseFilters && this.baseFilters.length > 0) ||  | ||||
|            (this.drilldownFilters && this.drilldownFilters.length > 0) ||  | ||||
|            this.hasActiveLayerFilters(); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active layer filters for current drilldown level
 | ||||
|   hasActiveLayerFilters(): boolean { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       return layerIndex < this.drilldownLayers.length &&  | ||||
|              this.drilldownLayers[layerIndex].filters &&  | ||||
|              this.drilldownLayers[layerIndex].filters.length > 0; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   // Get active layer filters for current drilldown level
 | ||||
|   getActiveLayerFilters(): any[] { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       if (layerIndex < this.drilldownLayers.length &&  | ||||
|           this.drilldownLayers[layerIndex].filters) { | ||||
|         return this.drilldownLayers[layerIndex].filters; | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Get filter options for dropdown/multiselect filters
 | ||||
|   getFilterOptions(filter: any): string[] { | ||||
|     if (filter.options) { | ||||
|       if (Array.isArray(filter.options)) { | ||||
|         return filter.options; | ||||
|       } else if (typeof filter.options === 'string') { | ||||
|         return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt); | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Check if an option is selected for multiselect filters
 | ||||
|   isOptionSelected(filter: any, option: string): boolean { | ||||
|     if (!filter.value) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.includes(option); | ||||
|     } | ||||
|      | ||||
|     return filter.value === option; | ||||
|   } | ||||
| 
 | ||||
|   // Handle base filter changes
 | ||||
|   onBaseFilterChange(filter: any): void { | ||||
|     console.log('Base filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle drilldown filter changes
 | ||||
|   onDrilldownFilterChange(filter: any): void { | ||||
|     console.log('Drilldown filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle layer filter changes
 | ||||
|   onLayerFilterChange(filter: any): void { | ||||
|     console.log('Layer filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle multiselect changes
 | ||||
|   onMultiSelectChange(filter: any, option: string, event: any): void { | ||||
|     const checked = event.target.checked; | ||||
|      | ||||
|     // Initialize filter.value as array if it's not already
 | ||||
|     if (!Array.isArray(filter.value)) { | ||||
|       filter.value = []; | ||||
|     } | ||||
|      | ||||
|     if (checked) { | ||||
|       // Add option to array if not already present
 | ||||
|       if (!filter.value.includes(option)) { | ||||
|         filter.value.push(option); | ||||
|       } | ||||
|     } else { | ||||
|       // Remove option from array
 | ||||
|       filter.value = filter.value.filter((item: string) => item !== option); | ||||
|     } | ||||
|      | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle date range changes
 | ||||
|   onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void { | ||||
|     filter.value = dateRange; | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle toggle changes
 | ||||
|   onToggleChange(filter: any, checked: boolean): void { | ||||
|     filter.value = checked; | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Toggle multiselect dropdown visibility
 | ||||
|   toggleMultiselect(filter: any, context: string): void { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     if (this.isMultiselectOpen(filter, context)) { | ||||
|       this.openMultiselects.delete(filterId); | ||||
|     } else { | ||||
|       // Close all other multiselects first
 | ||||
|       this.openMultiselects.clear(); | ||||
|       this.openMultiselects.set(filterId, context); | ||||
|        | ||||
|       // Add document click handler to close dropdown when clicking outside
 | ||||
|       this.addDocumentClickHandler(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Add document click handler to close dropdowns when clicking outside
 | ||||
|   private addDocumentClickHandler(): void { | ||||
|     if (!this.documentClickHandler) { | ||||
|       this.documentClickHandler = (event: MouseEvent) => { | ||||
|         const target = event.target as HTMLElement; | ||||
|         // Check if click is outside any multiselect dropdown
 | ||||
|         if (!target.closest('.multiselect-container')) { | ||||
|           this.openMultiselects.clear(); | ||||
|           this.removeDocumentClickHandler(); | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
 | ||||
|       setTimeout(() => { | ||||
|         document.addEventListener('click', this.documentClickHandler!); | ||||
|       }, 0); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Remove document click handler
 | ||||
|   private removeDocumentClickHandler(): void { | ||||
|     if (this.documentClickHandler) { | ||||
|       document.removeEventListener('click', this.documentClickHandler); | ||||
|       this.documentClickHandler = null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Check if multiselect dropdown is open
 | ||||
|   isMultiselectOpen(filter: any, context: string): boolean { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     return this.openMultiselects.has(filterId); | ||||
|   } | ||||
| 
 | ||||
|   // Get count of selected options for a multiselect filter
 | ||||
|   getSelectedOptionsCount(filter: any): number { | ||||
|     if (!filter.value) { | ||||
|       return 0; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.length; | ||||
|     } | ||||
|      | ||||
|     return 0; | ||||
|   } | ||||
| 
 | ||||
|   // Clear all filters
 | ||||
|   clearAllFilters(): void { | ||||
|     // Clear base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.type === 'multiselect') { | ||||
|               filter.value = []; | ||||
|             } else if (filter.type === 'date-range') { | ||||
|               filter.value = { start: null, end: null }; | ||||
|             } else if (filter.type === 'toggle') { | ||||
|               filter.value = false; | ||||
|             } else { | ||||
|               filter.value = ''; | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Close all multiselect dropdowns
 | ||||
|     this.openMultiselects.clear(); | ||||
|      | ||||
|     // Refresh data
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
|    | ||||
|   // Public method to refresh data when filters change
 | ||||
| @ -243,7 +566,7 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | ||||
|        | ||||
|       // Log the URL that will be called
 | ||||
|       const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||
|       console.log('Pie chart data URL:', url); | ||||
|       console.log('Chart data URL:', url); | ||||
|        | ||||
|       // Fetch data from the dashboard service with parameter field and value
 | ||||
|       // For base level, we pass empty parameter and value, but now also pass filters
 | ||||
| @ -251,12 +574,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | ||||
|         (data: any) => { | ||||
|           console.log('Received pie chart data:', data); | ||||
|           if (data === null) { | ||||
|             console.warn('Pie chart API returned null data. Check if the API endpoint is working correctly.'); | ||||
|             console.warn('API returned null data. Check if the API endpoint is working correctly.'); | ||||
|             this.noDataAvailable = true; | ||||
|             this.pieChartLabels = []; | ||||
|             this.pieChartData = []; | ||||
|             // Validate and sanitize data to show default data
 | ||||
|             this.validateChartData(); | ||||
|             // Reset flag after fetching
 | ||||
|             this.isFetchingData = false; | ||||
|             return; | ||||
| @ -264,50 +585,26 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | ||||
|            | ||||
|           // Handle the actual data structure returned by the API
 | ||||
|           if (data && data.chartLabels && data.chartData) { | ||||
|             // For pie charts, we need to extract the data differently
 | ||||
|             // The first dataset's data array contains the values for the pie chart
 | ||||
|             // Backend has already filtered the data, just display it
 | ||||
|             this.noDataAvailable = data.chartLabels.length === 0; | ||||
|             this.pieChartLabels = data.chartLabels || []; | ||||
|             if (data.chartData && data.chartData.length > 0) { | ||||
|               this.pieChartData = data.chartData[0].data.map(value => { | ||||
|                 // Convert to number if it's not already
 | ||||
|                 const numValue = Number(value); | ||||
|                 return isNaN(numValue) ? 0 : numValue; | ||||
|               }); | ||||
|             } else { | ||||
|               this.pieChartData = []; | ||||
|             } | ||||
|             // Ensure labels and data arrays have the same length
 | ||||
|             this.syncLabelAndDataArrays(); | ||||
|             // Validate and sanitize data
 | ||||
|             this.validateChartData(); | ||||
|             this.pieChartLabels = data.chartLabels; | ||||
|             this.pieChartData = data.chartData; | ||||
|             // Trigger change detection
 | ||||
|             this.pieChartData = [...this.pieChartData]; | ||||
|             console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData }); | ||||
|           } else if (data && data.labels && data.data) { | ||||
|             // Handle the original expected format as fallback
 | ||||
|           } else if (data && data.labels && data.datasets) { | ||||
|             // Backend has already filtered the data, just display it
 | ||||
|             this.noDataAvailable = data.labels.length === 0; | ||||
|             this.pieChartLabels = data.labels || []; | ||||
|             this.pieChartData = data.data.map(value => { | ||||
|               // Convert to number if it's not already
 | ||||
|               const numValue = Number(value); | ||||
|               return isNaN(numValue) ? 0 : numValue; | ||||
|             }); | ||||
|             // Ensure labels and data arrays have the same length
 | ||||
|             this.syncLabelAndDataArrays(); | ||||
|             // Validate and sanitize data
 | ||||
|             this.validateChartData(); | ||||
|             this.pieChartLabels = data.labels; | ||||
|             this.pieChartData = data.datasets[0]?.data || []; | ||||
|             // Trigger change detection
 | ||||
|             this.pieChartData = [...this.pieChartData]; | ||||
|             console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData }); | ||||
|           } else { | ||||
|             console.warn('Pie chart received data does not have expected structure', data); | ||||
|             // Reset to default data
 | ||||
|             console.warn('Received data does not have expected structure', data); | ||||
|             this.noDataAvailable = true; | ||||
|             this.pieChartLabels = []; | ||||
|             this.pieChartData = []; | ||||
|             // Validate and sanitize data to show default data
 | ||||
|             this.validateChartData(); | ||||
|           } | ||||
|           // Reset flag after fetching
 | ||||
|           this.isFetchingData = false; | ||||
| @ -317,21 +614,16 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | ||||
|           this.noDataAvailable = true; | ||||
|           this.pieChartLabels = []; | ||||
|           this.pieChartData = []; | ||||
|           // Validate and sanitize data to show default data
 | ||||
|           this.validateChartData(); | ||||
|           // Reset flag after fetching
 | ||||
|           this.isFetchingData = false; | ||||
|           // Keep default data in case of error
 | ||||
|         } | ||||
|       ); | ||||
|     } else { | ||||
|       console.log('Missing required data for pie chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); | ||||
|       // Don't set noDataAvailable to true when there's no required data
 | ||||
|       // This allows static data to be displayed
 | ||||
|       this.noDataAvailable = false; | ||||
|       // Validate the chart data to ensure we have some data to display
 | ||||
|       this.validateChartData(); | ||||
|       // Force a redraw to ensure the chart displays
 | ||||
|       this.pieChartData = [...this.pieChartData]; | ||||
|       console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); | ||||
|       this.noDataAvailable = true; | ||||
|       this.pieChartLabels = []; | ||||
|       this.pieChartData = []; | ||||
|       // Reset flag after fetching
 | ||||
|       this.isFetchingData = false; | ||||
|     } | ||||
| @ -477,39 +769,18 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | ||||
|          | ||||
|         // Handle the actual data structure returned by the API
 | ||||
|         if (data && data.chartLabels && data.chartData) { | ||||
|           // For pie charts, we need to extract the data differently
 | ||||
|           // The first dataset's data array contains the values for the pie chart
 | ||||
|           // Backend has already filtered the data, just display it
 | ||||
|           this.noDataAvailable = data.chartLabels.length === 0; | ||||
|           this.pieChartLabels = data.chartLabels || []; | ||||
|           if (data.chartData && data.chartData.length > 0) { | ||||
|             this.pieChartData = data.chartData[0].data.map(value => { | ||||
|               // Convert to number if it's not already
 | ||||
|               const numValue = Number(value); | ||||
|               return isNaN(numValue) ? 0 : numValue; | ||||
|             }); | ||||
|           } else { | ||||
|             this.pieChartData = []; | ||||
|           } | ||||
|           // Ensure labels and data arrays have the same length
 | ||||
|           this.syncLabelAndDataArrays(); | ||||
|           // Validate and sanitize data
 | ||||
|           this.validateChartData(); | ||||
|           this.pieChartLabels = data.chartLabels; | ||||
|           this.pieChartData = data.chartData; | ||||
|           // Trigger change detection
 | ||||
|           this.pieChartData = [...this.pieChartData]; | ||||
|           console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData }); | ||||
|         } else if (data && data.labels && data.data) { | ||||
|           // Handle the original expected format as fallback
 | ||||
|         } else if (data && data.labels && data.datasets) { | ||||
|           // Backend has already filtered the data, just display it
 | ||||
|           this.noDataAvailable = data.labels.length === 0; | ||||
|           this.pieChartLabels = data.labels || []; | ||||
|           this.pieChartData = data.data.map(value => { | ||||
|             // Convert to number if it's not already
 | ||||
|             const numValue = Number(value); | ||||
|             return isNaN(numValue) ? 0 : numValue; | ||||
|           }); | ||||
|           // Ensure labels and data arrays have the same length
 | ||||
|           this.syncLabelAndDataArrays(); | ||||
|           // Validate and sanitize data
 | ||||
|           this.validateChartData(); | ||||
|           this.pieChartLabels = data.labels; | ||||
|           this.pieChartData = data.datasets[0]?.data || []; | ||||
|           // Trigger change detection
 | ||||
|           this.pieChartData = [...this.pieChartData]; | ||||
|           console.log('Updated pie chart with drilldown legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData }); | ||||
| @ -518,8 +789,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | ||||
|           this.noDataAvailable = true; | ||||
|           this.pieChartLabels = []; | ||||
|           this.pieChartData = []; | ||||
|           // Validate and sanitize data
 | ||||
|           this.validateChartData(); | ||||
|         } | ||||
|       }, | ||||
|       (error) => { | ||||
| @ -588,82 +857,32 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Get color for legend item | ||||
|    * @param index Index of the legend item | ||||
|    */ | ||||
|   public getLegendColor(index: number): string { | ||||
|     return this.chartColors[index % this.chartColors.length]; | ||||
|   } | ||||
|   // Validate chart data to ensure labels and data arrays have the same length
 | ||||
|   private validateChartData(): void { | ||||
|     if (this.pieChartLabels && this.pieChartData) { | ||||
|       // For pie charts, we need to ensure labels and data arrays have the same length
 | ||||
|       const labelCount = this.pieChartLabels.length; | ||||
|       const dataCount = this.pieChartData.length; | ||||
|        | ||||
|   /** | ||||
|    * Ensure labels and data arrays have the same length | ||||
|    */ | ||||
|   private syncLabelAndDataArrays(): void { | ||||
|     // Ensure we have matching arrays
 | ||||
|     if (this.pieChartLabels.length !== this.pieChartData.length) { | ||||
|       const maxLength = Math.max(this.pieChartLabels.length, this.pieChartData.length); | ||||
|       while (this.pieChartLabels.length < maxLength) { | ||||
|         this.pieChartLabels.push(`Label ${this.pieChartLabels.length + 1}`); | ||||
|       } | ||||
|       while (this.pieChartData.length < maxLength) { | ||||
|       if (labelCount !== dataCount) { | ||||
|         console.warn('Pie chart labels and data arrays have different lengths:', { labels: labelCount, data: dataCount }); | ||||
|         // Pad or truncate data array to match label count
 | ||||
|         if (dataCount < labelCount) { | ||||
|           // Pad with zeros
 | ||||
|           while (this.pieChartData.length < labelCount) { | ||||
|             this.pieChartData.push(0); | ||||
|           } | ||||
|         } else if (dataCount > labelCount) { | ||||
|           // Truncate data array
 | ||||
|           this.pieChartData = this.pieChartData.slice(0, labelCount); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Validate and sanitize chart data | ||||
|    */ | ||||
|   private validateChartData(): void { | ||||
|     console.log('Validating chart data:', { labels: this.pieChartLabels, data: this.pieChartData }); | ||||
|      | ||||
|     // Ensure we have valid arrays
 | ||||
|     if (!Array.isArray(this.pieChartLabels)) { | ||||
|       this.pieChartLabels = []; | ||||
|     } | ||||
|      | ||||
|     if (!Array.isArray(this.pieChartData)) { | ||||
|       this.pieChartData = []; | ||||
|     } | ||||
|      | ||||
|     // Ensure we have some data to display
 | ||||
|     if (this.pieChartLabels.length === 0 && this.pieChartData.length === 0) { | ||||
|       // Add default data to ensure chart visibility
 | ||||
|       this.pieChartLabels = ['Category A', 'Category B', 'Category C']; | ||||
|       this.pieChartData = [30, 50, 20]; | ||||
|       console.log('Added default data for chart display'); | ||||
|     } | ||||
|      | ||||
|     // Ensure labels and data arrays have the same length
 | ||||
|     this.syncLabelAndDataArrays(); | ||||
|      | ||||
|     // Ensure all data values are numbers
 | ||||
|     this.pieChartData = this.pieChartData.map(value => { | ||||
|       const numValue = Number(value); | ||||
|       return isNaN(numValue) ? 0 : numValue; | ||||
|     }); | ||||
|      | ||||
|     console.log('After validation:', { labels: this.pieChartLabels, data: this.pieChartData }); | ||||
|   } | ||||
|    | ||||
|   ngAfterViewChecked() { | ||||
|     // Debugging: Log component state after view checks
 | ||||
|     console.log('PieChartComponent state:', { | ||||
|       labels: this.pieChartLabels, | ||||
|       data: this.pieChartData, | ||||
|       hasData: this.pieChartLabels.length > 0 && this.pieChartData.length > 0 | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if chart data is valid and ready to display | ||||
|    */ | ||||
|   public isChartDataValid(): boolean { | ||||
|     return this.pieChartLabels && this.pieChartData &&  | ||||
|            Array.isArray(this.pieChartLabels) && Array.isArray(this.pieChartData) && | ||||
|            this.pieChartLabels.length > 0 && this.pieChartData.length > 0 && | ||||
|            this.pieChartLabels.length === this.pieChartData.length; | ||||
|   // Get legend color for a specific index
 | ||||
|   getLegendColor(index: number): string { | ||||
|     return this.chartColors[index % this.chartColors.length]; | ||||
|   } | ||||
| 
 | ||||
|   // events
 | ||||
| @ -752,6 +971,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | ||||
|   } | ||||
| 
 | ||||
|   public chartHovered(e: any): void { | ||||
|     console.log(e); | ||||
|     console.log('Pie chart hovered:', e); | ||||
|   } | ||||
| 
 | ||||
|   ngAfterViewChecked(): void { | ||||
|     // This lifecycle hook can be used if needed for post-render operations
 | ||||
|   } | ||||
| } | ||||
| @ -1,5 +1,285 @@ | ||||
| <div style="display: block; height: 100%; width: 100%;"> | ||||
|   <!-- Filter Controls Section --> | ||||
|   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||
|     <!-- Base Filters --> | ||||
|     <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0"> | ||||
|       <h4>Base Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of baseFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
| <div style="display: block"> | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'base-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Drilldown Filters --> | ||||
|     <div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0"> | ||||
|       <h4>Drilldown Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of drilldownFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'drilldown-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Layer Filters --> | ||||
|     <div class="filter-group" *ngIf="hasActiveLayerFilters()"> | ||||
|       <h4>Layer Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of getActiveLayerFilters()" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'layer-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Clear Filters Button --> | ||||
|     <div class="filter-actions"> | ||||
|       <button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Header row with chart title and drilldown navigation --> | ||||
|   <div class="clr-row header-row"> | ||||
|     <div class="clr-col-6"> | ||||
|       <h3 class="chart-title">{{charttitle || 'Polar Chart'}}</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> | ||||
|    | ||||
|   <!-- Show current drilldown level --> | ||||
|   <div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0"> | ||||
|     <div class="clr-col-12"> | ||||
|       <div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;"> | ||||
|         <div class="alert-items"> | ||||
|           <div class="alert-item static"> | ||||
|             <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> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <div style="position: relative; height: calc(100% - 50px);"> | ||||
|     <canvas baseChart | ||||
|       [datasets]="polarAreaChartData" | ||||
|       [labels]="polarAreaChartLabels" | ||||
| @ -7,4 +287,5 @@ | ||||
|       (chartHover)="chartHovered($event)" | ||||
|      (chartClick)="chartClicked($event)"> | ||||
|     </canvas> | ||||
|   </div> | ||||
| </div> | ||||
| @ -1,18 +1,192 @@ | ||||
| // Polar Chart Component Styles | ||||
| div[style*="display: block"] { | ||||
| .filter-section { | ||||
|   margin-bottom: 20px; | ||||
|   padding: 15px; | ||||
|   border: 1px solid #ddd; | ||||
|   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; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| } | ||||
| 
 | ||||
| canvas { | ||||
|   max-width: 100%; | ||||
|   max-height: 100%; | ||||
| .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; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Ensure the chart container has proper sizing | ||||
| :host { | ||||
|   display: block; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| .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 { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       gap: 8px; | ||||
|       padding: 4px 0; | ||||
|        | ||||
|       .checkbox-label { | ||||
|         margin: 0; | ||||
|         font-size: 14px; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .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; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Responsive design | ||||
| @media (max-width: 768px) { | ||||
|   .filter-controls { | ||||
|     flex-direction: column; | ||||
|   } | ||||
|    | ||||
|   .filter-item { | ||||
|     min-width: 100%; | ||||
|   } | ||||
|    | ||||
|   .header-row { | ||||
|     .chart-title { | ||||
|       font-size: 16px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,5 +1,7 @@ | ||||
| import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||
| import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | ||||
| import { FilterService } from '../../common-filter/filter.service'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-polar-chart', | ||||
| @ -33,9 +35,20 @@ export class PolarChartComponent implements OnInit, OnChanges { | ||||
|   // Multi-layer drilldown configuration inputs
 | ||||
|   @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
 | ||||
| 
 | ||||
|   constructor(private dashboardService: Dashboard3Service) { } | ||||
|   constructor( | ||||
|     private dashboardService: Dashboard3Service, | ||||
|     private filterService: FilterService | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     // Subscribe to filter changes
 | ||||
|     this.subscriptions.push( | ||||
|       this.filterService.filterState$.subscribe(filters => { | ||||
|         // When filters change, refresh the chart data
 | ||||
|         this.fetchChartData(); | ||||
|       }) | ||||
|     ); | ||||
|      | ||||
|     // Initialize with default data
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| @ -43,6 +56,12 @@ export class PolarChartComponent implements OnInit, OnChanges { | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     console.log('PolarChartComponent input changes:', changes); | ||||
|      | ||||
|     // Initialize filter values if they haven't been initialized yet
 | ||||
|     if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { | ||||
|       this.initializeFilterValues(); | ||||
|       this.filtersInitialized = true; | ||||
|     } | ||||
|      | ||||
|     // Check if any of the key properties have changed
 | ||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||
| @ -85,6 +104,324 @@ export class PolarChartComponent implements OnInit, OnChanges { | ||||
|   // Flag to prevent infinite loops
 | ||||
|   private isFetchingData: boolean = false; | ||||
|    | ||||
|   // Subscriptions to unsubscribe on destroy
 | ||||
|   private subscriptions: Subscription[] = []; | ||||
| 
 | ||||
|   // Add properties for filter functionality
 | ||||
|   private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
 | ||||
|   private documentClickHandler: ((event: MouseEvent) => void) | null = null; | ||||
|   private filtersInitialized: boolean = false; | ||||
|    | ||||
|   // Initialize filter values with proper default values based on type
 | ||||
|   private initializeFilterValues(): void { | ||||
|     console.log('Initializing filter values'); | ||||
|      | ||||
|     // Initialize base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.value === undefined || filter.value === null) { | ||||
|               switch (filter.type) { | ||||
|                 case 'multiselect': | ||||
|                   filter.value = []; | ||||
|                   break; | ||||
|                 case 'date-range': | ||||
|                   filter.value = { start: null, end: null }; | ||||
|                   break; | ||||
|                 case 'toggle': | ||||
|                   filter.value = false; | ||||
|                   break; | ||||
|                 default: | ||||
|                   filter.value = ''; | ||||
|               } | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     console.log('Filter values initialized:', { | ||||
|       baseFilters: this.baseFilters, | ||||
|       drilldownFilters: this.drilldownFilters, | ||||
|       drilldownLayers: this.drilldownLayers | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active filters
 | ||||
|   hasActiveFilters(): boolean { | ||||
|     return (this.baseFilters && this.baseFilters.length > 0) ||  | ||||
|            (this.drilldownFilters && this.drilldownFilters.length > 0) ||  | ||||
|            this.hasActiveLayerFilters(); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active layer filters for current drilldown level
 | ||||
|   hasActiveLayerFilters(): boolean { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       return layerIndex < this.drilldownLayers.length &&  | ||||
|              this.drilldownLayers[layerIndex].filters &&  | ||||
|              this.drilldownLayers[layerIndex].filters.length > 0; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   // Get active layer filters for current drilldown level
 | ||||
|   getActiveLayerFilters(): any[] { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       if (layerIndex < this.drilldownLayers.length &&  | ||||
|           this.drilldownLayers[layerIndex].filters) { | ||||
|         return this.drilldownLayers[layerIndex].filters; | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Get filter options for dropdown/multiselect filters
 | ||||
|   getFilterOptions(filter: any): string[] { | ||||
|     if (filter.options) { | ||||
|       if (Array.isArray(filter.options)) { | ||||
|         return filter.options; | ||||
|       } else if (typeof filter.options === 'string') { | ||||
|         return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt); | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Check if an option is selected for multiselect filters
 | ||||
|   isOptionSelected(filter: any, option: string): boolean { | ||||
|     if (!filter.value) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.includes(option); | ||||
|     } | ||||
|      | ||||
|     return filter.value === option; | ||||
|   } | ||||
| 
 | ||||
|   // Handle base filter changes
 | ||||
|   onBaseFilterChange(filter: any): void { | ||||
|     console.log('Base filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle drilldown filter changes
 | ||||
|   onDrilldownFilterChange(filter: any): void { | ||||
|     console.log('Drilldown filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle layer filter changes
 | ||||
|   onLayerFilterChange(filter: any): void { | ||||
|     console.log('Layer filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle multiselect changes
 | ||||
|   onMultiSelectChange(filter: any, option: string, event: any): void { | ||||
|     const checked = event.target.checked; | ||||
|      | ||||
|     // Initialize filter.value as array if it's not already
 | ||||
|     if (!Array.isArray(filter.value)) { | ||||
|       filter.value = []; | ||||
|     } | ||||
|      | ||||
|     if (checked) { | ||||
|       // Add option to array if not already present
 | ||||
|       if (!filter.value.includes(option)) { | ||||
|         filter.value.push(option); | ||||
|       } | ||||
|     } else { | ||||
|       // Remove option from array
 | ||||
|       filter.value = filter.value.filter((item: string) => item !== option); | ||||
|     } | ||||
|      | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle date range changes
 | ||||
|   onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void { | ||||
|     filter.value = dateRange; | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle toggle changes
 | ||||
|   onToggleChange(filter: any, checked: boolean): void { | ||||
|     filter.value = checked; | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Toggle multiselect dropdown visibility
 | ||||
|   toggleMultiselect(filter: any, context: string): void { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     if (this.isMultiselectOpen(filter, context)) { | ||||
|       this.openMultiselects.delete(filterId); | ||||
|     } else { | ||||
|       // Close all other multiselects first
 | ||||
|       this.openMultiselects.clear(); | ||||
|       this.openMultiselects.set(filterId, context); | ||||
|        | ||||
|       // Add document click handler to close dropdown when clicking outside
 | ||||
|       this.addDocumentClickHandler(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Add document click handler to close dropdowns when clicking outside
 | ||||
|   private addDocumentClickHandler(): void { | ||||
|     if (!this.documentClickHandler) { | ||||
|       this.documentClickHandler = (event: MouseEvent) => { | ||||
|         const target = event.target as HTMLElement; | ||||
|         // Check if click is outside any multiselect dropdown
 | ||||
|         if (!target.closest('.multiselect-container')) { | ||||
|           this.openMultiselects.clear(); | ||||
|           this.removeDocumentClickHandler(); | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
 | ||||
|       setTimeout(() => { | ||||
|         document.addEventListener('click', this.documentClickHandler!); | ||||
|       }, 0); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Remove document click handler
 | ||||
|   private removeDocumentClickHandler(): void { | ||||
|     if (this.documentClickHandler) { | ||||
|       document.removeEventListener('click', this.documentClickHandler); | ||||
|       this.documentClickHandler = null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Check if multiselect dropdown is open
 | ||||
|   isMultiselectOpen(filter: any, context: string): boolean { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     return this.openMultiselects.has(filterId); | ||||
|   } | ||||
| 
 | ||||
|   // Get count of selected options for a multiselect filter
 | ||||
|   getSelectedOptionsCount(filter: any): number { | ||||
|     if (!filter.value) { | ||||
|       return 0; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.length; | ||||
|     } | ||||
|      | ||||
|     return 0; | ||||
|   } | ||||
| 
 | ||||
|   // Clear all filters
 | ||||
|   clearAllFilters(): void { | ||||
|     // Clear base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.type === 'multiselect') { | ||||
|               filter.value = []; | ||||
|             } else if (filter.type === 'date-range') { | ||||
|               filter.value = { start: null, end: null }; | ||||
|             } else if (filter.type === 'toggle') { | ||||
|               filter.value = false; | ||||
|             } else { | ||||
|               filter.value = ''; | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Close all multiselect dropdowns
 | ||||
|     this.openMultiselects.clear(); | ||||
|      | ||||
|     // Refresh data
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
|    | ||||
|   fetchChartData(): void { | ||||
|     // Set flag to prevent recursive calls
 | ||||
|     this.isFetchingData = true; | ||||
| @ -117,7 +454,49 @@ export class PolarChartComponent implements OnInit, OnChanges { | ||||
|           filterParams = JSON.stringify(filterObj); | ||||
|         } | ||||
|       } | ||||
|       console.log('Base filter parameters:', filterParams); | ||||
|        | ||||
|       // Add common filters to filter parameters
 | ||||
|       const commonFilters = this.filterService.getFilterValues(); | ||||
|       console.log('Common filters from service:', commonFilters); | ||||
|        | ||||
|       if (Object.keys(commonFilters).length > 0) { | ||||
|         // Merge common filters with base filters
 | ||||
|         const mergedFilterObj = {}; | ||||
|          | ||||
|         // Add base filters first
 | ||||
|         if (filterParams) { | ||||
|           try { | ||||
|             const baseFilterObj = JSON.parse(filterParams); | ||||
|             Object.assign(mergedFilterObj, baseFilterObj); | ||||
|           } catch (e) { | ||||
|             console.warn('Failed to parse base filter parameters:', e); | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         // Add common filters using the field name as the key, not the filter id
 | ||||
|         Object.keys(commonFilters).forEach(filterId => { | ||||
|           const filterValue = commonFilters[filterId]; | ||||
|           // Find the filter definition to get the field name
 | ||||
|           const filterDef = this.filterService.getFilters().find(f => f.id === filterId); | ||||
|           if (filterDef && filterDef.field) { | ||||
|             const fieldName = filterDef.field; | ||||
|             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||
|               mergedFilterObj[fieldName] = filterValue; | ||||
|             } | ||||
|           } else { | ||||
|             // Fallback to using filterId as field name if no field is defined
 | ||||
|             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||
|               mergedFilterObj[filterId] = filterValue; | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
|          | ||||
|         if (Object.keys(mergedFilterObj).length > 0) { | ||||
|           filterParams = JSON.stringify(mergedFilterObj); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       console.log('Final filter parameters:', filterParams); | ||||
|        | ||||
|       // Log the URL that will be called
 | ||||
|       const url = `chart/getdashjson/polar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||
| @ -287,6 +666,35 @@ export class PolarChartComponent implements OnInit, OnChanges { | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Add common filters to drilldown filter parameters
 | ||||
|     const commonFilters = this.filterService.getFilterValues(); | ||||
|     if (Object.keys(commonFilters).length > 0) { | ||||
|       // Merge common filters with drilldown filters
 | ||||
|       const mergedFilterObj = {}; | ||||
|        | ||||
|       // Add drilldown filters first
 | ||||
|       if (filterParams) { | ||||
|         try { | ||||
|           const drilldownFilterObj = JSON.parse(filterParams); | ||||
|           Object.assign(mergedFilterObj, drilldownFilterObj); | ||||
|         } catch (e) { | ||||
|           console.warn('Failed to parse drilldown filter parameters:', e); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Add common filters
 | ||||
|       Object.keys(commonFilters).forEach(key => { | ||||
|         const value = commonFilters[key]; | ||||
|         if (value !== undefined && value !== null && value !== '') { | ||||
|           mergedFilterObj[key] = value; | ||||
|         } | ||||
|       }); | ||||
|        | ||||
|       if (Object.keys(mergedFilterObj).length > 0) { | ||||
|         filterParams = JSON.stringify(mergedFilterObj); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Log the URL that will be called
 | ||||
|     const url = `chart/getdashjson/polar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||
|     console.log('Drilldown data URL:', url); | ||||
| @ -307,7 +715,6 @@ export class PolarChartComponent implements OnInit, OnChanges { | ||||
|         // Handle the actual data structure returned by the API
 | ||||
|         if (data && data.chartLabels && data.chartData) { | ||||
|           // For polar charts, we need to extract the data differently
 | ||||
|           // The first dataset's data array contains the values for the polar chart
 | ||||
|           this.noDataAvailable = data.chartLabels.length === 0; | ||||
|           this.polarAreaChartLabels = data.chartLabels; | ||||
|           if (data.chartData && data.chartData.length > 0) { | ||||
| @ -417,13 +824,13 @@ export class PolarChartComponent implements OnInit, OnChanges { | ||||
|       // Get the label of the clicked element
 | ||||
|       const clickedLabel = this.polarAreaChartLabels[clickedIndex]; | ||||
|        | ||||
|       console.log('Clicked on polar area:', { index: clickedIndex, label: clickedLabel }); | ||||
|       console.log('Clicked on polar point:', { index: clickedIndex, label: clickedLabel }); | ||||
|        | ||||
|       // If we're not at the base level, store original data
 | ||||
|       if (this.currentDrilldownLevel === 0) { | ||||
|         // Store original data before entering drilldown mode
 | ||||
|         this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels]; | ||||
|         this.originalPolarAreaChartData = [...this.polarAreaChartData]; | ||||
|         this.originalPolarAreaChartData = JSON.parse(JSON.stringify(this.polarAreaChartData)); | ||||
|         console.log('Stored original data for drilldown'); | ||||
|       } | ||||
|        | ||||
| @ -491,6 +898,12 @@ export class PolarChartComponent implements OnInit, OnChanges { | ||||
|   } | ||||
| 
 | ||||
|   public chartHovered(e: any): void { | ||||
|     console.log(e); | ||||
|     console.log('Polar chart hovered:', e); | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     this.subscriptions.forEach(sub => sub.unsubscribe()); | ||||
|     // Clean up document click handler
 | ||||
|     this.removeDocumentClickHandler(); | ||||
|   } | ||||
| } | ||||
| @ -1,14 +1,283 @@ | ||||
| <div style="display: block"> | ||||
|   <!-- Drilldown mode indicator --> | ||||
|   <div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> | ||||
|     <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> | ||||
|     <button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> | ||||
|       Back to Level {{currentDrilldownLevel - 1}} | ||||
|     </button> | ||||
|     <button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> | ||||
|       Back to Main View | ||||
| <div style="display: block; height: 100%; width: 100%;"> | ||||
|   <!-- Filter Controls Section --> | ||||
|   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||
|     <!-- Base Filters --> | ||||
|     <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0"> | ||||
|       <h4>Base Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of baseFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'base-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Drilldown Filters --> | ||||
|     <div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0"> | ||||
|       <h4>Drilldown Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of drilldownFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'drilldown-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Layer Filters --> | ||||
|     <div class="filter-group" *ngIf="hasActiveLayerFilters()"> | ||||
|       <h4>Layer Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of getActiveLayerFilters()" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'layer-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Clear Filters Button --> | ||||
|     <div class="filter-actions"> | ||||
|       <button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Header row with chart title and drilldown navigation --> | ||||
|   <div class="clr-row header-row"> | ||||
|     <div class="clr-col-6"> | ||||
|       <h3 class="chart-title">{{charttitle || 'Radar Chart'}}</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> | ||||
|    | ||||
|   <!-- Show current drilldown level --> | ||||
|   <div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0"> | ||||
|     <div class="clr-col-12"> | ||||
|       <div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;"> | ||||
|         <div class="alert-items"> | ||||
|           <div class="alert-item static"> | ||||
|             <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> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <!-- No data message --> | ||||
|   <div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;"> | ||||
| @ -16,7 +285,7 @@ | ||||
|   </div> | ||||
|    | ||||
|   <!-- Chart display --> | ||||
|   <div *ngIf="!noDataAvailable"> | ||||
|   <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);"> | ||||
|     <canvas baseChart | ||||
|       [datasets]="radarChartData" | ||||
|       [labels]="radarChartLabels" | ||||
|  | ||||
| @ -0,0 +1,192 @@ | ||||
| .filter-section { | ||||
|   margin-bottom: 20px; | ||||
|   padding: 15px; | ||||
|   border: 1px solid #ddd; | ||||
|   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 { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       gap: 8px; | ||||
|       padding: 4px 0; | ||||
|        | ||||
|       .checkbox-label { | ||||
|         margin: 0; | ||||
|         font-size: 14px; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .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; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Responsive design | ||||
| @media (max-width: 768px) { | ||||
|   .filter-controls { | ||||
|     flex-direction: column; | ||||
|   } | ||||
|    | ||||
|   .filter-item { | ||||
|     min-width: 100%; | ||||
|   } | ||||
|    | ||||
|   .header-row { | ||||
|     .chart-title { | ||||
|       font-size: 16px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,5 +1,7 @@ | ||||
| import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||
| import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | ||||
| import { FilterService } from '../../common-filter/filter.service'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-radar-chart', | ||||
| @ -62,15 +64,40 @@ export class RadarChartComponent implements OnInit, OnChanges { | ||||
|   // Flag to prevent infinite loops
 | ||||
|   private isFetchingData: boolean = false; | ||||
|    | ||||
|   constructor(private dashboardService: Dashboard3Service) { } | ||||
|   // Subscriptions to unsubscribe on destroy
 | ||||
|   private subscriptions: Subscription[] = []; | ||||
| 
 | ||||
|   // Add properties for filter functionality
 | ||||
|   private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
 | ||||
|   private documentClickHandler: ((event: MouseEvent) => void) | null = null; | ||||
|   private filtersInitialized: boolean = false; | ||||
| 
 | ||||
|   constructor( | ||||
|     private dashboardService: Dashboard3Service, | ||||
|     private filterService: FilterService | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     // Subscribe to filter changes
 | ||||
|     this.subscriptions.push( | ||||
|       this.filterService.filterState$.subscribe(filters => { | ||||
|         // When filters change, refresh the chart data
 | ||||
|         this.fetchChartData(); | ||||
|       }) | ||||
|     ); | ||||
|      | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     console.log('RadarChartComponent input changes:', changes); | ||||
|      | ||||
|     // Initialize filter values if they haven't been initialized yet
 | ||||
|     if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { | ||||
|       this.initializeFilterValues(); | ||||
|       this.filtersInitialized = true; | ||||
|     } | ||||
|      | ||||
|     // Check if any of the key properties have changed
 | ||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||
| @ -94,6 +121,316 @@ export class RadarChartComponent implements OnInit, OnChanges { | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Initialize filter values with proper default values based on type
 | ||||
|   private initializeFilterValues(): void { | ||||
|     console.log('Initializing filter values'); | ||||
|      | ||||
|     // Initialize base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.value === undefined || filter.value === null) { | ||||
|               switch (filter.type) { | ||||
|                 case 'multiselect': | ||||
|                   filter.value = []; | ||||
|                   break; | ||||
|                 case 'date-range': | ||||
|                   filter.value = { start: null, end: null }; | ||||
|                   break; | ||||
|                 case 'toggle': | ||||
|                   filter.value = false; | ||||
|                   break; | ||||
|                 default: | ||||
|                   filter.value = ''; | ||||
|               } | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     console.log('Filter values initialized:', { | ||||
|       baseFilters: this.baseFilters, | ||||
|       drilldownFilters: this.drilldownFilters, | ||||
|       drilldownLayers: this.drilldownLayers | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active filters
 | ||||
|   hasActiveFilters(): boolean { | ||||
|     return (this.baseFilters && this.baseFilters.length > 0) ||  | ||||
|            (this.drilldownFilters && this.drilldownFilters.length > 0) ||  | ||||
|            this.hasActiveLayerFilters(); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active layer filters for current drilldown level
 | ||||
|   hasActiveLayerFilters(): boolean { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       return layerIndex < this.drilldownLayers.length &&  | ||||
|              this.drilldownLayers[layerIndex].filters &&  | ||||
|              this.drilldownLayers[layerIndex].filters.length > 0; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   // Get active layer filters for current drilldown level
 | ||||
|   getActiveLayerFilters(): any[] { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       if (layerIndex < this.drilldownLayers.length &&  | ||||
|           this.drilldownLayers[layerIndex].filters) { | ||||
|         return this.drilldownLayers[layerIndex].filters; | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Get filter options for dropdown/multiselect filters
 | ||||
|   getFilterOptions(filter: any): string[] { | ||||
|     if (filter.options) { | ||||
|       if (Array.isArray(filter.options)) { | ||||
|         return filter.options; | ||||
|       } else if (typeof filter.options === 'string') { | ||||
|         return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt); | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Check if an option is selected for multiselect filters
 | ||||
|   isOptionSelected(filter: any, option: string): boolean { | ||||
|     if (!filter.value) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.includes(option); | ||||
|     } | ||||
|      | ||||
|     return filter.value === option; | ||||
|   } | ||||
| 
 | ||||
|   // Handle base filter changes
 | ||||
|   onBaseFilterChange(filter: any): void { | ||||
|     console.log('Base filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle drilldown filter changes
 | ||||
|   onDrilldownFilterChange(filter: any): void { | ||||
|     console.log('Drilldown filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle layer filter changes
 | ||||
|   onLayerFilterChange(filter: any): void { | ||||
|     console.log('Layer filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle multiselect changes
 | ||||
|   onMultiSelectChange(filter: any, option: string, event: any): void { | ||||
|     const checked = event.target.checked; | ||||
|      | ||||
|     // Initialize filter.value as array if it's not already
 | ||||
|     if (!Array.isArray(filter.value)) { | ||||
|       filter.value = []; | ||||
|     } | ||||
|      | ||||
|     if (checked) { | ||||
|       // Add option to array if not already present
 | ||||
|       if (!filter.value.includes(option)) { | ||||
|         filter.value.push(option); | ||||
|       } | ||||
|     } else { | ||||
|       // Remove option from array
 | ||||
|       filter.value = filter.value.filter((item: string) => item !== option); | ||||
|     } | ||||
|      | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle date range changes
 | ||||
|   onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void { | ||||
|     filter.value = dateRange; | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle toggle changes
 | ||||
|   onToggleChange(filter: any, checked: boolean): void { | ||||
|     filter.value = checked; | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Toggle multiselect dropdown visibility
 | ||||
|   toggleMultiselect(filter: any, context: string): void { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     if (this.isMultiselectOpen(filter, context)) { | ||||
|       this.openMultiselects.delete(filterId); | ||||
|     } else { | ||||
|       // Close all other multiselects first
 | ||||
|       this.openMultiselects.clear(); | ||||
|       this.openMultiselects.set(filterId, context); | ||||
|        | ||||
|       // Add document click handler to close dropdown when clicking outside
 | ||||
|       this.addDocumentClickHandler(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Add document click handler to close dropdowns when clicking outside
 | ||||
|   private addDocumentClickHandler(): void { | ||||
|     if (!this.documentClickHandler) { | ||||
|       this.documentClickHandler = (event: MouseEvent) => { | ||||
|         const target = event.target as HTMLElement; | ||||
|         // Check if click is outside any multiselect dropdown
 | ||||
|         if (!target.closest('.multiselect-container')) { | ||||
|           this.openMultiselects.clear(); | ||||
|           this.removeDocumentClickHandler(); | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
 | ||||
|       setTimeout(() => { | ||||
|         document.addEventListener('click', this.documentClickHandler!); | ||||
|       }, 0); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Remove document click handler
 | ||||
|   private removeDocumentClickHandler(): void { | ||||
|     if (this.documentClickHandler) { | ||||
|       document.removeEventListener('click', this.documentClickHandler); | ||||
|       this.documentClickHandler = null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Check if multiselect dropdown is open
 | ||||
|   isMultiselectOpen(filter: any, context: string): boolean { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     return this.openMultiselects.has(filterId); | ||||
|   } | ||||
| 
 | ||||
|   // Get count of selected options for a multiselect filter
 | ||||
|   getSelectedOptionsCount(filter: any): number { | ||||
|     if (!filter.value) { | ||||
|       return 0; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.length; | ||||
|     } | ||||
|      | ||||
|     return 0; | ||||
|   } | ||||
| 
 | ||||
|   // Clear all filters
 | ||||
|   clearAllFilters(): void { | ||||
|     // Clear base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.type === 'multiselect') { | ||||
|               filter.value = []; | ||||
|             } else if (filter.type === 'date-range') { | ||||
|               filter.value = { start: null, end: null }; | ||||
|             } else if (filter.type === 'toggle') { | ||||
|               filter.value = false; | ||||
|             } else { | ||||
|               filter.value = ''; | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Close all multiselect dropdowns
 | ||||
|     this.openMultiselects.clear(); | ||||
|      | ||||
|     // Refresh data
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
|    | ||||
|   fetchChartData(): void { | ||||
|     // Set flag to prevent recursive calls
 | ||||
|     this.isFetchingData = true; | ||||
| @ -126,7 +463,49 @@ export class RadarChartComponent implements OnInit, OnChanges { | ||||
|           filterParams = JSON.stringify(filterObj); | ||||
|         } | ||||
|       } | ||||
|       console.log('Base filter parameters:', filterParams); | ||||
|        | ||||
|       // Add common filters to filter parameters
 | ||||
|       const commonFilters = this.filterService.getFilterValues(); | ||||
|       console.log('Common filters from service:', commonFilters); | ||||
|        | ||||
|       if (Object.keys(commonFilters).length > 0) { | ||||
|         // Merge common filters with base filters
 | ||||
|         const mergedFilterObj = {}; | ||||
|          | ||||
|         // Add base filters first
 | ||||
|         if (filterParams) { | ||||
|           try { | ||||
|             const baseFilterObj = JSON.parse(filterParams); | ||||
|             Object.assign(mergedFilterObj, baseFilterObj); | ||||
|           } catch (e) { | ||||
|             console.warn('Failed to parse base filter parameters:', e); | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         // Add common filters using the field name as the key, not the filter id
 | ||||
|         Object.keys(commonFilters).forEach(filterId => { | ||||
|           const filterValue = commonFilters[filterId]; | ||||
|           // Find the filter definition to get the field name
 | ||||
|           const filterDef = this.filterService.getFilters().find(f => f.id === filterId); | ||||
|           if (filterDef && filterDef.field) { | ||||
|             const fieldName = filterDef.field; | ||||
|             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||
|               mergedFilterObj[fieldName] = filterValue; | ||||
|             } | ||||
|           } else { | ||||
|             // Fallback to using filterId as field name if no field is defined
 | ||||
|             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||
|               mergedFilterObj[filterId] = filterValue; | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
|          | ||||
|         if (Object.keys(mergedFilterObj).length > 0) { | ||||
|           filterParams = JSON.stringify(mergedFilterObj); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       console.log('Final filter parameters:', filterParams); | ||||
|        | ||||
|       // Log the URL that will be called
 | ||||
|       const url = `chart/getdashjson/radar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||
| @ -298,6 +677,35 @@ export class RadarChartComponent implements OnInit, OnChanges { | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Add common filters to drilldown filter parameters
 | ||||
|     const commonFilters = this.filterService.getFilterValues(); | ||||
|     if (Object.keys(commonFilters).length > 0) { | ||||
|       // Merge common filters with drilldown filters
 | ||||
|       const mergedFilterObj = {}; | ||||
|        | ||||
|       // Add drilldown filters first
 | ||||
|       if (filterParams) { | ||||
|         try { | ||||
|           const drilldownFilterObj = JSON.parse(filterParams); | ||||
|           Object.assign(mergedFilterObj, drilldownFilterObj); | ||||
|         } catch (e) { | ||||
|           console.warn('Failed to parse drilldown filter parameters:', e); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Add common filters
 | ||||
|       Object.keys(commonFilters).forEach(key => { | ||||
|         const value = commonFilters[key]; | ||||
|         if (value !== undefined && value !== null && value !== '') { | ||||
|           mergedFilterObj[key] = value; | ||||
|         } | ||||
|       }); | ||||
|        | ||||
|       if (Object.keys(mergedFilterObj).length > 0) { | ||||
|         filterParams = JSON.stringify(mergedFilterObj); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Log the URL that will be called
 | ||||
|     const url = `chart/getdashjson/radar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||
|     console.log('Drilldown data URL:', url); | ||||
| @ -321,7 +729,6 @@ export class RadarChartComponent implements OnInit, OnChanges { | ||||
|           this.noDataAvailable = data.chartLabels.length === 0; | ||||
|           this.radarChartLabels = data.chartLabels; | ||||
|           // For radar charts, we need to ensure the data is properly formatted
 | ||||
|           // Each dataset should have a data array with numeric values
 | ||||
|           this.radarChartData = data.chartData.map(dataset => ({ | ||||
|             ...dataset, | ||||
|             data: dataset.data ? dataset.data.map(value => { | ||||
| @ -358,6 +765,7 @@ export class RadarChartComponent implements OnInit, OnChanges { | ||||
|         this.noDataAvailable = true; | ||||
|         this.radarChartLabels = []; | ||||
|         this.radarChartData = []; | ||||
|         // Keep current data in case of error
 | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
| @ -436,7 +844,7 @@ export class RadarChartComponent implements OnInit, OnChanges { | ||||
|       if (this.currentDrilldownLevel === 0) { | ||||
|         // Store original data before entering drilldown mode
 | ||||
|         this.originalRadarChartLabels = [...this.radarChartLabels]; | ||||
|         this.originalRadarChartData = [...this.radarChartData]; | ||||
|         this.originalRadarChartData = JSON.parse(JSON.stringify(this.radarChartData)); | ||||
|         console.log('Stored original data for drilldown'); | ||||
|       } | ||||
|        | ||||
| @ -504,6 +912,12 @@ export class RadarChartComponent implements OnInit, OnChanges { | ||||
|   } | ||||
| 
 | ||||
|   public chartHovered(e: any): void { | ||||
|     console.log(e); | ||||
|     console.log('Radar chart hovered:', e); | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     this.subscriptions.forEach(sub => sub.unsubscribe()); | ||||
|     // Clean up document click handler
 | ||||
|     this.removeDocumentClickHandler(); | ||||
|   } | ||||
| } | ||||
| @ -1,14 +1,283 @@ | ||||
| <div style="display: block"> | ||||
|   <!-- Drilldown mode indicator --> | ||||
|   <div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> | ||||
|     <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> | ||||
|     <button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> | ||||
|       Back to Level {{currentDrilldownLevel - 1}} | ||||
|     </button> | ||||
|     <button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> | ||||
|       Back to Main View | ||||
| <div style="display: block; height: 100%; width: 100%;"> | ||||
|   <!-- Filter Controls Section --> | ||||
|   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||
|     <!-- Base Filters --> | ||||
|     <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0"> | ||||
|       <h4>Base Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of baseFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'base-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Drilldown Filters --> | ||||
|     <div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0"> | ||||
|       <h4>Drilldown Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of drilldownFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'drilldown-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Layer Filters --> | ||||
|     <div class="filter-group" *ngIf="hasActiveLayerFilters()"> | ||||
|       <h4>Layer Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of getActiveLayerFilters()" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'layer-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Clear Filters Button --> | ||||
|     <div class="filter-actions"> | ||||
|       <button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Header row with chart title and drilldown navigation --> | ||||
|   <div class="clr-row header-row"> | ||||
|     <div class="clr-col-6"> | ||||
|       <h3 class="chart-title">{{charttitle || 'Scatter Chart'}}</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> | ||||
|    | ||||
|   <!-- Show current drilldown level --> | ||||
|   <div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0"> | ||||
|     <div class="clr-col-12"> | ||||
|       <div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;"> | ||||
|         <div class="alert-items"> | ||||
|           <div class="alert-item static"> | ||||
|             <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> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <!-- No data message --> | ||||
|   <div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;"> | ||||
| @ -16,7 +285,7 @@ | ||||
|   </div> | ||||
|    | ||||
|   <!-- Chart display --> | ||||
|   <div *ngIf="!noDataAvailable"> | ||||
|   <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);"> | ||||
|     <canvas baseChart | ||||
|       [datasets]="scatterChartData" | ||||
|       [type]="scatterChartType" | ||||
|  | ||||
| @ -0,0 +1,192 @@ | ||||
| .filter-section { | ||||
|   margin-bottom: 20px; | ||||
|   padding: 15px; | ||||
|   border: 1px solid #ddd; | ||||
|   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 { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       gap: 8px; | ||||
|       padding: 4px 0; | ||||
|        | ||||
|       .checkbox-label { | ||||
|         margin: 0; | ||||
|         font-size: 14px; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .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; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Responsive design | ||||
| @media (max-width: 768px) { | ||||
|   .filter-controls { | ||||
|     flex-direction: column; | ||||
|   } | ||||
|    | ||||
|   .filter-item { | ||||
|     min-width: 100%; | ||||
|   } | ||||
|    | ||||
|   .header-row { | ||||
|     .chart-title { | ||||
|       font-size: 16px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,6 +1,8 @@ | ||||
| import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||
| import { ChartData,ChartDataset } from 'chart.js'; | ||||
| import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | ||||
| import { FilterService } from '../../common-filter/filter.service'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-scatter-chart', | ||||
| @ -34,9 +36,20 @@ export class ScatterChartComponent implements OnInit, OnChanges { | ||||
|   // Multi-layer drilldown configuration inputs
 | ||||
|   @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
 | ||||
| 
 | ||||
|   constructor(private dashboardService: Dashboard3Service) { } | ||||
|   constructor( | ||||
|     private dashboardService: Dashboard3Service, | ||||
|     private filterService: FilterService | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     // Subscribe to filter changes
 | ||||
|     this.subscriptions.push( | ||||
|       this.filterService.filterState$.subscribe(filters => { | ||||
|         // When filters change, refresh the chart data
 | ||||
|         this.fetchChartData(); | ||||
|       }) | ||||
|     ); | ||||
|      | ||||
|     // Initialize with default data
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| @ -44,6 +57,12 @@ export class ScatterChartComponent implements OnInit, OnChanges { | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     console.log('ScatterChartComponent input changes:', changes); | ||||
|      | ||||
|     // Initialize filter values if they haven't been initialized yet
 | ||||
|     if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { | ||||
|       this.initializeFilterValues(); | ||||
|       this.filtersInitialized = true; | ||||
|     } | ||||
|      | ||||
|     // Check if any of the key properties have changed
 | ||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||
| @ -107,6 +126,367 @@ export class ScatterChartComponent implements OnInit, OnChanges { | ||||
|   // Flag to prevent infinite loops
 | ||||
|   private isFetchingData: boolean = false; | ||||
|    | ||||
|   // Subscriptions to unsubscribe on destroy
 | ||||
|   private subscriptions: Subscription[] = []; | ||||
| 
 | ||||
|   // Add properties for filter functionality
 | ||||
|   private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
 | ||||
|   private documentClickHandler: ((event: MouseEvent) => void) | null = null; | ||||
|   private filtersInitialized: boolean = false; | ||||
|    | ||||
|   // Initialize filter values with proper default values based on type
 | ||||
|   private initializeFilterValues(): void { | ||||
|     console.log('Initializing filter values'); | ||||
|      | ||||
|     // Initialize base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.value === undefined || filter.value === null) { | ||||
|               switch (filter.type) { | ||||
|                 case 'multiselect': | ||||
|                   filter.value = []; | ||||
|                   break; | ||||
|                 case 'date-range': | ||||
|                   filter.value = { start: null, end: null }; | ||||
|                   break; | ||||
|                 case 'toggle': | ||||
|                   filter.value = false; | ||||
|                   break; | ||||
|                 default: | ||||
|                   filter.value = ''; | ||||
|               } | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     console.log('Filter values initialized:', { | ||||
|       baseFilters: this.baseFilters, | ||||
|       drilldownFilters: this.drilldownFilters, | ||||
|       drilldownLayers: this.drilldownLayers | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active filters
 | ||||
|   hasActiveFilters(): boolean { | ||||
|     return (this.baseFilters && this.baseFilters.length > 0) ||  | ||||
|            (this.drilldownFilters && this.drilldownFilters.length > 0) ||  | ||||
|            this.hasActiveLayerFilters(); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active layer filters for current drilldown level
 | ||||
|   hasActiveLayerFilters(): boolean { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       return layerIndex < this.drilldownLayers.length &&  | ||||
|              this.drilldownLayers[layerIndex].filters &&  | ||||
|              this.drilldownLayers[layerIndex].filters.length > 0; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   // Get active layer filters for current drilldown level
 | ||||
|   getActiveLayerFilters(): any[] { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       if (layerIndex < this.drilldownLayers.length &&  | ||||
|           this.drilldownLayers[layerIndex].filters) { | ||||
|         return this.drilldownLayers[layerIndex].filters; | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Get filter options for dropdown/multiselect filters
 | ||||
|   getFilterOptions(filter: any): string[] { | ||||
|     if (filter.options) { | ||||
|       if (Array.isArray(filter.options)) { | ||||
|         return filter.options; | ||||
|       } else if (typeof filter.options === 'string') { | ||||
|         return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt); | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Check if an option is selected for multiselect filters
 | ||||
|   isOptionSelected(filter: any, option: string): boolean { | ||||
|     if (!filter.value) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.includes(option); | ||||
|     } | ||||
|      | ||||
|     return filter.value === option; | ||||
|   } | ||||
| 
 | ||||
|   // Handle base filter changes
 | ||||
|   onBaseFilterChange(filter: any): void { | ||||
|     console.log('Base filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle drilldown filter changes
 | ||||
|   onDrilldownFilterChange(filter: any): void { | ||||
|     console.log('Drilldown filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle layer filter changes
 | ||||
|   onLayerFilterChange(filter: any): void { | ||||
|     console.log('Layer filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle multiselect changes
 | ||||
|   onMultiSelectChange(filter: any, option: string, event: any): void { | ||||
|     const checked = event.target.checked; | ||||
|      | ||||
|     // Initialize filter.value as array if it's not already
 | ||||
|     if (!Array.isArray(filter.value)) { | ||||
|       filter.value = []; | ||||
|     } | ||||
|      | ||||
|     if (checked) { | ||||
|       // Add option to array if not already present
 | ||||
|       if (!filter.value.includes(option)) { | ||||
|         filter.value.push(option); | ||||
|       } | ||||
|     } else { | ||||
|       // Remove option from array
 | ||||
|       filter.value = filter.value.filter((item: string) => item !== option); | ||||
|     } | ||||
|      | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle date range changes
 | ||||
|   onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void { | ||||
|     filter.value = dateRange; | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Handle toggle changes
 | ||||
|   onToggleChange(filter: any, checked: boolean): void { | ||||
|     filter.value = checked; | ||||
|     // Refresh data when filter changes
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
| 
 | ||||
|   // Toggle multiselect dropdown visibility
 | ||||
|   toggleMultiselect(filter: any, context: string): void { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     if (this.isMultiselectOpen(filter, context)) { | ||||
|       this.openMultiselects.delete(filterId); | ||||
|     } else { | ||||
|       // Close all other multiselects first
 | ||||
|       this.openMultiselects.clear(); | ||||
|       this.openMultiselects.set(filterId, context); | ||||
|        | ||||
|       // Add document click handler to close dropdown when clicking outside
 | ||||
|       this.addDocumentClickHandler(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Add document click handler to close dropdowns when clicking outside
 | ||||
|   private addDocumentClickHandler(): void { | ||||
|     if (!this.documentClickHandler) { | ||||
|       this.documentClickHandler = (event: MouseEvent) => { | ||||
|         const target = event.target as HTMLElement; | ||||
|         // Check if click is outside any multiselect dropdown
 | ||||
|         if (!target.closest('.multiselect-container')) { | ||||
|           this.openMultiselects.clear(); | ||||
|           this.removeDocumentClickHandler(); | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
 | ||||
|       setTimeout(() => { | ||||
|         document.addEventListener('click', this.documentClickHandler!); | ||||
|       }, 0); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Remove document click handler
 | ||||
|   private removeDocumentClickHandler(): void { | ||||
|     if (this.documentClickHandler) { | ||||
|       document.removeEventListener('click', this.documentClickHandler); | ||||
|       this.documentClickHandler = null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Check if multiselect dropdown is open
 | ||||
|   isMultiselectOpen(filter: any, context: string): boolean { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     return this.openMultiselects.has(filterId); | ||||
|   } | ||||
| 
 | ||||
|   // Get count of selected options for a multiselect filter
 | ||||
|   getSelectedOptionsCount(filter: any): number { | ||||
|     if (!filter.value) { | ||||
|       return 0; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.length; | ||||
|     } | ||||
|      | ||||
|     return 0; | ||||
|   } | ||||
| 
 | ||||
|   // Clear all filters
 | ||||
|   clearAllFilters(): void { | ||||
|     // Clear base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.type === 'multiselect') { | ||||
|               filter.value = []; | ||||
|             } else if (filter.type === 'date-range') { | ||||
|               filter.value = { start: null, end: null }; | ||||
|             } else if (filter.type === 'toggle') { | ||||
|               filter.value = false; | ||||
|             } else { | ||||
|               filter.value = ''; | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Close all multiselect dropdowns
 | ||||
|     this.openMultiselects.clear(); | ||||
|      | ||||
|     // Refresh data
 | ||||
|     this.fetchChartData(); | ||||
|   } | ||||
|    | ||||
|   // Transform data to scatter chart format
 | ||||
|   private transformToScatterData(labels: any[], data: any[]): ChartDataset[] { | ||||
|     // For scatter charts, we need to transform the data into scatter format
 | ||||
|     // Scatter charts expect data in the format: {x: number, y: number}
 | ||||
|     console.log('Transforming data to scatter format:', { labels, data }); | ||||
|      | ||||
|     // If we have the expected scatter data format, return it as is
 | ||||
|     if (data && data.length > 0 && data[0].data && data[0].data.length > 0 &&  | ||||
|         typeof data[0].data[0] === 'object' && data[0].data[0].hasOwnProperty('x') &&  | ||||
|         data[0].data[0].hasOwnProperty('y')) { | ||||
|       return data; | ||||
|     } | ||||
|      | ||||
|     // Otherwise, create a default scatter dataset
 | ||||
|     const scatterDatasets: ChartDataset[] = [ | ||||
|       { | ||||
|         data: [ | ||||
|           { x: 1, y: 1 }, | ||||
|           { x: 2, y: 3 }, | ||||
|           { x: 3, y: -2 }, | ||||
|           { x: 4, y: 4 }, | ||||
|           { x: 5, y: -3 }, | ||||
|         ], | ||||
|         label: 'Dataset 1', | ||||
|         pointRadius: 10, | ||||
|         backgroundColor: [ | ||||
|           'red', | ||||
|           'green', | ||||
|           'blue', | ||||
|           'purple', | ||||
|           'yellow', | ||||
|           'brown', | ||||
|           'magenta', | ||||
|           'cyan', | ||||
|           'orange', | ||||
|           'pink' | ||||
|         ], | ||||
|       } | ||||
|     ]; | ||||
|      | ||||
|     return scatterDatasets; | ||||
|   } | ||||
| 
 | ||||
|   fetchChartData(): void { | ||||
|     // Set flag to prevent recursive calls
 | ||||
|     this.isFetchingData = true; | ||||
| @ -139,7 +519,49 @@ export class ScatterChartComponent implements OnInit, OnChanges { | ||||
|           filterParams = JSON.stringify(filterObj); | ||||
|         } | ||||
|       } | ||||
|       console.log('Base filter parameters:', filterParams); | ||||
|        | ||||
|       // Add common filters to filter parameters
 | ||||
|       const commonFilters = this.filterService.getFilterValues(); | ||||
|       console.log('Common filters from service:', commonFilters); | ||||
|        | ||||
|       if (Object.keys(commonFilters).length > 0) { | ||||
|         // Merge common filters with base filters
 | ||||
|         const mergedFilterObj = {}; | ||||
|          | ||||
|         // Add base filters first
 | ||||
|         if (filterParams) { | ||||
|           try { | ||||
|             const baseFilterObj = JSON.parse(filterParams); | ||||
|             Object.assign(mergedFilterObj, baseFilterObj); | ||||
|           } catch (e) { | ||||
|             console.warn('Failed to parse base filter parameters:', e); | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         // Add common filters using the field name as the key, not the filter id
 | ||||
|         Object.keys(commonFilters).forEach(filterId => { | ||||
|           const filterValue = commonFilters[filterId]; | ||||
|           // Find the filter definition to get the field name
 | ||||
|           const filterDef = this.filterService.getFilters().find(f => f.id === filterId); | ||||
|           if (filterDef && filterDef.field) { | ||||
|             const fieldName = filterDef.field; | ||||
|             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||
|               mergedFilterObj[fieldName] = filterValue; | ||||
|             } | ||||
|           } else { | ||||
|             // Fallback to using filterId as field name if no field is defined
 | ||||
|             if (filterValue !== undefined && filterValue !== null && filterValue !== '') { | ||||
|               mergedFilterObj[filterId] = filterValue; | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
|          | ||||
|         if (Object.keys(mergedFilterObj).length > 0) { | ||||
|           filterParams = JSON.stringify(mergedFilterObj); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       console.log('Final filter parameters:', filterParams); | ||||
|        | ||||
|       // Log the URL that will be called
 | ||||
|       const url = `chart/getdashjson/scatter?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||
| @ -287,24 +709,34 @@ export class ScatterChartComponent implements OnInit, OnChanges { | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Convert drilldownFilters to filter parameters for drilldown level
 | ||||
|     let drilldownFilterParams = ''; | ||||
|     if (this.drilldownFilters && this.drilldownFilters.length > 0) { | ||||
|       const filterObj = {}; | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.field && filter.value) { | ||||
|           filterObj[filter.field] = filter.value; | ||||
|     // Add common filters to drilldown filter parameters
 | ||||
|     const commonFilters = this.filterService.getFilterValues(); | ||||
|     if (Object.keys(commonFilters).length > 0) { | ||||
|       // Merge common filters with drilldown filters
 | ||||
|       const mergedFilterObj = {}; | ||||
|        | ||||
|       // Add drilldown filters first
 | ||||
|       if (filterParams) { | ||||
|         try { | ||||
|           const drilldownFilterObj = JSON.parse(filterParams); | ||||
|           Object.assign(mergedFilterObj, drilldownFilterObj); | ||||
|         } catch (e) { | ||||
|           console.warn('Failed to parse drilldown filter parameters:', e); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Add common filters
 | ||||
|       Object.keys(commonFilters).forEach(key => { | ||||
|         const value = commonFilters[key]; | ||||
|         if (value !== undefined && value !== null && value !== '') { | ||||
|           mergedFilterObj[key] = value; | ||||
|         } | ||||
|       }); | ||||
|       if (Object.keys(filterObj).length > 0) { | ||||
|         drilldownFilterParams = JSON.stringify(filterObj); | ||||
|       } | ||||
|     } | ||||
|     console.log('Drilldown filter parameters:', drilldownFilterParams); | ||||
|        | ||||
|     // Use drilldown filters if available, otherwise use layer filters
 | ||||
|     const finalFilterParams = drilldownFilterParams || filterParams; | ||||
|     console.log('Final filter parameters:', finalFilterParams); | ||||
|       if (Object.keys(mergedFilterObj).length > 0) { | ||||
|         filterParams = JSON.stringify(mergedFilterObj); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Log the URL that will be called
 | ||||
|     const url = `chart/getdashjson/scatter?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||
| @ -312,7 +744,7 @@ export class ScatterChartComponent implements OnInit, OnChanges { | ||||
|      | ||||
|     // Fetch data from the dashboard service with parameter field and value
 | ||||
|     // Backend handles filtering, we just pass the parameter field and value
 | ||||
|     this.dashboardService.getChartData(actualApiUrl, 'scatter', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, finalFilterParams).subscribe( | ||||
|     this.dashboardService.getChartData(actualApiUrl, 'scatter', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, filterParams).subscribe( | ||||
|       (data: any) => { | ||||
|         console.log('Received drilldown data:', data); | ||||
|         if (data === null) { | ||||
| @ -325,7 +757,6 @@ export class ScatterChartComponent implements OnInit, OnChanges { | ||||
|         // Handle the actual data structure returned by the API
 | ||||
|         if (data && data.chartLabels && data.chartData) { | ||||
|           // For scatter charts, we need to transform the data into scatter format
 | ||||
|           // Scatter charts expect data in the format: {x: number, y: number}
 | ||||
|           this.noDataAvailable = data.chartLabels.length === 0; | ||||
|           this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData); | ||||
|           console.log('Updated scatter chart with drilldown data:', this.scatterChartData); | ||||
| @ -349,33 +780,6 @@ export class ScatterChartComponent implements OnInit, OnChanges { | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   // Transform chart data to scatter chart format
 | ||||
|   private transformToScatterData(labels: string[], datasets: any[]): ChartDataset[] { | ||||
|     // For scatter charts, we need to transform the data into scatter format
 | ||||
|     // Scatter charts expect data in the format: {x: number, y: number}
 | ||||
|      | ||||
|     // This is a simple transformation - in a real implementation, you might want to
 | ||||
|     // create a more sophisticated mapping based on your data structure
 | ||||
|     return datasets.map((dataset, index) => { | ||||
|       // Create scatter data points
 | ||||
|       const scatterData = labels.map((label, i) => { | ||||
|         // Use x-axis data as x coordinate, y-axis data as y coordinate
 | ||||
|         const xValue = dataset.data[i] || 0; | ||||
|         const yValue = i < datasets.length ? (datasets[i].data[index] || 0) : 0; | ||||
|          | ||||
|         return { x: xValue, y: yValue }; | ||||
|       }); | ||||
|        | ||||
|       return { | ||||
|         data: scatterData, | ||||
|         label: dataset.label || `Dataset ${index + 1}`, | ||||
|         backgroundColor: dataset.backgroundColor || `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.6)`, | ||||
|         borderColor: dataset.borderColor || 'rgba(0, 0, 0, 1)', | ||||
|         pointRadius: dataset.pointRadius || 5 | ||||
|       }; | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   // Reset to original data (go back to base level)
 | ||||
|   resetToOriginalData(): void { | ||||
|     console.log('Resetting to original data'); | ||||
| @ -436,16 +840,18 @@ export class ScatterChartComponent implements OnInit, OnChanges { | ||||
|       // Get the index of the clicked element
 | ||||
|       const clickedIndex = e.active[0].index; | ||||
|        | ||||
|       // Get the label of the clicked element
 | ||||
|       // For scatter charts, we might not have labels in the same way as other charts
 | ||||
|       const clickedLabel = `Point ${clickedIndex}`; | ||||
|       // Get the dataset index
 | ||||
|       const datasetIndex = e.active[0].datasetIndex; | ||||
|        | ||||
|       console.log('Clicked on scatter point:', { index: clickedIndex, label: clickedLabel }); | ||||
|       // Get the data point
 | ||||
|       const dataPoint = this.scatterChartData[datasetIndex].data[clickedIndex]; | ||||
|        | ||||
|       console.log('Clicked on scatter point:', { datasetIndex, clickedIndex, dataPoint }); | ||||
|        | ||||
|       // If we're not at the base level, store original data
 | ||||
|       if (this.currentDrilldownLevel === 0) { | ||||
|         // Store original data before entering drilldown mode
 | ||||
|         this.originalScatterChartData = [...this.scatterChartData]; | ||||
|         this.originalScatterChartData = JSON.parse(JSON.stringify(this.scatterChartData)); | ||||
|         console.log('Stored original data for drilldown'); | ||||
|       } | ||||
|        | ||||
| @ -487,9 +893,10 @@ export class ScatterChartComponent implements OnInit, OnChanges { | ||||
|         // Add this click to the drilldown stack
 | ||||
|         const stackEntry = { | ||||
|           level: nextDrilldownLevel, | ||||
|           datasetIndex: datasetIndex, | ||||
|           clickedIndex: clickedIndex, | ||||
|           clickedLabel: clickedLabel, | ||||
|           clickedValue: clickedLabel // Using label as value for now
 | ||||
|           dataPoint: dataPoint, | ||||
|           clickedValue: dataPoint // Using data point as value for now
 | ||||
|         }; | ||||
|          | ||||
|         this.drilldownStack.push(stackEntry); | ||||
| @ -513,6 +920,12 @@ export class ScatterChartComponent implements OnInit, OnChanges { | ||||
|   } | ||||
| 
 | ||||
|   public chartHovered(e: any): void { | ||||
|     console.log(e); | ||||
|     console.log('Scatter chart hovered:', e); | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     this.subscriptions.forEach(sub => sub.unsubscribe()); | ||||
|     // Clean up document click handler
 | ||||
|     this.removeDocumentClickHandler(); | ||||
|   } | ||||
| } | ||||
| @ -1,4 +1,286 @@ | ||||
| <table class="table"> | ||||
| <div class="to-do-chart-container"> | ||||
|   <!-- Filter Controls Section --> | ||||
|   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||
|     <!-- Base Filters --> | ||||
|     <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0"> | ||||
|       <h4>Base Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of baseFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onBaseFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'base-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Drilldown Filters --> | ||||
|     <div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0"> | ||||
|       <h4>Drilldown Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of drilldownFilters" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onDrilldownFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'drilldown-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Layer Filters --> | ||||
|     <div class="filter-group" *ngIf="hasActiveLayerFilters()"> | ||||
|       <h4>Layer Filters</h4> | ||||
|       <div class="filter-controls"> | ||||
|         <div *ngFor="let filter of getActiveLayerFilters()" class="filter-item"> | ||||
|           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||
|            | ||||
|           <!-- Text Filter --> | ||||
|           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||
|             <input type="text"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                    [placeholder]="filter.field" | ||||
|                    class="clr-input filter-text-input"> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Dropdown Filter --> | ||||
|           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||
|             <select [(ngModel)]="filter.value"  | ||||
|                     (ngModelChange)="onLayerFilterChange(filter)" | ||||
|                     class="clr-select filter-select"> | ||||
|               <option value="">Select {{ filter.field }}</option> | ||||
|               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Multi-Select Filter - Updated to show key first, then dropdown on click --> | ||||
|           <div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')"> | ||||
|               <span class="multiselect-label">{{ filter.field }}</span> | ||||
|               <span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 ({{ getSelectedOptionsCount(filter) }} selected) | ||||
|               </span> | ||||
|               <clr-icon shape="caret down" class="dropdown-icon"></clr-icon> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')"> | ||||
|               <div class="checkbox-group"> | ||||
|                 <div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item"> | ||||
|                   <input type="checkbox"  | ||||
|                          [checked]="isOptionSelected(filter, option)"  | ||||
|                          (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                          [id]="'layer-' + filter.field + '-' + i"  | ||||
|                          class="clr-checkbox"> | ||||
|                   <label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Date Range Filter --> | ||||
|           <div *ngIf="filter.type === 'date-range'" class="filter-input date-range"> | ||||
|             <div class="date-input-group"> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.start"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })" | ||||
|                      placeholder="Start Date" | ||||
|                      class="clr-input filter-date"> | ||||
|               <span class="date-separator">to</span> | ||||
|               <input type="date"  | ||||
|                      [(ngModel)]="filter.value.end"  | ||||
|                      (ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })" | ||||
|                      placeholder="End Date" | ||||
|                      class="clr-input filter-date"> | ||||
|             </div> | ||||
|           </div> | ||||
|            | ||||
|           <!-- Toggle Filter --> | ||||
|           <div *ngIf="filter.type === 'toggle'" class="filter-input toggle"> | ||||
|             <input type="checkbox"  | ||||
|                    [(ngModel)]="filter.value"  | ||||
|                    (ngModelChange)="onToggleChange(filter, $event)" | ||||
|                    clrToggle  | ||||
|                    class="clr-toggle"> | ||||
|             <label class="toggle-label">{{ filter.field }}</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <!-- Clear Filters Button --> | ||||
|     <div class="filter-actions"> | ||||
|       <button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Header row with chart title and drilldown navigation --> | ||||
|   <div class="clr-row header-row"> | ||||
|     <div class="clr-col-6"> | ||||
|       <h3 class="chart-title">{{charttitle || 'To Do Chart'}}</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> | ||||
|    | ||||
|   <!-- Show current drilldown level --> | ||||
|   <div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0"> | ||||
|     <div class="clr-col-12"> | ||||
|       <div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;"> | ||||
|         <div class="alert-items"> | ||||
|           <div class="alert-item static"> | ||||
|             <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> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Existing content --> | ||||
|   <table class="table todo-table"> | ||||
|     <thead> | ||||
|     <th class="c-col">#</th> | ||||
|     <th>Item</th> | ||||
| @ -16,7 +298,7 @@ | ||||
|     <tr> | ||||
|         <td></td> | ||||
|         <td> | ||||
|         <input [(ngModel)]="todo" placeholder="Add Todo" class="clr-input"> | ||||
|           <input [(ngModel)]="todo" placeholder="Add Todo" class="clr-input todo-input"> | ||||
|         </td> | ||||
|         <td style="text-align:right"> | ||||
|             <a  routerLink="." color='primary' (click)="addTodo(todo)"> | ||||
| @ -24,4 +306,5 @@ | ||||
|             </a> | ||||
|         </td> | ||||
|     </tr> | ||||
| </table> | ||||
|   </table> | ||||
| </div> | ||||
| @ -0,0 +1,249 @@ | ||||
| .filter-section { | ||||
|   margin-bottom: 20px; | ||||
|   padding: 15px; | ||||
|   border: 1px solid #ddd; | ||||
|   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 { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       gap: 8px; | ||||
|       padding: 4px 0; | ||||
|        | ||||
|       .checkbox-label { | ||||
|         margin: 0; | ||||
|         font-size: 14px; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .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; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Responsive design | ||||
| @media (max-width: 768px) { | ||||
|   .filter-controls { | ||||
|     flex-direction: column; | ||||
|   } | ||||
|    | ||||
|   .filter-item { | ||||
|     min-width: 100%; | ||||
|   } | ||||
|    | ||||
|   .header-row { | ||||
|     .chart-title { | ||||
|       font-size: 16px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // To Do Chart specific styles | ||||
| .to-do-chart-container { | ||||
|   padding: 20px; | ||||
| } | ||||
| 
 | ||||
| .todo-table { | ||||
|   width: 100%; | ||||
|   border-collapse: collapse; | ||||
|   margin-top: 20px; | ||||
|    | ||||
|   th, td { | ||||
|     padding: 12px; | ||||
|     text-align: left; | ||||
|     border-bottom: 1px solid #ddd; | ||||
|   } | ||||
|    | ||||
|   th { | ||||
|     background-color: #f2f2f2; | ||||
|     font-weight: bold; | ||||
|   } | ||||
|    | ||||
|   tr:hover { | ||||
|     background-color: #f5f5f5; | ||||
|   } | ||||
|    | ||||
|   .c-col { | ||||
|     width: 50px; | ||||
|   } | ||||
|    | ||||
|   .todo-input { | ||||
|     width: 100%; | ||||
|     padding: 8px; | ||||
|     border: 1px solid #ccc; | ||||
|     border-radius: 4px; | ||||
|   } | ||||
|    | ||||
|   .add-button, .remove-button { | ||||
|     background: none; | ||||
|     border: none; | ||||
|     cursor: pointer; | ||||
|     padding: 5px; | ||||
|     border-radius: 3px; | ||||
|      | ||||
|     &:hover { | ||||
|       background-color: #e0e0e0; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   .add-button { | ||||
|     color: #28a745; | ||||
|   } | ||||
|    | ||||
|   .remove-button { | ||||
|     color: #dc3545; | ||||
|   } | ||||
| } | ||||
| @ -1,4 +1,6 @@ | ||||
| import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||
| import { FilterService } from '../../common-filter/filter.service'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-to-do-chart', | ||||
| @ -21,15 +23,43 @@ export class ToDoChartComponent implements OnInit, OnChanges { | ||||
|   @Input() datasource: string; | ||||
|   @Input() fieldName: string; | ||||
|   @Input() connection: number; // Add connection input
 | ||||
|   // Drilldown configuration inputs
 | ||||
|   @Input() drilldownEnabled: boolean = false; | ||||
|   @Input() drilldownApiUrl: string; | ||||
|   @Input() drilldownXAxis: string; | ||||
|   @Input() drilldownYAxis: string; | ||||
|   @Input() drilldownParameter: string; // Add drilldown parameter input
 | ||||
|   @Input() baseFilters: any[] = []; // Add base filters input
 | ||||
|   @Input() drilldownFilters: any[] = []; // Add drilldown filters input
 | ||||
|   // Multi-layer drilldown configuration inputs
 | ||||
|   @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
 | ||||
|    | ||||
|   constructor() { } | ||||
|   // Multi-layer drilldown state tracking
 | ||||
|   drilldownStack: any[] = []; // Stack to track drilldown navigation history
 | ||||
|   currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
 | ||||
|   originalTodoList: string[] = []; | ||||
| 
 | ||||
|   constructor(private filterService: FilterService) { } | ||||
|    | ||||
|   ngOnInit(): void { | ||||
|     // Subscribe to filter changes
 | ||||
|     this.subscriptions.push( | ||||
|       this.filterService.filterState$.subscribe(filters => { | ||||
|         // When filters change, refresh the chart data
 | ||||
|         // For To Do chart, this would trigger a refresh of the todo list
 | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     console.log('ToDoChartComponent input changes:', changes); | ||||
|      | ||||
|     // Initialize filter values if they haven't been initialized yet
 | ||||
|     if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { | ||||
|       this.initializeFilterValues(); | ||||
|       this.filtersInitialized = true; | ||||
|     } | ||||
|      | ||||
|     // Check if any of the key properties have changed
 | ||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||
| @ -48,6 +78,14 @@ export class ToDoChartComponent implements OnInit, OnChanges { | ||||
|   todo: string; | ||||
|   todoList = ['todo 1']; | ||||
|    | ||||
|   // Add properties for filter functionality
 | ||||
|   private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
 | ||||
|   private documentClickHandler: ((event: MouseEvent) => void) | null = null; | ||||
|   private filtersInitialized: boolean = false; | ||||
|    | ||||
|   // Subscriptions to unsubscribe on destroy
 | ||||
|   private subscriptions: Subscription[] = []; | ||||
|    | ||||
|   fetchToDoData(): void { | ||||
|     // If we have the necessary data, fetch to-do data from the service
 | ||||
|     if (this.table) { | ||||
| @ -73,4 +111,322 @@ export class ToDoChartComponent implements OnInit, OnChanges { | ||||
|         this.todoList.splice(todoIx, 1); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Initialize filter values with proper default values based on type
 | ||||
|   private initializeFilterValues(): void { | ||||
|     console.log('Initializing filter values'); | ||||
|      | ||||
|     // Initialize base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.value === undefined || filter.value === null) { | ||||
|           switch (filter.type) { | ||||
|             case 'multiselect': | ||||
|               filter.value = []; | ||||
|               break; | ||||
|             case 'date-range': | ||||
|               filter.value = { start: null, end: null }; | ||||
|               break; | ||||
|             case 'toggle': | ||||
|               filter.value = false; | ||||
|               break; | ||||
|             default: | ||||
|               filter.value = ''; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Initialize layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.value === undefined || filter.value === null) { | ||||
|               switch (filter.type) { | ||||
|                 case 'multiselect': | ||||
|                   filter.value = []; | ||||
|                   break; | ||||
|                 case 'date-range': | ||||
|                   filter.value = { start: null, end: null }; | ||||
|                   break; | ||||
|                 case 'toggle': | ||||
|                   filter.value = false; | ||||
|                   break; | ||||
|                 default: | ||||
|                   filter.value = ''; | ||||
|               } | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     console.log('Filter values initialized:', { | ||||
|       baseFilters: this.baseFilters, | ||||
|       drilldownFilters: this.drilldownFilters, | ||||
|       drilldownLayers: this.drilldownLayers | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active filters
 | ||||
|   hasActiveFilters(): boolean { | ||||
|     return (this.baseFilters && this.baseFilters.length > 0) ||  | ||||
|            (this.drilldownFilters && this.drilldownFilters.length > 0) ||  | ||||
|            this.hasActiveLayerFilters(); | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are active layer filters for current drilldown level
 | ||||
|   hasActiveLayerFilters(): boolean { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       return layerIndex < this.drilldownLayers.length &&  | ||||
|              this.drilldownLayers[layerIndex].filters &&  | ||||
|              this.drilldownLayers[layerIndex].filters.length > 0; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   // Get active layer filters for current drilldown level
 | ||||
|   getActiveLayerFilters(): any[] { | ||||
|     if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { | ||||
|       const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
 | ||||
|       if (layerIndex < this.drilldownLayers.length &&  | ||||
|           this.drilldownLayers[layerIndex].filters) { | ||||
|         return this.drilldownLayers[layerIndex].filters; | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Get filter options for dropdown/multiselect filters
 | ||||
|   getFilterOptions(filter: any): string[] { | ||||
|     if (filter.options) { | ||||
|       if (Array.isArray(filter.options)) { | ||||
|         return filter.options; | ||||
|       } else if (typeof filter.options === 'string') { | ||||
|         return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt); | ||||
|       } | ||||
|     } | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   // Check if an option is selected for multiselect filters
 | ||||
|   isOptionSelected(filter: any, option: string): boolean { | ||||
|     if (!filter.value) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.includes(option); | ||||
|     } | ||||
|      | ||||
|     return filter.value === option; | ||||
|   } | ||||
| 
 | ||||
|   // Handle base filter changes
 | ||||
|   onBaseFilterChange(filter: any): void { | ||||
|     console.log('Base filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     // For To Do chart, this would trigger a refresh of the todo list
 | ||||
|   } | ||||
| 
 | ||||
|   // Handle drilldown filter changes
 | ||||
|   onDrilldownFilterChange(filter: any): void { | ||||
|     console.log('Drilldown filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     // For To Do chart, this would trigger a refresh of the todo list
 | ||||
|   } | ||||
| 
 | ||||
|   // Handle layer filter changes
 | ||||
|   onLayerFilterChange(filter: any): void { | ||||
|     console.log('Layer filter changed:', filter); | ||||
|     // Refresh data when filter changes
 | ||||
|     // For To Do chart, this would trigger a refresh of the todo list
 | ||||
|   } | ||||
| 
 | ||||
|   // Handle multiselect changes
 | ||||
|   onMultiSelectChange(filter: any, option: string, event: any): void { | ||||
|     const checked = event.target.checked; | ||||
|      | ||||
|     // Initialize filter.value as array if it's not already
 | ||||
|     if (!Array.isArray(filter.value)) { | ||||
|       filter.value = []; | ||||
|     } | ||||
|      | ||||
|     if (checked) { | ||||
|       // Add option to array if not already present
 | ||||
|       if (!filter.value.includes(option)) { | ||||
|         filter.value.push(option); | ||||
|       } | ||||
|     } else { | ||||
|       // Remove option from array
 | ||||
|       filter.value = filter.value.filter((item: string) => item !== option); | ||||
|     } | ||||
|      | ||||
|     // Refresh data when filter changes
 | ||||
|     // For To Do chart, this would trigger a refresh of the todo list
 | ||||
|   } | ||||
| 
 | ||||
|   // Handle date range changes
 | ||||
|   onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void { | ||||
|     filter.value = dateRange; | ||||
|     // Refresh data when filter changes
 | ||||
|     // For To Do chart, this would trigger a refresh of the todo list
 | ||||
|   } | ||||
| 
 | ||||
|   // Handle toggle changes
 | ||||
|   onToggleChange(filter: any, checked: boolean): void { | ||||
|     filter.value = checked; | ||||
|     // Refresh data when filter changes
 | ||||
|     // For To Do chart, this would trigger a refresh of the todo list
 | ||||
|   } | ||||
| 
 | ||||
|   // Toggle multiselect dropdown visibility
 | ||||
|   toggleMultiselect(filter: any, context: string): void { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     if (this.isMultiselectOpen(filter, context)) { | ||||
|       this.openMultiselects.delete(filterId); | ||||
|     } else { | ||||
|       // Close all other multiselects first
 | ||||
|       this.openMultiselects.clear(); | ||||
|       this.openMultiselects.set(filterId, context); | ||||
|        | ||||
|       // Add document click handler to close dropdown when clicking outside
 | ||||
|       this.addDocumentClickHandler(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Add document click handler to close dropdowns when clicking outside
 | ||||
|   private addDocumentClickHandler(): void { | ||||
|     if (!this.documentClickHandler) { | ||||
|       this.documentClickHandler = (event: MouseEvent) => { | ||||
|         const target = event.target as HTMLElement; | ||||
|         // Check if click is outside any multiselect dropdown
 | ||||
|         if (!target.closest('.multiselect-container')) { | ||||
|           this.openMultiselects.clear(); | ||||
|           this.removeDocumentClickHandler(); | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
 | ||||
|       setTimeout(() => { | ||||
|         document.addEventListener('click', this.documentClickHandler!); | ||||
|       }, 0); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Remove document click handler
 | ||||
|   private removeDocumentClickHandler(): void { | ||||
|     if (this.documentClickHandler) { | ||||
|       document.removeEventListener('click', this.documentClickHandler); | ||||
|       this.documentClickHandler = null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Check if multiselect dropdown is open
 | ||||
|   isMultiselectOpen(filter: any, context: string): boolean { | ||||
|     const filterId = `${context}-${filter.field}`; | ||||
|     return this.openMultiselects.has(filterId); | ||||
|   } | ||||
| 
 | ||||
|   // Get count of selected options for a multiselect filter
 | ||||
|   getSelectedOptionsCount(filter: any): number { | ||||
|     if (!filter.value) { | ||||
|       return 0; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(filter.value)) { | ||||
|       return filter.value.length; | ||||
|     } | ||||
|      | ||||
|     return 0; | ||||
|   } | ||||
| 
 | ||||
|   // Clear all filters
 | ||||
|   clearAllFilters(): void { | ||||
|     // Clear base filters
 | ||||
|     if (this.baseFilters) { | ||||
|       this.baseFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear drilldown filters
 | ||||
|     if (this.drilldownFilters) { | ||||
|       this.drilldownFilters.forEach(filter => { | ||||
|         if (filter.type === 'multiselect') { | ||||
|           filter.value = []; | ||||
|         } else if (filter.type === 'date-range') { | ||||
|           filter.value = { start: null, end: null }; | ||||
|         } else if (filter.type === 'toggle') { | ||||
|           filter.value = false; | ||||
|         } else { | ||||
|           filter.value = ''; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Clear layer filters
 | ||||
|     if (this.drilldownLayers) { | ||||
|       this.drilldownLayers.forEach(layer => { | ||||
|         if (layer.filters) { | ||||
|           layer.filters.forEach((filter: any) => { | ||||
|             if (filter.type === 'multiselect') { | ||||
|               filter.value = []; | ||||
|             } else if (filter.type === 'date-range') { | ||||
|               filter.value = { start: null, end: null }; | ||||
|             } else if (filter.type === 'toggle') { | ||||
|               filter.value = false; | ||||
|             } else { | ||||
|               filter.value = ''; | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Close all multiselect dropdowns
 | ||||
|     this.openMultiselects.clear(); | ||||
|      | ||||
|     // Refresh data
 | ||||
|     // For To Do chart, this would trigger a refresh of the todo list
 | ||||
|   } | ||||
|    | ||||
|   ngOnDestroy(): void { | ||||
|     // Unsubscribe from all subscriptions to prevent memory leaks
 | ||||
|     this.subscriptions.forEach(subscription => subscription.unsubscribe()); | ||||
|      | ||||
|     // Remove document click handler if it exists
 | ||||
|     this.removeDocumentClickHandler(); | ||||
|   } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user