chart
This commit is contained in:
		
							parent
							
								
									87810acc9e
								
							
						
					
					
						commit
						bedcc0822d
					
				| @ -1,13 +1,282 @@ | |||||||
| <div style="display:block"> | <div style="display:block; height: 100%; width: 100%;"> | ||||||
|   <!-- Drilldown mode indicator --> |   <!-- Filter Controls Section --> | ||||||
|   <div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> |   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||||
|     <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> |     <!-- Base Filters --> | ||||||
|     <button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> |     <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0"> | ||||||
|       Back to Level {{currentDrilldownLevel - 1}} |       <h4>Base Filters</h4> | ||||||
|     </button> |       <div class="filter-controls"> | ||||||
|     <button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> |         <div *ngFor="let filter of baseFilters" class="filter-item"> | ||||||
|       Back to Main View |           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||||
|     </button> |            | ||||||
|  |           <!-- 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> |   </div> | ||||||
|    |    | ||||||
|   <!-- No data message --> |   <!-- No data message --> | ||||||
| @ -16,7 +285,7 @@ | |||||||
|   </div> |   </div> | ||||||
|    |    | ||||||
|   <!-- Chart display --> |   <!-- Chart display --> | ||||||
|   <div *ngIf="!noDataAvailable"> |   <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);"> | ||||||
|     <canvas baseChart |     <canvas baseChart | ||||||
|     [datasets]="bubbleChartData" |     [datasets]="bubbleChartData" | ||||||
|     [type]="bubbleChartType" |     [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 { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||||
| import { ChartConfiguration, ChartDataset, ChartOptions } from 'chart.js'; | import { ChartConfiguration, ChartDataset, ChartOptions } from 'chart.js'; | ||||||
| import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | ||||||
|  | import { FilterService } from '../../common-filter/filter.service'; | ||||||
|  | import { Subscription } from 'rxjs'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-bubble-chart', |   selector: 'app-bubble-chart', | ||||||
| @ -95,16 +97,41 @@ export class BubbleChartComponent implements OnInit, OnChanges { | |||||||
|    |    | ||||||
|   // Flag to prevent infinite loops
 |   // Flag to prevent infinite loops
 | ||||||
|   private isFetchingData: boolean = false; |   private isFetchingData: boolean = false; | ||||||
|  |    | ||||||
|  |   // Subscriptions to unsubscribe on destroy
 | ||||||
|  |   private subscriptions: Subscription[] = []; | ||||||
| 
 | 
 | ||||||
|   constructor(private dashboardService: Dashboard3Service) { } |   // 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 { |   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(); |     this.fetchChartData(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   ngOnChanges(changes: SimpleChanges): void { |   ngOnChanges(changes: SimpleChanges): void { | ||||||
|     console.log('BubbleChartComponent input changes:', changes); |     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
 |     // Check if any of the key properties have changed
 | ||||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; |     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; |     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||||
| @ -127,6 +154,349 @@ export class BubbleChartComponent implements OnInit, OnChanges { | |||||||
|       this.fetchChartData(); |       this.fetchChartData(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |    | ||||||
|  |   // 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 { |   fetchChartData(): void { | ||||||
|     // Set flag to prevent recursive calls
 |     // Set flag to prevent recursive calls
 | ||||||
| @ -160,7 +530,49 @@ export class BubbleChartComponent implements OnInit, OnChanges { | |||||||
|           filterParams = JSON.stringify(filterObj); |           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
 |       // 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}` : ''}`; |       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 = []; |           this.bubbleChartData = []; | ||||||
|           // Reset flag after fetching
 |           // Reset flag after fetching
 | ||||||
|           this.isFetchingData = false; |           this.isFetchingData = false; | ||||||
|  |           // Keep default data in case of error
 | ||||||
|         } |         } | ||||||
|       ); |       ); | ||||||
|     } else { |     } 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
 |     // 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}` : ''}`; |     const url = `chart/getdashjson/bubble?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||||
|     console.log('Drilldown data URL:', url); |     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
 |         // Handle the actual data structure returned by the API
 | ||||||
|         if (data && data.chartLabels && data.chartData) { |         if (data && data.chartLabels && data.chartData) { | ||||||
|           // For bubble charts, we need to transform the data into bubble format
 |           // 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.noDataAvailable = data.chartLabels.length === 0; | ||||||
|           this.bubbleChartData = this.transformToBubbleData(data.chartLabels, data.chartData); |           this.bubbleChartData = this.transformToBubbleData(data.chartLabels, data.chartData); | ||||||
|           console.log('Updated bubble chart with drilldown data:', this.bubbleChartData); |           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); |         console.error('Error fetching drilldown data:', error); | ||||||
|         this.noDataAvailable = true; |         this.noDataAvailable = true; | ||||||
|         this.bubbleChartData = []; |         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)
 |   // Reset to original data (go back to base level)
 | ||||||
|   resetToOriginalData(): void { |   resetToOriginalData(): void { | ||||||
|     console.log('Resetting to original data'); |     console.log('Resetting to original data'); | ||||||
| @ -438,16 +851,18 @@ export class BubbleChartComponent implements OnInit, OnChanges { | |||||||
|       // Get the index of the clicked element
 |       // Get the index of the clicked element
 | ||||||
|       const clickedIndex = e.active[0].index; |       const clickedIndex = e.active[0].index; | ||||||
|        |        | ||||||
|       // Get the label of the clicked element
 |       // Get the dataset index
 | ||||||
|       // For bubble charts, we might not have labels in the same way as other charts
 |       const datasetIndex = e.active[0].datasetIndex; | ||||||
|       const clickedLabel = `Bubble ${clickedIndex}`; |  | ||||||
|        |        | ||||||
|       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 we're not at the base level, store original data
 | ||||||
|       if (this.currentDrilldownLevel === 0) { |       if (this.currentDrilldownLevel === 0) { | ||||||
|         // Store original data before entering drilldown mode
 |         // 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'); |         console.log('Stored original data for drilldown'); | ||||||
|       } |       } | ||||||
|        |        | ||||||
| @ -489,9 +904,10 @@ export class BubbleChartComponent implements OnInit, OnChanges { | |||||||
|         // Add this click to the drilldown stack
 |         // Add this click to the drilldown stack
 | ||||||
|         const stackEntry = { |         const stackEntry = { | ||||||
|           level: nextDrilldownLevel, |           level: nextDrilldownLevel, | ||||||
|  |           datasetIndex: datasetIndex, | ||||||
|           clickedIndex: clickedIndex, |           clickedIndex: clickedIndex, | ||||||
|           clickedLabel: clickedLabel, |           dataPoint: dataPoint, | ||||||
|           clickedValue: clickedLabel // Using label as value for now
 |           clickedValue: dataPoint // Using data point as value for now
 | ||||||
|         }; |         }; | ||||||
|          |          | ||||||
|         this.drilldownStack.push(stackEntry); |         this.drilldownStack.push(stackEntry); | ||||||
| @ -515,6 +931,6 @@ export class BubbleChartComponent implements OnInit, OnChanges { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public chartHovered(e: any): void { |   public chartHovered(e: any): void { | ||||||
|     console.log(e); |     console.log('Bubble chart hovered:', e); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -1,26 +1,282 @@ | |||||||
| <div class="doughnut-chart-container"> | <div class="doughnut-chart-container"> | ||||||
|   <!-- Compact Filters --> |   <!-- Filter Controls Section --> | ||||||
|   <div class="compact-filters-container" *ngIf="baseFilters && baseFilters.length > 0"> |   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||||
|     <app-compact-filter  |     <!-- Base Filters --> | ||||||
|       *ngFor="let filter of baseFilters"  |     <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0"> | ||||||
|       [filterKey]="filter.field" |       <h4>Base Filters</h4> | ||||||
|       (filterChange)="onFilterChange($event)"> |       <div class="filter-controls"> | ||||||
|     </app-compact-filter> |         <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> |   </div> | ||||||
|    |    | ||||||
|   <!-- Drilldown mode indicator --> |   <!-- Header row with chart title and drilldown navigation --> | ||||||
|   <div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator"> |   <div class="clr-row header-row"> | ||||||
|     <span class="drilldown-text">Drilldown Level: {{currentDrilldownLevel}}</span> |     <div class="clr-col-6"> | ||||||
|     <button class="btn btn-secondary btn-sm" (click)="navigateBack()"> |       <h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3> | ||||||
|       Back to Level {{currentDrilldownLevel - 1}} |     </div> | ||||||
|     </button> |     <div class="clr-col-6" style="text-align: right;"> | ||||||
|     <button class="btn btn-danger btn-sm" (click)="resetToOriginalData()"> |       <!-- Add drilldown navigation controls --> | ||||||
|       Back to Main View |       <button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()"> | ||||||
|     </button> |         <cds-icon shape="arrow" direction="left"></cds-icon> | ||||||
|  |         Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}} | ||||||
|  |       </button> | ||||||
|  |     </div> | ||||||
|   </div> |   </div> | ||||||
|    |    | ||||||
|   <div class="chart-header"> |   <!-- Show current drilldown level --> | ||||||
|     <h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3> |   <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> | ||||||
|    |    | ||||||
|   <div class="chart-wrapper"> |   <div class="chart-wrapper"> | ||||||
|  | |||||||
| @ -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 */ | /* Responsive design */ | ||||||
| @media (max-width: 768px) { | @media (max-width: 768px) { | ||||||
|   .doughnut-chart-container { |   .doughnut-chart-container { | ||||||
| @ -287,4 +467,18 @@ | |||||||
|   .compact-filters-container { |   .compact-filters-container { | ||||||
|     flex-wrap: wrap; |     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', |   templateUrl: './doughnut-chart.component.html', | ||||||
|   styleUrls: ['./doughnut-chart.component.scss'] |   styleUrls: ['./doughnut-chart.component.scss'] | ||||||
| }) | }) | ||||||
| export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked { | export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy { | ||||||
|   @Input() xAxis: string; |   @Input() xAxis: string; | ||||||
|   @Input() yAxis: string | string[]; |   @Input() yAxis: string | string[]; | ||||||
|   @Input() table: string; |   @Input() table: string; | ||||||
| @ -102,6 +102,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|   // Subscriptions to unsubscribe on destroy
 |   // Subscriptions to unsubscribe on destroy
 | ||||||
|   private subscriptions: Subscription[] = []; |   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( |   constructor( | ||||||
|     private dashboardService: Dashboard3Service, |     private dashboardService: Dashboard3Service, | ||||||
|     private filterService: FilterService |     private filterService: FilterService | ||||||
| @ -164,6 +169,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|   ngOnChanges(changes: SimpleChanges): void { |   ngOnChanges(changes: SimpleChanges): void { | ||||||
|     console.log('DoughnutChartComponent input changes:', changes); |     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
 |     // Check if any of the key properties have changed
 | ||||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; |     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; |     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||||
| @ -198,12 +209,318 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
| 
 | 
 | ||||||
|   ngOnDestroy(): void { |   ngOnDestroy(): void { | ||||||
|     this.subscriptions.forEach(sub => sub.unsubscribe()); |     this.subscriptions.forEach(sub => sub.unsubscribe()); | ||||||
|  |     // Clean up document click handler
 | ||||||
|  |     this.removeDocumentClickHandler(); | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   // Handle filter changes from compact filters
 |   // Initialize filter values with proper default values based on type
 | ||||||
|   onFilterChange(event: { filterId: string, value: any }): void { |   private initializeFilterValues(): void { | ||||||
|     console.log('Compact filter changed:', event); |     console.log('Initializing filter values'); | ||||||
|     // The filter service will automatically trigger chart updates through the subscription
 |      | ||||||
|  |     // 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
 |   // 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
 |       // 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}` : ''}`; |       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
 |       // 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
 |       // 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) => { |         (data: any) => { | ||||||
|           console.log('Received doughnut chart data:', data); |           console.log('Received doughnut chart data:', data); | ||||||
|           if (data === null) { |           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.noDataAvailable = true; | ||||||
|             this.doughnutChartLabels = []; |             this.doughnutChartLabels = []; | ||||||
|             this.doughnutChartData = []; |             this.doughnutChartData = []; | ||||||
|             // Validate and sanitize data to show default data
 |  | ||||||
|             this.validateChartData(); |  | ||||||
|             // Reset flag after fetching
 |             // Reset flag after fetching
 | ||||||
|             this.isFetchingData = false; |             this.isFetchingData = false; | ||||||
|             return; |             return; | ||||||
| @ -310,50 +625,26 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|            |            | ||||||
|           // Handle the actual data structure returned by the API
 |           // Handle the actual data structure returned by the API
 | ||||||
|           if (data && data.chartLabels && data.chartData) { |           if (data && data.chartLabels && data.chartData) { | ||||||
|             // For doughnut charts, we need to extract the data differently
 |             // Backend has already filtered the data, just display it
 | ||||||
|             // The first dataset's data array contains the values for the doughnut chart
 |  | ||||||
|             this.noDataAvailable = data.chartLabels.length === 0; |             this.noDataAvailable = data.chartLabels.length === 0; | ||||||
|             this.doughnutChartLabels = data.chartLabels || []; |             this.doughnutChartLabels = data.chartLabels; | ||||||
|             if (data.chartData && data.chartData.length > 0) { |             this.doughnutChartData = data.chartData; | ||||||
|               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(); |  | ||||||
|             // Trigger change detection
 |             // Trigger change detection
 | ||||||
|             this.doughnutChartData = [...this.doughnutChartData]; |             this.doughnutChartData = [...this.doughnutChartData]; | ||||||
|             console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); |             console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); | ||||||
|           } else if (data && data.labels && data.data) { |           } else if (data && data.labels && data.datasets) { | ||||||
|             // Handle the original expected format as fallback
 |             // Backend has already filtered the data, just display it
 | ||||||
|             this.noDataAvailable = data.labels.length === 0; |             this.noDataAvailable = data.labels.length === 0; | ||||||
|             this.doughnutChartLabels = data.labels || []; |             this.doughnutChartLabels = data.labels; | ||||||
|             this.doughnutChartData = data.data.map(value => { |             this.doughnutChartData = data.datasets[0]?.data || []; | ||||||
|               // 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(); |  | ||||||
|             // Trigger change detection
 |             // Trigger change detection
 | ||||||
|             this.doughnutChartData = [...this.doughnutChartData]; |             this.doughnutChartData = [...this.doughnutChartData]; | ||||||
|             console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); |             console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); | ||||||
|           } else { |           } else { | ||||||
|             console.warn('Doughnut chart received data does not have expected structure', data); |             console.warn('Received data does not have expected structure', data); | ||||||
|             // Reset to default data
 |  | ||||||
|             this.noDataAvailable = true; |             this.noDataAvailable = true; | ||||||
|             this.doughnutChartLabels = []; |             this.doughnutChartLabels = []; | ||||||
|             this.doughnutChartData = []; |             this.doughnutChartData = []; | ||||||
|             // Validate and sanitize data to show default data
 |  | ||||||
|             this.validateChartData(); |  | ||||||
|           } |           } | ||||||
|           // Reset flag after fetching
 |           // Reset flag after fetching
 | ||||||
|           this.isFetchingData = false; |           this.isFetchingData = false; | ||||||
| @ -363,21 +654,16 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|           this.noDataAvailable = true; |           this.noDataAvailable = true; | ||||||
|           this.doughnutChartLabels = []; |           this.doughnutChartLabels = []; | ||||||
|           this.doughnutChartData = []; |           this.doughnutChartData = []; | ||||||
|           // Validate and sanitize data to show default data
 |  | ||||||
|           this.validateChartData(); |  | ||||||
|           // Reset flag after fetching
 |           // Reset flag after fetching
 | ||||||
|           this.isFetchingData = false; |           this.isFetchingData = false; | ||||||
|  |           // Keep default data in case of error
 | ||||||
|         } |         } | ||||||
|       ); |       ); | ||||||
|     } else { |     } else { | ||||||
|       console.log('Missing required data for doughnut chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); |       console.log('Missing required data for chart:', { 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.noDataAvailable = true; | ||||||
|       // This allows static data to be displayed
 |       this.doughnutChartLabels = []; | ||||||
|       this.noDataAvailable = false; |       this.doughnutChartData = []; | ||||||
|       // 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]; |  | ||||||
|       // Reset flag after fetching
 |       // Reset flag after fetching
 | ||||||
|       this.isFetchingData = false; |       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
 |     // 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}` : ''}`; |     const url = `chart/getdashjson/doughnut?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||||
|     console.log('Drilldown data URL:', url); |     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
 |         // Handle the actual data structure returned by the API
 | ||||||
|         if (data && data.chartLabels && data.chartData) { |         if (data && data.chartLabels && data.chartData) { | ||||||
|           // For doughnut charts, we need to extract the data differently
 |           // Backend has already filtered the data, just display it
 | ||||||
|           // The first dataset's data array contains the values for the doughnut chart
 |  | ||||||
|           this.noDataAvailable = data.chartLabels.length === 0; |           this.noDataAvailable = data.chartLabels.length === 0; | ||||||
|           this.doughnutChartLabels = data.chartLabels || []; |           this.doughnutChartLabels = data.chartLabels; | ||||||
|           if (data.chartData && data.chartData.length > 0) { |           this.doughnutChartData = data.chartData; | ||||||
|             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(); |  | ||||||
|           // Trigger change detection
 |           // Trigger change detection
 | ||||||
|           this.doughnutChartData = [...this.doughnutChartData]; |           this.doughnutChartData = [...this.doughnutChartData]; | ||||||
|           console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); |           console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); | ||||||
|         } else if (data && data.labels && data.data) { |         } else if (data && data.labels && data.datasets) { | ||||||
|           // Handle the original expected format as fallback
 |           // Backend has already filtered the data, just display it
 | ||||||
|           this.noDataAvailable = data.labels.length === 0; |           this.noDataAvailable = data.labels.length === 0; | ||||||
|           this.doughnutChartLabels = data.labels || []; |           this.doughnutChartLabels = data.labels; | ||||||
|           this.doughnutChartData = data.data.map(value => { |           this.doughnutChartData = data.datasets[0]?.data || []; | ||||||
|             // 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(); |  | ||||||
|           // Trigger change detection
 |           // Trigger change detection
 | ||||||
|           this.doughnutChartData = [...this.doughnutChartData]; |           this.doughnutChartData = [...this.doughnutChartData]; | ||||||
|           console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); |           console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); | ||||||
| @ -535,8 +829,6 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|           this.noDataAvailable = true; |           this.noDataAvailable = true; | ||||||
|           this.doughnutChartLabels = []; |           this.doughnutChartLabels = []; | ||||||
|           this.doughnutChartData = []; |           this.doughnutChartData = []; | ||||||
|           // Validate and sanitize data
 |  | ||||||
|           this.validateChartData(); |  | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       (error) => { |       (error) => { | ||||||
| @ -604,44 +896,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|       this.resetToOriginalData(); |       this.resetToOriginalData(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|    | 
 | ||||||
|   /** |   // Get legend color for a specific index
 | ||||||
|    * Get color for legend item |   getLegendColor(index: number): string { | ||||||
|    * @param index Index of the legend item |  | ||||||
|    */ |  | ||||||
|   public getLegendColor(index: number): string { |  | ||||||
|     return this.chartColors[index % this.chartColors.length]; |     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
 |   // events
 | ||||||
|   public chartClicked(e: any): void { |   public chartClicked(e: any): void { | ||||||
| @ -729,6 +988,6 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public chartHovered(e: any): void { |   public chartHovered(e: any): void { | ||||||
|     console.log(e); |     console.log('Doughnut chart hovered:', e); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -1,4 +1,285 @@ | |||||||
| <div class="dynamic-chart-container"> | <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 --> |   <!-- Drilldown mode indicator --> | ||||||
|   <div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> |   <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> |     <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 { ChartConfiguration, ChartData, ChartDataset } from 'chart.js'; | ||||||
| import { BaseChartDirective } from 'ng2-charts'; | import { BaseChartDirective } from 'ng2-charts'; | ||||||
| import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | ||||||
|  | import { FilterService } from '../../common-filter/filter.service'; | ||||||
|  | import { Subscription } from 'rxjs'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-dynamic-chart', |   selector: 'app-dynamic-chart', | ||||||
| @ -37,9 +39,20 @@ export class DynamicChartComponent implements OnInit, OnChanges { | |||||||
| 
 | 
 | ||||||
|   @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined; |   @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined; | ||||||
| 
 | 
 | ||||||
|   constructor(private dashboardService: Dashboard3Service) { } |   constructor( | ||||||
|  |     private dashboardService: Dashboard3Service, | ||||||
|  |     private filterService: FilterService | ||||||
|  |   ) { } | ||||||
| 
 | 
 | ||||||
|   ngOnInit(): void { |   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
 |     // Initialize with default data
 | ||||||
|     this.fetchChartData(); |     this.fetchChartData(); | ||||||
|   } |   } | ||||||
| @ -47,6 +60,12 @@ export class DynamicChartComponent implements OnInit, OnChanges { | |||||||
|   ngOnChanges(changes: SimpleChanges): void { |   ngOnChanges(changes: SimpleChanges): void { | ||||||
|     console.log('DynamicChartComponent input changes:', changes); |     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
 |     // Check if any of the key properties have changed
 | ||||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; |     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; |     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||||
| @ -106,6 +125,14 @@ export class DynamicChartComponent implements OnInit, OnChanges { | |||||||
|    |    | ||||||
|   // Flag to prevent infinite loops
 |   // Flag to prevent infinite loops
 | ||||||
|   private isFetchingData: boolean = false; |   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 { | 	fetchChartData(): void { | ||||||
|     // Set flag to prevent recursive calls
 |     // Set flag to prevent recursive calls
 | ||||||
| @ -139,7 +166,49 @@ export class DynamicChartComponent implements OnInit, OnChanges { | |||||||
|           filterParams = JSON.stringify(filterObj); |           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
 |       // 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}` : ''}`; |       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; |     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"> | <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 --> |   <!-- Drilldown mode indicator --> | ||||||
|   <div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> |   <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> |     <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> | ||||||
|  | |||||||
| @ -1,108 +1,192 @@ | |||||||
| .financial-chart-container { | .filter-section { | ||||||
|   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; |  | ||||||
|   margin-bottom: 20px; |   margin-bottom: 20px; | ||||||
|   text-align: center; |   padding: 15px; | ||||||
|   padding-bottom: 15px; |   border: 1px solid #ddd; | ||||||
|   border-bottom: 2px solid #3498db; |   border-radius: 4px; | ||||||
|   text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); |   background-color: #f9f9f9; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .chart-wrapper { | .filter-group { | ||||||
|   position: relative; |   margin-bottom: 15px; | ||||||
|   flex: 1; |    | ||||||
|   min-height: 250px; |   h4 { | ||||||
|   margin: 15px 0; |     margin-top: 0; | ||||||
|   background: #f8f9fa; |     margin-bottom: 10px; | ||||||
|   border: 1px solid #e9ecef; |     color: #333; | ||||||
|   border-radius: 8px; |     font-weight: 600; | ||||||
|   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; |  | ||||||
|   } |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .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 { |   .chart-title { | ||||||
|     font-size: 20px; |     margin: 0; | ||||||
|     margin-bottom: 15px; |     font-size: 18px; | ||||||
|  |     font-weight: 600; | ||||||
|  |     color: #333; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Responsive design | ||||||
|  | @media (max-width: 768px) { | ||||||
|  |   .filter-controls { | ||||||
|  |     flex-direction: column; | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   .chart-wrapper { |   .filter-item { | ||||||
|     min-height: 200px; |     min-width: 100%; | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   .no-data-message { |   .header-row { | ||||||
|     font-size: 16px; |     .chart-title { | ||||||
|     padding: 20px; |       font-size: 16px; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -1,5 +1,7 @@ | |||||||
| import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||||
| import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | ||||||
|  | import { FilterService } from '../../common-filter/filter.service'; | ||||||
|  | import { Subscription } from 'rxjs'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-financial-chart', |   selector: 'app-financial-chart', | ||||||
| @ -33,9 +35,20 @@ export class FinancialChartComponent implements OnInit, OnChanges { | |||||||
|   // Multi-layer drilldown configuration inputs
 |   // Multi-layer drilldown configuration inputs
 | ||||||
|   @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
 |   @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
 | ||||||
| 
 | 
 | ||||||
|   constructor(private dashboardService: Dashboard3Service) { } |   constructor( | ||||||
|  |     private dashboardService: Dashboard3Service, | ||||||
|  |     private filterService: FilterService | ||||||
|  |   ) { } | ||||||
| 
 | 
 | ||||||
|   ngOnInit(): void { |   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
 |     // Initialize with default data
 | ||||||
|     this.fetchChartData(); |     this.fetchChartData(); | ||||||
|   } |   } | ||||||
| @ -43,6 +56,12 @@ export class FinancialChartComponent implements OnInit, OnChanges { | |||||||
|   ngOnChanges(changes: SimpleChanges): void { |   ngOnChanges(changes: SimpleChanges): void { | ||||||
|     console.log('FinancialChartComponent input changes:', changes); |     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
 |     // Check if any of the key properties have changed
 | ||||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; |     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; |     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||||
| @ -86,6 +105,14 @@ export class FinancialChartComponent implements OnInit, OnChanges { | |||||||
|   // Flag to prevent infinite loops
 |   // Flag to prevent infinite loops
 | ||||||
|   private isFetchingData: boolean = false; |   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 { |   fetchChartData(): void { | ||||||
|     // Set flag to prevent recursive calls
 |     // Set flag to prevent recursive calls
 | ||||||
|     this.isFetchingData = true; |     this.isFetchingData = true; | ||||||
| @ -118,7 +145,49 @@ export class FinancialChartComponent implements OnInit, OnChanges { | |||||||
|           filterParams = JSON.stringify(filterObj); |           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
 |       // 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}` : ''}`; |       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 { |   public chartHovered(e: any): void { | ||||||
|     console.log(e); |     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"> | <div class="pie-chart-container"> | ||||||
|   <!-- Drilldown mode indicator --> |   <!-- Filter Controls Section --> | ||||||
|   <div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> |   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||||
|     <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> |     <!-- Base Filters --> | ||||||
|     <button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> |     <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0"> | ||||||
|       Back to Level {{currentDrilldownLevel - 1}} |       <h4>Base Filters</h4> | ||||||
|     </button> |       <div class="filter-controls"> | ||||||
|     <button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> |         <div *ngFor="let filter of baseFilters" class="filter-item"> | ||||||
|       Back to Main View |           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||||
|     </button> |            | ||||||
|  |           <!-- 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> | ||||||
|    |    | ||||||
|   <h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3> |  | ||||||
|   <div class="chart-wrapper"> |   <div class="chart-wrapper"> | ||||||
|     <!-- Show loading indicator --> |     <!-- Show loading indicator --> | ||||||
|     <div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable"> |     <div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable"> | ||||||
|  | |||||||
| @ -149,10 +149,192 @@ | |||||||
|   100% { transform: rotate(360deg); } |   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) { | @media (max-width: 768px) { | ||||||
|   .pie-chart-container { |   .pie-chart-container { | ||||||
|     padding: 15px; |     padding: 15px; | ||||||
|  |     height: auto; | ||||||
|  |     min-height: 300px; | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   .chart-title { |   .chart-title { | ||||||
| @ -179,4 +361,18 @@ | |||||||
|     font-size: 16px; |     font-size: 16px; | ||||||
|     padding: 20px; |     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', |   templateUrl: './pie-chart.component.html', | ||||||
|   styleUrls: ['./pie-chart.component.scss'] |   styleUrls: ['./pie-chart.component.scss'] | ||||||
| }) | }) | ||||||
| export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy { | ||||||
|   @Input() xAxis: string; |   @Input() xAxis: string; | ||||||
|   @Input() yAxis: string | string[]; |   @Input() yAxis: string | string[]; | ||||||
|   @Input() table: string; |   @Input() table: string; | ||||||
| @ -101,6 +101,11 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | |||||||
|   // Subscriptions to unsubscribe on destroy
 |   // Subscriptions to unsubscribe on destroy
 | ||||||
|   private subscriptions: Subscription[] = []; |   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( |   constructor( | ||||||
|     private dashboardService: Dashboard3Service, |     private dashboardService: Dashboard3Service, | ||||||
|     private filterService: FilterService |     private filterService: FilterService | ||||||
| @ -133,6 +138,12 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | |||||||
|   ngOnChanges(changes: SimpleChanges): void { |   ngOnChanges(changes: SimpleChanges): void { | ||||||
|     console.log('PieChartComponent input changes:', changes); |     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
 |     // Check if any of the key properties have changed
 | ||||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; |     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; |     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||||
| @ -158,6 +169,318 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | |||||||
| 
 | 
 | ||||||
|   ngOnDestroy(): void { |   ngOnDestroy(): void { | ||||||
|     this.subscriptions.forEach(sub => sub.unsubscribe()); |     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
 |   // 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
 |       // 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}` : ''}`; |       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
 |       // 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
 |       // 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) => { |         (data: any) => { | ||||||
|           console.log('Received pie chart data:', data); |           console.log('Received pie chart data:', data); | ||||||
|           if (data === null) { |           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.noDataAvailable = true; | ||||||
|             this.pieChartLabels = []; |             this.pieChartLabels = []; | ||||||
|             this.pieChartData = []; |             this.pieChartData = []; | ||||||
|             // Validate and sanitize data to show default data
 |  | ||||||
|             this.validateChartData(); |  | ||||||
|             // Reset flag after fetching
 |             // Reset flag after fetching
 | ||||||
|             this.isFetchingData = false; |             this.isFetchingData = false; | ||||||
|             return; |             return; | ||||||
| @ -264,50 +585,26 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | |||||||
|            |            | ||||||
|           // Handle the actual data structure returned by the API
 |           // Handle the actual data structure returned by the API
 | ||||||
|           if (data && data.chartLabels && data.chartData) { |           if (data && data.chartLabels && data.chartData) { | ||||||
|             // For pie charts, we need to extract the data differently
 |             // Backend has already filtered the data, just display it
 | ||||||
|             // The first dataset's data array contains the values for the pie chart
 |  | ||||||
|             this.noDataAvailable = data.chartLabels.length === 0; |             this.noDataAvailable = data.chartLabels.length === 0; | ||||||
|             this.pieChartLabels = data.chartLabels || []; |             this.pieChartLabels = data.chartLabels; | ||||||
|             if (data.chartData && data.chartData.length > 0) { |             this.pieChartData = data.chartData; | ||||||
|               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(); |  | ||||||
|             // Trigger change detection
 |             // Trigger change detection
 | ||||||
|             this.pieChartData = [...this.pieChartData]; |             this.pieChartData = [...this.pieChartData]; | ||||||
|             console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData }); |             console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData }); | ||||||
|           } else if (data && data.labels && data.data) { |           } else if (data && data.labels && data.datasets) { | ||||||
|             // Handle the original expected format as fallback
 |             // Backend has already filtered the data, just display it
 | ||||||
|             this.noDataAvailable = data.labels.length === 0; |             this.noDataAvailable = data.labels.length === 0; | ||||||
|             this.pieChartLabels = data.labels || []; |             this.pieChartLabels = data.labels; | ||||||
|             this.pieChartData = data.data.map(value => { |             this.pieChartData = data.datasets[0]?.data || []; | ||||||
|               // 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(); |  | ||||||
|             // Trigger change detection
 |             // Trigger change detection
 | ||||||
|             this.pieChartData = [...this.pieChartData]; |             this.pieChartData = [...this.pieChartData]; | ||||||
|             console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData }); |             console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData }); | ||||||
|           } else { |           } else { | ||||||
|             console.warn('Pie chart received data does not have expected structure', data); |             console.warn('Received data does not have expected structure', data); | ||||||
|             // Reset to default data
 |  | ||||||
|             this.noDataAvailable = true; |             this.noDataAvailable = true; | ||||||
|             this.pieChartLabels = []; |             this.pieChartLabels = []; | ||||||
|             this.pieChartData = []; |             this.pieChartData = []; | ||||||
|             // Validate and sanitize data to show default data
 |  | ||||||
|             this.validateChartData(); |  | ||||||
|           } |           } | ||||||
|           // Reset flag after fetching
 |           // Reset flag after fetching
 | ||||||
|           this.isFetchingData = false; |           this.isFetchingData = false; | ||||||
| @ -317,21 +614,16 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | |||||||
|           this.noDataAvailable = true; |           this.noDataAvailable = true; | ||||||
|           this.pieChartLabels = []; |           this.pieChartLabels = []; | ||||||
|           this.pieChartData = []; |           this.pieChartData = []; | ||||||
|           // Validate and sanitize data to show default data
 |  | ||||||
|           this.validateChartData(); |  | ||||||
|           // Reset flag after fetching
 |           // Reset flag after fetching
 | ||||||
|           this.isFetchingData = false; |           this.isFetchingData = false; | ||||||
|  |           // Keep default data in case of error
 | ||||||
|         } |         } | ||||||
|       ); |       ); | ||||||
|     } else { |     } else { | ||||||
|       console.log('Missing required data for pie chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); |       console.log('Missing required data for chart:', { 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.noDataAvailable = true; | ||||||
|       // This allows static data to be displayed
 |       this.pieChartLabels = []; | ||||||
|       this.noDataAvailable = false; |       this.pieChartData = []; | ||||||
|       // 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]; |  | ||||||
|       // Reset flag after fetching
 |       // Reset flag after fetching
 | ||||||
|       this.isFetchingData = false; |       this.isFetchingData = false; | ||||||
|     } |     } | ||||||
| @ -477,39 +769,18 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | |||||||
|          |          | ||||||
|         // Handle the actual data structure returned by the API
 |         // Handle the actual data structure returned by the API
 | ||||||
|         if (data && data.chartLabels && data.chartData) { |         if (data && data.chartLabels && data.chartData) { | ||||||
|           // For pie charts, we need to extract the data differently
 |           // Backend has already filtered the data, just display it
 | ||||||
|           // The first dataset's data array contains the values for the pie chart
 |  | ||||||
|           this.noDataAvailable = data.chartLabels.length === 0; |           this.noDataAvailable = data.chartLabels.length === 0; | ||||||
|           this.pieChartLabels = data.chartLabels || []; |           this.pieChartLabels = data.chartLabels; | ||||||
|           if (data.chartData && data.chartData.length > 0) { |           this.pieChartData = data.chartData; | ||||||
|             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(); |  | ||||||
|           // Trigger change detection
 |           // Trigger change detection
 | ||||||
|           this.pieChartData = [...this.pieChartData]; |           this.pieChartData = [...this.pieChartData]; | ||||||
|           console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData }); |           console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData }); | ||||||
|         } else if (data && data.labels && data.data) { |         } else if (data && data.labels && data.datasets) { | ||||||
|           // Handle the original expected format as fallback
 |           // Backend has already filtered the data, just display it
 | ||||||
|           this.noDataAvailable = data.labels.length === 0; |           this.noDataAvailable = data.labels.length === 0; | ||||||
|           this.pieChartLabels = data.labels || []; |           this.pieChartLabels = data.labels; | ||||||
|           this.pieChartData = data.data.map(value => { |           this.pieChartData = data.datasets[0]?.data || []; | ||||||
|             // 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(); |  | ||||||
|           // Trigger change detection
 |           // Trigger change detection
 | ||||||
|           this.pieChartData = [...this.pieChartData]; |           this.pieChartData = [...this.pieChartData]; | ||||||
|           console.log('Updated pie chart with drilldown legacy data format:', { labels: this.pieChartLabels, data: 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.noDataAvailable = true; | ||||||
|           this.pieChartLabels = []; |           this.pieChartLabels = []; | ||||||
|           this.pieChartData = []; |           this.pieChartData = []; | ||||||
|           // Validate and sanitize data
 |  | ||||||
|           this.validateChartData(); |  | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       (error) => { |       (error) => { | ||||||
| @ -588,84 +857,34 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   // Validate chart data to ensure labels and data arrays have the same length
 | ||||||
|    * Get color for legend item |   private validateChartData(): void { | ||||||
|    * @param index Index of the legend item |     if (this.pieChartLabels && this.pieChartData) { | ||||||
|    */ |       // For pie charts, we need to ensure labels and data arrays have the same length
 | ||||||
|   public getLegendColor(index: number): string { |       const labelCount = this.pieChartLabels.length; | ||||||
|  |       const dataCount = this.pieChartData.length; | ||||||
|  |        | ||||||
|  |       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); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Get legend color for a specific index
 | ||||||
|  |   getLegendColor(index: number): string { | ||||||
|     return this.chartColors[index % this.chartColors.length]; |     return this.chartColors[index % this.chartColors.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) { |  | ||||||
|         this.pieChartData.push(0); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   /** |  | ||||||
|    * 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; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   // events
 |   // events
 | ||||||
|   public chartClicked(e: any): void { |   public chartClicked(e: any): void { | ||||||
|     console.log('Pie chart clicked:', e); |     console.log('Pie chart clicked:', e); | ||||||
| @ -752,6 +971,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public chartHovered(e: any): void { |   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,10 +1,291 @@ | |||||||
| 
 | <div style="display: block; height: 100%; width: 100%;"> | ||||||
| <div style="display: block"> |   <!-- Filter Controls Section --> | ||||||
|   <canvas baseChart |   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||||
|     [datasets]="polarAreaChartData" |     <!-- Base Filters --> | ||||||
|     [labels]="polarAreaChartLabels" |     <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0"> | ||||||
|     [type]="polarAreaChartType" |       <h4>Base Filters</h4> | ||||||
|     (chartHover)="chartHovered($event)" |       <div class="filter-controls"> | ||||||
|    (chartClick)="chartClicked($event)"> |         <div *ngFor="let filter of baseFilters" class="filter-item"> | ||||||
|   </canvas> |           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||||
| </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 || '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" | ||||||
|  |       [type]="polarAreaChartType" | ||||||
|  |       (chartHover)="chartHovered($event)" | ||||||
|  |      (chartClick)="chartClicked($event)"> | ||||||
|  |     </canvas> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
| @ -1,18 +1,192 @@ | |||||||
| // Polar Chart Component Styles | .filter-section { | ||||||
| div[style*="display: block"] { |   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; |   position: relative; | ||||||
|   width: 100%; |  | ||||||
|   height: 100%; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| canvas { | .multiselect-display { | ||||||
|   max-width: 100%; |   display: flex; | ||||||
|   max-height: 100%; |   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 | .multiselect-dropdown { | ||||||
| :host { |   position: absolute; | ||||||
|   display: block; |   top: 100%; | ||||||
|   width: 100%; |   left: 0; | ||||||
|   height: 100%; |   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 { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||||
| import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | ||||||
|  | import { FilterService } from '../../common-filter/filter.service'; | ||||||
|  | import { Subscription } from 'rxjs'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-polar-chart', |   selector: 'app-polar-chart', | ||||||
| @ -33,9 +35,20 @@ export class PolarChartComponent implements OnInit, OnChanges { | |||||||
|   // Multi-layer drilldown configuration inputs
 |   // Multi-layer drilldown configuration inputs
 | ||||||
|   @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
 |   @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
 | ||||||
| 
 | 
 | ||||||
|   constructor(private dashboardService: Dashboard3Service) { } |   constructor( | ||||||
|  |     private dashboardService: Dashboard3Service, | ||||||
|  |     private filterService: FilterService | ||||||
|  |   ) { } | ||||||
| 
 | 
 | ||||||
|   ngOnInit(): void { |   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
 |     // Initialize with default data
 | ||||||
|     this.fetchChartData(); |     this.fetchChartData(); | ||||||
|   } |   } | ||||||
| @ -43,6 +56,12 @@ export class PolarChartComponent implements OnInit, OnChanges { | |||||||
|   ngOnChanges(changes: SimpleChanges): void { |   ngOnChanges(changes: SimpleChanges): void { | ||||||
|     console.log('PolarChartComponent input changes:', changes); |     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
 |     // Check if any of the key properties have changed
 | ||||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; |     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; |     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||||
| @ -85,6 +104,324 @@ export class PolarChartComponent implements OnInit, OnChanges { | |||||||
|   // Flag to prevent infinite loops
 |   // Flag to prevent infinite loops
 | ||||||
|   private isFetchingData: boolean = false; |   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 { |   fetchChartData(): void { | ||||||
|     // Set flag to prevent recursive calls
 |     // Set flag to prevent recursive calls
 | ||||||
|     this.isFetchingData = true; |     this.isFetchingData = true; | ||||||
| @ -117,7 +454,49 @@ export class PolarChartComponent implements OnInit, OnChanges { | |||||||
|           filterParams = JSON.stringify(filterObj); |           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
 |       // 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}` : ''}`; |       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
 |     // 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}` : ''}`; |     const url = `chart/getdashjson/polar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||||
|     console.log('Drilldown data URL:', url); |     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
 |         // Handle the actual data structure returned by the API
 | ||||||
|         if (data && data.chartLabels && data.chartData) { |         if (data && data.chartLabels && data.chartData) { | ||||||
|           // For polar charts, we need to extract the data differently
 |           // 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.noDataAvailable = data.chartLabels.length === 0; | ||||||
|           this.polarAreaChartLabels = data.chartLabels; |           this.polarAreaChartLabels = data.chartLabels; | ||||||
|           if (data.chartData && data.chartData.length > 0) { |           if (data.chartData && data.chartData.length > 0) { | ||||||
| @ -417,13 +824,13 @@ export class PolarChartComponent implements OnInit, OnChanges { | |||||||
|       // Get the label of the clicked element
 |       // Get the label of the clicked element
 | ||||||
|       const clickedLabel = this.polarAreaChartLabels[clickedIndex]; |       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 we're not at the base level, store original data
 | ||||||
|       if (this.currentDrilldownLevel === 0) { |       if (this.currentDrilldownLevel === 0) { | ||||||
|         // Store original data before entering drilldown mode
 |         // Store original data before entering drilldown mode
 | ||||||
|         this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels]; |         this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels]; | ||||||
|         this.originalPolarAreaChartData = [...this.polarAreaChartData]; |         this.originalPolarAreaChartData = JSON.parse(JSON.stringify(this.polarAreaChartData)); | ||||||
|         console.log('Stored original data for drilldown'); |         console.log('Stored original data for drilldown'); | ||||||
|       } |       } | ||||||
|        |        | ||||||
| @ -491,6 +898,12 @@ export class PolarChartComponent implements OnInit, OnChanges { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public chartHovered(e: any): void { |   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,13 +1,282 @@ | |||||||
| <div style="display: block"> | <div style="display: block; height: 100%; width: 100%;"> | ||||||
|   <!-- Drilldown mode indicator --> |   <!-- Filter Controls Section --> | ||||||
|   <div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> |   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||||
|     <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> |     <!-- Base Filters --> | ||||||
|     <button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> |     <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0"> | ||||||
|       Back to Level {{currentDrilldownLevel - 1}} |       <h4>Base Filters</h4> | ||||||
|     </button> |       <div class="filter-controls"> | ||||||
|     <button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> |         <div *ngFor="let filter of baseFilters" class="filter-item"> | ||||||
|       Back to Main View |           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||||
|     </button> |            | ||||||
|  |           <!-- 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> |   </div> | ||||||
|    |    | ||||||
|   <!-- No data message --> |   <!-- No data message --> | ||||||
| @ -16,7 +285,7 @@ | |||||||
|   </div> |   </div> | ||||||
|    |    | ||||||
|   <!-- Chart display --> |   <!-- Chart display --> | ||||||
|   <div *ngIf="!noDataAvailable"> |   <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);"> | ||||||
|     <canvas baseChart |     <canvas baseChart | ||||||
|       [datasets]="radarChartData" |       [datasets]="radarChartData" | ||||||
|       [labels]="radarChartLabels" |       [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 { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||||
| import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | ||||||
|  | import { FilterService } from '../../common-filter/filter.service'; | ||||||
|  | import { Subscription } from 'rxjs'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-radar-chart', |   selector: 'app-radar-chart', | ||||||
| @ -61,16 +63,41 @@ export class RadarChartComponent implements OnInit, OnChanges { | |||||||
|    |    | ||||||
|   // Flag to prevent infinite loops
 |   // Flag to prevent infinite loops
 | ||||||
|   private isFetchingData: boolean = false; |   private isFetchingData: boolean = false; | ||||||
|  |    | ||||||
|  |   // Subscriptions to unsubscribe on destroy
 | ||||||
|  |   private subscriptions: Subscription[] = []; | ||||||
| 
 | 
 | ||||||
|   constructor(private dashboardService: Dashboard3Service) { } |   // 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 { |   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(); |     this.fetchChartData(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   ngOnChanges(changes: SimpleChanges): void { |   ngOnChanges(changes: SimpleChanges): void { | ||||||
|     console.log('RadarChartComponent input changes:', changes); |     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
 |     // Check if any of the key properties have changed
 | ||||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; |     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; |     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||||
| @ -93,7 +120,317 @@ export class RadarChartComponent implements OnInit, OnChanges { | |||||||
|       this.fetchChartData(); |       this.fetchChartData(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |    | ||||||
|  |   // 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 { |   fetchChartData(): void { | ||||||
|     // Set flag to prevent recursive calls
 |     // Set flag to prevent recursive calls
 | ||||||
|     this.isFetchingData = true; |     this.isFetchingData = true; | ||||||
| @ -126,7 +463,49 @@ export class RadarChartComponent implements OnInit, OnChanges { | |||||||
|           filterParams = JSON.stringify(filterObj); |           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
 |       // 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}` : ''}`; |       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
 |     // 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}` : ''}`; |     const url = `chart/getdashjson/radar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; | ||||||
|     console.log('Drilldown data URL:', url); |     console.log('Drilldown data URL:', url); | ||||||
| @ -321,7 +729,6 @@ export class RadarChartComponent implements OnInit, OnChanges { | |||||||
|           this.noDataAvailable = data.chartLabels.length === 0; |           this.noDataAvailable = data.chartLabels.length === 0; | ||||||
|           this.radarChartLabels = data.chartLabels; |           this.radarChartLabels = data.chartLabels; | ||||||
|           // For radar charts, we need to ensure the data is properly formatted
 |           // 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 => ({ |           this.radarChartData = data.chartData.map(dataset => ({ | ||||||
|             ...dataset, |             ...dataset, | ||||||
|             data: dataset.data ? dataset.data.map(value => { |             data: dataset.data ? dataset.data.map(value => { | ||||||
| @ -358,6 +765,7 @@ export class RadarChartComponent implements OnInit, OnChanges { | |||||||
|         this.noDataAvailable = true; |         this.noDataAvailable = true; | ||||||
|         this.radarChartLabels = []; |         this.radarChartLabels = []; | ||||||
|         this.radarChartData = []; |         this.radarChartData = []; | ||||||
|  |         // Keep current data in case of error
 | ||||||
|       } |       } | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| @ -436,7 +844,7 @@ export class RadarChartComponent implements OnInit, OnChanges { | |||||||
|       if (this.currentDrilldownLevel === 0) { |       if (this.currentDrilldownLevel === 0) { | ||||||
|         // Store original data before entering drilldown mode
 |         // Store original data before entering drilldown mode
 | ||||||
|         this.originalRadarChartLabels = [...this.radarChartLabels]; |         this.originalRadarChartLabels = [...this.radarChartLabels]; | ||||||
|         this.originalRadarChartData = [...this.radarChartData]; |         this.originalRadarChartData = JSON.parse(JSON.stringify(this.radarChartData)); | ||||||
|         console.log('Stored original data for drilldown'); |         console.log('Stored original data for drilldown'); | ||||||
|       } |       } | ||||||
|        |        | ||||||
| @ -504,6 +912,12 @@ export class RadarChartComponent implements OnInit, OnChanges { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public chartHovered(e: any): void { |   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,13 +1,282 @@ | |||||||
| <div style="display: block"> | <div style="display: block; height: 100%; width: 100%;"> | ||||||
|   <!-- Drilldown mode indicator --> |   <!-- Filter Controls Section --> | ||||||
|   <div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> |   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||||
|     <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> |     <!-- Base Filters --> | ||||||
|     <button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> |     <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0"> | ||||||
|       Back to Level {{currentDrilldownLevel - 1}} |       <h4>Base Filters</h4> | ||||||
|     </button> |       <div class="filter-controls"> | ||||||
|     <button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> |         <div *ngFor="let filter of baseFilters" class="filter-item"> | ||||||
|       Back to Main View |           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||||
|     </button> |            | ||||||
|  |           <!-- 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> |   </div> | ||||||
|    |    | ||||||
|   <!-- No data message --> |   <!-- No data message --> | ||||||
| @ -16,7 +285,7 @@ | |||||||
|   </div> |   </div> | ||||||
|    |    | ||||||
|   <!-- Chart display --> |   <!-- Chart display --> | ||||||
|   <div *ngIf="!noDataAvailable"> |   <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);"> | ||||||
|     <canvas baseChart |     <canvas baseChart | ||||||
|       [datasets]="scatterChartData" |       [datasets]="scatterChartData" | ||||||
|       [type]="scatterChartType" |       [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 { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||||
| import { ChartData,ChartDataset } from 'chart.js'; | import { ChartData,ChartDataset } from 'chart.js'; | ||||||
| import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; | ||||||
|  | import { FilterService } from '../../common-filter/filter.service'; | ||||||
|  | import { Subscription } from 'rxjs'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-scatter-chart', |   selector: 'app-scatter-chart', | ||||||
| @ -34,9 +36,20 @@ export class ScatterChartComponent implements OnInit, OnChanges { | |||||||
|   // Multi-layer drilldown configuration inputs
 |   // Multi-layer drilldown configuration inputs
 | ||||||
|   @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
 |   @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
 | ||||||
| 
 | 
 | ||||||
|   constructor(private dashboardService: Dashboard3Service) { } |   constructor( | ||||||
|  |     private dashboardService: Dashboard3Service, | ||||||
|  |     private filterService: FilterService | ||||||
|  |   ) { } | ||||||
| 
 | 
 | ||||||
|   ngOnInit(): void { |   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
 |     // Initialize with default data
 | ||||||
|     this.fetchChartData(); |     this.fetchChartData(); | ||||||
|   } |   } | ||||||
| @ -44,6 +57,12 @@ export class ScatterChartComponent implements OnInit, OnChanges { | |||||||
|   ngOnChanges(changes: SimpleChanges): void { |   ngOnChanges(changes: SimpleChanges): void { | ||||||
|     console.log('ScatterChartComponent input changes:', changes); |     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
 |     // Check if any of the key properties have changed
 | ||||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; |     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; |     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||||
| @ -107,6 +126,367 @@ export class ScatterChartComponent implements OnInit, OnChanges { | |||||||
|   // Flag to prevent infinite loops
 |   // Flag to prevent infinite loops
 | ||||||
|   private isFetchingData: boolean = false; |   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 { |   fetchChartData(): void { | ||||||
|     // Set flag to prevent recursive calls
 |     // Set flag to prevent recursive calls
 | ||||||
|     this.isFetchingData = true; |     this.isFetchingData = true; | ||||||
| @ -139,7 +519,49 @@ export class ScatterChartComponent implements OnInit, OnChanges { | |||||||
|           filterParams = JSON.stringify(filterObj); |           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
 |       // 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}` : ''}`; |       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
 |     // Add common filters to drilldown filter parameters
 | ||||||
|     let drilldownFilterParams = ''; |     const commonFilters = this.filterService.getFilterValues(); | ||||||
|     if (this.drilldownFilters && this.drilldownFilters.length > 0) { |     if (Object.keys(commonFilters).length > 0) { | ||||||
|       const filterObj = {}; |       // Merge common filters with drilldown filters
 | ||||||
|       this.drilldownFilters.forEach(filter => { |       const mergedFilterObj = {}; | ||||||
|         if (filter.field && filter.value) { |        | ||||||
|           filterObj[filter.field] = filter.value; |       // 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); |       if (Object.keys(mergedFilterObj).length > 0) { | ||||||
|  |         filterParams = JSON.stringify(mergedFilterObj); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     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); |  | ||||||
|      |      | ||||||
|     // Log the URL that will be called
 |     // 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}` : ''}`; |     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
 |     // Fetch data from the dashboard service with parameter field and value
 | ||||||
|     // Backend handles filtering, we just pass the 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) => { |       (data: any) => { | ||||||
|         console.log('Received drilldown data:', data); |         console.log('Received drilldown data:', data); | ||||||
|         if (data === null) { |         if (data === null) { | ||||||
| @ -325,7 +757,6 @@ export class ScatterChartComponent implements OnInit, OnChanges { | |||||||
|         // Handle the actual data structure returned by the API
 |         // Handle the actual data structure returned by the API
 | ||||||
|         if (data && data.chartLabels && data.chartData) { |         if (data && data.chartLabels && data.chartData) { | ||||||
|           // For scatter charts, we need to transform the data into scatter format
 |           // 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.noDataAvailable = data.chartLabels.length === 0; | ||||||
|           this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData); |           this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData); | ||||||
|           console.log('Updated scatter chart with drilldown data:', this.scatterChartData); |           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)
 |   // Reset to original data (go back to base level)
 | ||||||
|   resetToOriginalData(): void { |   resetToOriginalData(): void { | ||||||
|     console.log('Resetting to original data'); |     console.log('Resetting to original data'); | ||||||
| @ -436,16 +840,18 @@ export class ScatterChartComponent implements OnInit, OnChanges { | |||||||
|       // Get the index of the clicked element
 |       // Get the index of the clicked element
 | ||||||
|       const clickedIndex = e.active[0].index; |       const clickedIndex = e.active[0].index; | ||||||
|        |        | ||||||
|       // Get the label of the clicked element
 |       // Get the dataset index
 | ||||||
|       // For scatter charts, we might not have labels in the same way as other charts
 |       const datasetIndex = e.active[0].datasetIndex; | ||||||
|       const clickedLabel = `Point ${clickedIndex}`; |  | ||||||
|        |        | ||||||
|       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 we're not at the base level, store original data
 | ||||||
|       if (this.currentDrilldownLevel === 0) { |       if (this.currentDrilldownLevel === 0) { | ||||||
|         // Store original data before entering drilldown mode
 |         // 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'); |         console.log('Stored original data for drilldown'); | ||||||
|       } |       } | ||||||
|        |        | ||||||
| @ -487,9 +893,10 @@ export class ScatterChartComponent implements OnInit, OnChanges { | |||||||
|         // Add this click to the drilldown stack
 |         // Add this click to the drilldown stack
 | ||||||
|         const stackEntry = { |         const stackEntry = { | ||||||
|           level: nextDrilldownLevel, |           level: nextDrilldownLevel, | ||||||
|  |           datasetIndex: datasetIndex, | ||||||
|           clickedIndex: clickedIndex, |           clickedIndex: clickedIndex, | ||||||
|           clickedLabel: clickedLabel, |           dataPoint: dataPoint, | ||||||
|           clickedValue: clickedLabel // Using label as value for now
 |           clickedValue: dataPoint // Using data point as value for now
 | ||||||
|         }; |         }; | ||||||
|          |          | ||||||
|         this.drilldownStack.push(stackEntry); |         this.drilldownStack.push(stackEntry); | ||||||
| @ -513,6 +920,12 @@ export class ScatterChartComponent implements OnInit, OnChanges { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public chartHovered(e: any): void { |   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,27 +1,310 @@ | |||||||
| <table class="table"> | <div class="to-do-chart-container"> | ||||||
|   <thead> |   <!-- Filter Controls Section --> | ||||||
|   <th class="c-col">#</th> |   <div class="filter-section" *ngIf="hasActiveFilters()"> | ||||||
|   <th>Item</th> |     <!-- Base Filters --> | ||||||
|   <th></th> |     <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0"> | ||||||
|   </thead> |       <h4>Base Filters</h4> | ||||||
|   <tr class="ui basic segment" *ngFor="let todo of todoList; let i = index"> |       <div class="filter-controls"> | ||||||
|       <td class="c-col">{{i + 1}}</td> |         <div *ngFor="let filter of baseFilters" class="filter-item"> | ||||||
|       <td>{{todo}}</td> |           <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div> | ||||||
|       <td style="text-align:right"> |            | ||||||
|           <a routerLink="." (click)="removeTodo(i)"> |           <!-- Text Filter --> | ||||||
|               <clr-icon shape="times"></clr-icon> |           <div *ngIf="!filter.type || filter.type === 'text'" class="filter-input"> | ||||||
|           </a> |             <input type="text"  | ||||||
|       </td> |                    [(ngModel)]="filter.value"  | ||||||
|   </tr> |                    (ngModelChange)="onBaseFilterChange(filter)" | ||||||
|   <tr> |                    [placeholder]="filter.field" | ||||||
|       <td></td> |                    class="clr-input filter-text-input"> | ||||||
|       <td> |           </div> | ||||||
|         <input [(ngModel)]="todo" placeholder="Add Todo" class="clr-input"> |            | ||||||
|       </td> |           <!-- Dropdown Filter --> | ||||||
|       <td style="text-align:right"> |           <div *ngIf="filter.type === 'dropdown'" class="filter-input"> | ||||||
|           <a  routerLink="." color='primary' (click)="addTodo(todo)"> |             <select [(ngModel)]="filter.value"  | ||||||
|               <clr-icon shape="plus"></clr-icon> |                     (ngModelChange)="onBaseFilterChange(filter)" | ||||||
|           </a> |                     class="clr-select filter-select"> | ||||||
|       </td> |               <option value="">Select {{ filter.field }}</option> | ||||||
|   </tr> |               <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option> | ||||||
| </table> |             </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> | ||||||
|  |     <th></th> | ||||||
|  |     </thead> | ||||||
|  |     <tr class="ui basic segment" *ngFor="let todo of todoList; let i = index"> | ||||||
|  |         <td class="c-col">{{i + 1}}</td> | ||||||
|  |         <td>{{todo}}</td> | ||||||
|  |         <td style="text-align:right"> | ||||||
|  |             <a routerLink="." (click)="removeTodo(i)"> | ||||||
|  |                 <clr-icon shape="times"></clr-icon> | ||||||
|  |             </a> | ||||||
|  |         </td> | ||||||
|  |     </tr> | ||||||
|  |     <tr> | ||||||
|  |         <td></td> | ||||||
|  |         <td> | ||||||
|  |           <input [(ngModel)]="todo" placeholder="Add Todo" class="clr-input todo-input"> | ||||||
|  |         </td> | ||||||
|  |         <td style="text-align:right"> | ||||||
|  |             <a  routerLink="." color='primary' (click)="addTodo(todo)"> | ||||||
|  |                 <clr-icon shape="plus"></clr-icon> | ||||||
|  |             </a> | ||||||
|  |         </td> | ||||||
|  |     </tr> | ||||||
|  |   </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 { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||||
|  | import { FilterService } from '../../common-filter/filter.service'; | ||||||
|  | import { Subscription } from 'rxjs'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-to-do-chart', |   selector: 'app-to-do-chart', | ||||||
| @ -21,15 +23,43 @@ export class ToDoChartComponent implements OnInit, OnChanges { | |||||||
|   @Input() datasource: string; |   @Input() datasource: string; | ||||||
|   @Input() fieldName: string; |   @Input() fieldName: string; | ||||||
|   @Input() connection: number; // Add connection input
 |   @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
 | ||||||
|  |    | ||||||
|  |   // 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() { } |   constructor(private filterService: FilterService) { } | ||||||
| 
 |    | ||||||
|   ngOnInit(): void { |   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 { |   ngOnChanges(changes: SimpleChanges): void { | ||||||
|     console.log('ToDoChartComponent input changes:', changes); |     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
 |     // Check if any of the key properties have changed
 | ||||||
|     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; |     const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; | ||||||
|     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; |     const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; | ||||||
| @ -48,6 +78,14 @@ export class ToDoChartComponent implements OnInit, OnChanges { | |||||||
|   todo: string; |   todo: string; | ||||||
|   todoList = ['todo 1']; |   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 { |   fetchToDoData(): void { | ||||||
|     // If we have the necessary data, fetch to-do data from the service
 |     // If we have the necessary data, fetch to-do data from the service
 | ||||||
|     if (this.table) { |     if (this.table) { | ||||||
| @ -73,4 +111,322 @@ export class ToDoChartComponent implements OnInit, OnChanges { | |||||||
|         this.todoList.splice(todoIx, 1); |         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