unified
This commit is contained in:
		
							parent
							
								
									7396843bc6
								
							
						
					
					
						commit
						ffda17e6b1
					
				| @ -0,0 +1 @@ | ||||
| export * from './unified-chart.component'; | ||||
| @ -0,0 +1,365 @@ | ||||
| <div class="chart-container" *ngIf="!noDataAvailable && !isLoading"> | ||||
|   <!-- Back button for drilldown navigation --> | ||||
|   <div class="drilldown-back" *ngIf="currentDrilldownLevel > 0"> | ||||
|     <button class="btn btn-sm btn-secondary" (click)="navigateBack()"> | ||||
|       <clr-icon shape="arrow" dir="left"></clr-icon> | ||||
|       Back | ||||
|     </button> | ||||
|     <span class="drilldown-level">Level: {{ currentDrilldownLevel }}</span> | ||||
|   </div> | ||||
| 
 | ||||
|   <!-- Chart title --> | ||||
|   <div class="chart-title" *ngIf="charttitle"> | ||||
|     <h4>{{ charttitle }}</h4> | ||||
|   </div> | ||||
| 
 | ||||
|   <!-- Render different chart types based on chartType input --> | ||||
|   <div class="chart-wrapper"> | ||||
|     <!-- Bar Chart --> | ||||
|     <div *ngIf="chartType === 'bar'"> | ||||
|       <canvas baseChart | ||||
|         [data]="chartData" | ||||
|         [labels]="chartLabels" | ||||
|         [options]="chartOptions" | ||||
|         [legend]="chartLegend" | ||||
|         [chartType]="'bar'" | ||||
|         (chartClick)="chartClicked($event)" | ||||
|         (chartHover)="chartHovered($event)"> | ||||
|       </canvas> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Line Chart --> | ||||
|     <div *ngIf="chartType === 'line'"> | ||||
|       <canvas baseChart | ||||
|         [data]="chartData" | ||||
|         [labels]="chartLabels" | ||||
|         [options]="chartOptions" | ||||
|         [legend]="chartLegend" | ||||
|         [chartType]="'line'" | ||||
|         (chartClick)="chartClicked($event)" | ||||
|         (chartHover)="chartHovered($event)"> | ||||
|       </canvas> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Pie Chart --> | ||||
|     <div *ngIf="chartType === 'pie'"> | ||||
|       <canvas baseChart | ||||
|         [data]="chartData" | ||||
|         [labels]="chartLabels" | ||||
|         [options]="chartOptions" | ||||
|         [legend]="chartLegend" | ||||
|         [chartType]="'pie'" | ||||
|         (chartClick)="chartClicked($event)" | ||||
|         (chartHover)="chartHovered($event)"> | ||||
|       </canvas> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Doughnut Chart --> | ||||
|     <div *ngIf="chartType === 'doughnut'"> | ||||
|       <canvas baseChart | ||||
|         [data]="chartData" | ||||
|         [labels]="chartLabels" | ||||
|         [options]="chartOptions" | ||||
|         [legend]="chartLegend" | ||||
|         [chartType]="'doughnut'" | ||||
|         (chartClick)="chartClicked($event)" | ||||
|         (chartHover)="chartHovered($event)"> | ||||
|       </canvas> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Bubble Chart --> | ||||
|     <div *ngIf="chartType === 'bubble'"> | ||||
|       <canvas baseChart | ||||
|         [datasets]="bubbleChartData" | ||||
|         [options]="chartOptions" | ||||
|         [legend]="chartLegend" | ||||
|         [chartType]="'bubble'" | ||||
|         (chartClick)="chartClicked($event)" | ||||
|         (chartHover)="chartHovered($event)"> | ||||
|       </canvas> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Radar Chart --> | ||||
|     <div *ngIf="chartType === 'radar'"> | ||||
|       <canvas baseChart | ||||
|         [data]="chartData" | ||||
|         [labels]="chartLabels" | ||||
|         [options]="chartOptions" | ||||
|         [legend]="chartLegend" | ||||
|         [chartType]="'radar'" | ||||
|         (chartClick)="chartClicked($event)" | ||||
|         (chartHover)="chartHovered($event)"> | ||||
|       </canvas> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Polar Area Chart --> | ||||
|     <div *ngIf="chartType === 'polar'"> | ||||
|       <canvas baseChart | ||||
|         [data]="chartData" | ||||
|         [labels]="chartLabels" | ||||
|         [options]="chartOptions" | ||||
|         [legend]="chartLegend" | ||||
|         [chartType]="'polarArea'" | ||||
|         (chartClick)="chartClicked($event)" | ||||
|         (chartHover)="chartHovered($event)"> | ||||
|       </canvas> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Scatter Chart --> | ||||
|     <div *ngIf="chartType === 'scatter'"> | ||||
|       <canvas baseChart | ||||
|         [datasets]="chartData" | ||||
|         [options]="chartOptions" | ||||
|         [legend]="chartLegend" | ||||
|         [chartType]="'scatter'" | ||||
|         (chartClick)="chartClicked($event)" | ||||
|         (chartHover)="chartHovered($event)"> | ||||
|       </canvas> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Default/Unknown Chart Type --> | ||||
|     <div *ngIf="!['bar', 'line', 'pie', 'doughnut', 'bubble', 'radar', 'polar', 'scatter'].includes(chartType)"> | ||||
|       <canvas baseChart | ||||
|         [data]="chartData" | ||||
|         [labels]="chartLabels" | ||||
|         [options]="chartOptions" | ||||
|         [legend]="chartLegend" | ||||
|         [chartType]="'bar'" | ||||
|         (chartClick)="chartClicked($event)" | ||||
|         (chartHover)="chartHovered($event)"> | ||||
|       </canvas> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <!-- Base Filters --> | ||||
|   <div class="filters-section" *ngIf="baseFilters && baseFilters.length > 0"> | ||||
|     <h5>Filters</h5> | ||||
|     <div class="filters-container"> | ||||
|       <div class="filter-item" *ngFor="let filter of baseFilters; let i = index"> | ||||
|         <!-- Text Filter --> | ||||
|         <div *ngIf="filter.type === 'text'" class="filter-text"> | ||||
|           <label>{{ filter.field }}</label> | ||||
|           <input type="text" [(ngModel)]="filter.value" (ngModelChange)="onBaseFilterChange(filter)"  | ||||
|                  class="form-control" placeholder="Enter {{ filter.field }}"> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Dropdown Filter --> | ||||
|         <div *ngIf="filter.type === 'dropdown'" class="filter-dropdown"> | ||||
|           <label>{{ filter.field }}</label> | ||||
|           <select [(ngModel)]="filter.value" (ngModelChange)="onBaseFilterChange(filter)" class="form-control"> | ||||
|             <option value="">Select {{ filter.field }}</option> | ||||
|             <option *ngFor="let option of getFilterOptions(filter)" [value]="option"> | ||||
|               {{ option }} | ||||
|             </option> | ||||
|           </select> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Multiselect Filter --> | ||||
|         <div *ngIf="filter.type === 'multiselect'" class="filter-multiselect"> | ||||
|           <label>{{ filter.field }}</label> | ||||
|           <div class="multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')"> | ||||
|               <span *ngIf="getSelectedOptionsCount(filter) === 0">Select {{ filter.field }}</span> | ||||
|               <span *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 {{ getSelectedOptionsCount(filter) }} selected | ||||
|               </span> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')"> | ||||
|               <div class="multiselect-option" *ngFor="let option of getFilterOptions(filter)"> | ||||
|                 <input type="checkbox"  | ||||
|                        [checked]="isOptionSelected(filter, option)" | ||||
|                        (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                        id="base-{{ filter.field }}-{{ option }}"> | ||||
|                 <label [for]="'base-' + filter.field + '-' + option">{{ option }}</label> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Date Range Filter --> | ||||
|         <div *ngIf="filter.type === 'date-range'" class="filter-date-range"> | ||||
|           <label>{{ filter.field }}</label> | ||||
|           <div class="date-range-inputs"> | ||||
|             <input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeChange(filter, filter.value)"  | ||||
|                    class="form-control" placeholder="Start Date"> | ||||
|             <input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeChange(filter, filter.value)"  | ||||
|                    class="form-control" placeholder="End Date"> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Toggle Filter --> | ||||
|         <div *ngIf="filter.type === 'toggle'" class="filter-toggle"> | ||||
|           <label>{{ filter.field }}</label> | ||||
|           <div class="toggle-switch"> | ||||
|             <input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)"  | ||||
|                    id="toggle-{{ filter.field }}"> | ||||
|             <label [for]="'toggle-' + filter.field" class="toggle-label"> | ||||
|               <span class="toggle-slider"></span> | ||||
|             </label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <!-- Drilldown Filters --> | ||||
|   <div class="filters-section" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0"> | ||||
|     <h5>Drilldown Filters</h5> | ||||
|     <div class="filters-container"> | ||||
|       <div class="filter-item" *ngFor="let filter of drilldownFilters; let i = index"> | ||||
|         <!-- Text Filter --> | ||||
|         <div *ngIf="filter.type === 'text'" class="filter-text"> | ||||
|           <label>{{ filter.field }}</label> | ||||
|           <input type="text" [(ngModel)]="filter.value" (ngModelChange)="onDrilldownFilterChange(filter)"  | ||||
|                  class="form-control" placeholder="Enter {{ filter.field }}"> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Dropdown Filter --> | ||||
|         <div *ngIf="filter.type === 'dropdown'" class="filter-dropdown"> | ||||
|           <label>{{ filter.field }}</label> | ||||
|           <select [(ngModel)]="filter.value" (ngModelChange)="onDrilldownFilterChange(filter)" class="form-control"> | ||||
|             <option value="">Select {{ filter.field }}</option> | ||||
|             <option *ngFor="let option of getFilterOptions(filter)" [value]="option"> | ||||
|               {{ option }} | ||||
|             </option> | ||||
|           </select> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Multiselect Filter --> | ||||
|         <div *ngIf="filter.type === 'multiselect'" class="filter-multiselect"> | ||||
|           <label>{{ filter.field }}</label> | ||||
|           <div class="multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')"> | ||||
|               <span *ngIf="getSelectedOptionsCount(filter) === 0">Select {{ filter.field }}</span> | ||||
|               <span *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 {{ getSelectedOptionsCount(filter) }} selected | ||||
|               </span> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')"> | ||||
|               <div class="multiselect-option" *ngFor="let option of getFilterOptions(filter)"> | ||||
|                 <input type="checkbox"  | ||||
|                        [checked]="isOptionSelected(filter, option)" | ||||
|                        (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                        id="drilldown-{{ filter.field }}-{{ option }}"> | ||||
|                 <label [for]="'drilldown-' + filter.field + '-' + option">{{ option }}</label> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Date Range Filter --> | ||||
|         <div *ngIf="filter.type === 'date-range'" class="filter-date-range"> | ||||
|           <label>{{ filter.field }}</label> | ||||
|           <div class="date-range-inputs"> | ||||
|             <input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeChange(filter, filter.value)"  | ||||
|                    class="form-control" placeholder="Start Date"> | ||||
|             <input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeChange(filter, filter.value)"  | ||||
|                    class="form-control" placeholder="End Date"> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Toggle Filter --> | ||||
|         <div *ngIf="filter.type === 'toggle'" class="filter-toggle"> | ||||
|           <label>{{ filter.field }}</label> | ||||
|           <div class="toggle-switch"> | ||||
|             <input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)"  | ||||
|                    id="drilldown-toggle-{{ filter.field }}"> | ||||
|             <label [for]="'drilldown-toggle-' + filter.field" class="toggle-label"> | ||||
|               <span class="toggle-slider"></span> | ||||
|             </label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <!-- Layer Filters --> | ||||
|   <div class="filters-section" *ngIf="hasActiveLayerFilters()"> | ||||
|     <h5>Layer Filters</h5> | ||||
|     <div class="filters-container"> | ||||
|       <div class="filter-item" *ngFor="let filter of getActiveLayerFilters(); let i = index"> | ||||
|         <!-- Text Filter --> | ||||
|         <div *ngIf="filter.type === 'text'" class="filter-text"> | ||||
|           <label>{{ filter.field }}</label> | ||||
|           <input type="text" [(ngModel)]="filter.value" (ngModelChange)="onLayerFilterChange(filter)"  | ||||
|                  class="form-control" placeholder="Enter {{ filter.field }}"> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Dropdown Filter --> | ||||
|         <div *ngIf="filter.type === 'dropdown'" class="filter-dropdown"> | ||||
|           <label>{{ filter.field }}</label> | ||||
|           <select [(ngModel)]="filter.value" (ngModelChange)="onLayerFilterChange(filter)" class="form-control"> | ||||
|             <option value="">Select {{ filter.field }}</option> | ||||
|             <option *ngFor="let option of getFilterOptions(filter)" [value]="option"> | ||||
|               {{ option }} | ||||
|             </option> | ||||
|           </select> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Multiselect Filter --> | ||||
|         <div *ngIf="filter.type === 'multiselect'" class="filter-multiselect"> | ||||
|           <label>{{ filter.field }}</label> | ||||
|           <div class="multiselect-container"> | ||||
|             <div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')"> | ||||
|               <span *ngIf="getSelectedOptionsCount(filter) === 0">Select {{ filter.field }}</span> | ||||
|               <span *ngIf="getSelectedOptionsCount(filter) > 0"> | ||||
|                 {{ getSelectedOptionsCount(filter) }} selected | ||||
|               </span> | ||||
|             </div> | ||||
|             <div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')"> | ||||
|               <div class="multiselect-option" *ngFor="let option of getFilterOptions(filter)"> | ||||
|                 <input type="checkbox"  | ||||
|                        [checked]="isOptionSelected(filter, option)" | ||||
|                        (change)="onMultiSelectChange(filter, option, $event)" | ||||
|                        id="layer-{{ filter.field }}-{{ option }}"> | ||||
|                 <label [for]="'layer-' + filter.field + '-' + option">{{ option }}</label> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Date Range Filter --> | ||||
|         <div *ngIf="filter.type === 'date-range'" class="filter-date-range"> | ||||
|           <label>{{ filter.field }}</label> | ||||
|           <div class="date-range-inputs"> | ||||
|             <input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeChange(filter, filter.value)"  | ||||
|                    class="form-control" placeholder="Start Date"> | ||||
|             <input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeChange(filter, filter.value)"  | ||||
|                    class="form-control" placeholder="End Date"> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Toggle Filter --> | ||||
|         <div *ngIf="filter.type === 'toggle'" class="filter-toggle"> | ||||
|           <label>{{ filter.field }}</label> | ||||
|           <div class="toggle-switch"> | ||||
|             <input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)"  | ||||
|                    id="layer-toggle-{{ filter.field }}"> | ||||
|             <label [for]="'layer-toggle-' + filter.field" class="toggle-label"> | ||||
|               <span class="toggle-slider"></span> | ||||
|             </label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <!-- Clear Filters Button --> | ||||
|   <div class="clear-filters" *ngIf="hasActiveFilters()"> | ||||
|     <button class="btn btn-sm btn-outline" (click)="clearAllFilters()"> | ||||
|       Clear All Filters | ||||
|     </button> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <!-- No Data Available Message --> | ||||
| <div class="no-data-message" *ngIf="noDataAvailable && !isLoading"> | ||||
|   <p>No data available for the selected filters.</p> | ||||
|   <button class="btn btn-sm btn-primary" (click)="fetchChartData()">Retry</button> | ||||
| </div> | ||||
| 
 | ||||
