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