| <!-- Loading Indicator --> | ||||
| <div class="loading-indicator" *ngIf="isLoading"> | ||||
|   <div class="spinner"></div> | ||||
|   <p>Loading chart data...</p> | ||||
| </div> | ||||
| @ -0,0 +1,262 @@ | ||||
| .chart-container { | ||||
|   position: relative; | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
|   padding: 10px; | ||||
| } | ||||
| 
 | ||||
| .drilldown-back { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   margin-bottom: 10px; | ||||
|    | ||||
|   .drilldown-level { | ||||
|     margin-left: 10px; | ||||
|     font-size: 14px; | ||||
|     color: #666; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .chart-title { | ||||
|   text-align: center; | ||||
|   margin-bottom: 15px; | ||||
|    | ||||
|   h4 { | ||||
|     margin: 0; | ||||
|     color: #333; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .chart-wrapper { | ||||
|   position: relative; | ||||
|   height: calc(100% - 100px); | ||||
|   min-height: 300px; | ||||
| } | ||||
| 
 | ||||
| .filters-section { | ||||
|   margin-top: 20px; | ||||
|   padding: 15px; | ||||
|   border: 1px solid #e0e0e0; | ||||
|   border-radius: 4px; | ||||
|   background-color: #f9f9f9; | ||||
|    | ||||
|   h5 { | ||||
|     margin-top: 0; | ||||
|     margin-bottom: 10px; | ||||
|     color: #333; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .filters-container { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   gap: 15px; | ||||
| } | ||||
| 
 | ||||
| .filter-item { | ||||
|   flex: 1 1 200px; | ||||
|   min-width: 150px; | ||||
|    | ||||
|   label { | ||||
|     display: block; | ||||
|     margin-bottom: 5px; | ||||
|     font-weight: 500; | ||||
|     color: #555; | ||||
|   } | ||||
|    | ||||
|   .form-control { | ||||
|     width: 100%; | ||||
|     padding: 8px; | ||||
|     border: 1px solid #ccc; | ||||
|     border-radius: 4px; | ||||
|     font-size: 14px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .filter-text, | ||||
| .filter-dropdown, | ||||
| .filter-date-range { | ||||
|   .form-control { | ||||
|     height: 36px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .date-range-inputs { | ||||
|   display: flex; | ||||
|   gap: 10px; | ||||
|    | ||||
|   .form-control { | ||||
|     flex: 1; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .filter-multiselect { | ||||
|   position: relative; | ||||
|    | ||||
|   .multiselect-container { | ||||
|     position: relative; | ||||
|   } | ||||
|    | ||||
|   .multiselect-display { | ||||
|     width: 100%; | ||||
|     padding: 8px; | ||||
|     border: 1px solid #ccc; | ||||
|     border-radius: 4px; | ||||
|     background-color: white; | ||||
|     cursor: pointer; | ||||
|     min-height: 36px; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|   } | ||||
|    | ||||
|   .multiselect-dropdown { | ||||
|     position: absolute; | ||||
|     top: 100%; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|     background-color: white; | ||||
|     border: 1px solid #ccc; | ||||
|     border-radius: 4px; | ||||
|     box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); | ||||
|     z-index: 1000; | ||||
|     max-height: 200px; | ||||
|     overflow-y: auto; | ||||
|   } | ||||
|    | ||||
|   .multiselect-option { | ||||
|     padding: 8px 12px; | ||||
|     border-bottom: 1px solid #eee; | ||||
|      | ||||
|     &:last-child { | ||||
|       border-bottom: none; | ||||
|     } | ||||
|      | ||||
|     &:hover { | ||||
|       background-color: #f5f5f5; | ||||
|     } | ||||
|      | ||||
|     input[type="checkbox"] { | ||||
|       margin-right: 8px; | ||||
|     } | ||||
|      | ||||
|     label { | ||||
|       margin: 0; | ||||
|       font-weight: normal; | ||||
|       cursor: pointer; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .filter-toggle { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: 10px; | ||||
|    | ||||
|   .toggle-switch { | ||||
|     position: relative; | ||||
|     display: inline-block; | ||||
|     width: 50px; | ||||
|     height: 24px; | ||||
|   } | ||||
|    | ||||
|   .toggle-switch input { | ||||
|     opacity: 0; | ||||
|     width: 0; | ||||
|     height: 0; | ||||
|   } | ||||
|    | ||||
|   .toggle-label { | ||||
|     position: absolute; | ||||
|     cursor: pointer; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|     bottom: 0; | ||||
|     background-color: #ccc; | ||||
|     transition: .4s; | ||||
|     border-radius: 24px; | ||||
|   } | ||||
|    | ||||
|   .toggle-slider { | ||||
|     position: absolute; | ||||
|     content: ""; | ||||
|     height: 16px; | ||||
|     width: 16px; | ||||
|     left: 4px; | ||||
|     bottom: 4px; | ||||
|     background-color: white; | ||||
|     transition: .4s; | ||||
|     border-radius: 50%; | ||||
|   } | ||||
|    | ||||
|   input:checked + .toggle-label { | ||||
|     background-color: #2196F3; | ||||
|   } | ||||
|    | ||||
|   input:checked + .toggle-label .toggle-slider { | ||||
|     transform: translateX(26px); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .clear-filters { | ||||
|   margin-top: 15px; | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| .no-data-message { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   height: 300px; | ||||
|   text-align: center; | ||||
|   color: #666; | ||||
|    | ||||
|   p { | ||||
|     margin-bottom: 15px; | ||||
|     font-size: 16px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .loading-indicator { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   height: 300px; | ||||
|    | ||||
|   .spinner { | ||||
|     width: 40px; | ||||
|     height: 40px; | ||||
|     border: 4px solid #f3f3f3; | ||||
|     border-top: 4px solid #3498db; | ||||
|     border-radius: 50%; | ||||
|     animation: spin 1s linear infinite; | ||||
|     margin-bottom: 15px; | ||||
|   } | ||||
|    | ||||
|   p { | ||||
|     margin: 0; | ||||
|     color: #666; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes spin { | ||||
|   0% { transform: rotate(0deg); } | ||||
|   100% { transform: rotate(360deg); } | ||||
| } | ||||
| 
 | ||||
| // Responsive adjustments | ||||
| @media (max-width: 768px) { | ||||
|   .filters-container { | ||||
|     flex-direction: column; | ||||
|   } | ||||
|    | ||||
|   .filter-item { | ||||
|     min-width: 100%; | ||||
|   } | ||||
|    | ||||
|   .chart-wrapper { | ||||
|     min-height: 250px; | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,21 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
| 
 | ||||
| import { UnifiedChartComponent } from './unified-chart.component'; | ||||
| 
 | ||||
| describe('UnifiedChartComponent', () => { | ||||
|   let component: UnifiedChartComponent; | ||||
|   let fixture: ComponentFixture<UnifiedChartComponent>; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     TestBed.configureTestingModule({ | ||||
|       declarations: [UnifiedChartComponent] | ||||
|     }); | ||||
|     fixture = TestBed.createComponent(UnifiedChartComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user