chart
This commit is contained in:
@@ -1,14 +1,283 @@
|
||||
<div style="display:block">
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
<div style="display:block; height: 100%; width: 100%;">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Bubble Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data message -->
|
||||
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
||||
@@ -16,7 +285,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Chart display -->
|
||||
<div *ngIf="!noDataAvailable">
|
||||
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
|
||||
<canvas baseChart
|
||||
[datasets]="bubbleChartData"
|
||||
[type]="bubbleChartType"
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { ChartConfiguration, ChartDataset, ChartOptions } from 'chart.js';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bubble-chart',
|
||||
@@ -96,15 +98,40 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('BubbleChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -128,6 +155,349 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Transform data to bubble chart format
|
||||
private transformToBubbleData(labels: any[], data: any[]): ChartDataset[] {
|
||||
// For bubble charts, we need to transform the data into bubble format
|
||||
// Bubble charts expect data in the format: {x: number, y: number, r: number}
|
||||
console.log('Transforming data to bubble format:', { labels, data });
|
||||
|
||||
// If we have the expected bubble data format, return it as is
|
||||
if (data && data.length > 0 && data[0].data && data[0].data.length > 0 &&
|
||||
typeof data[0].data[0] === 'object' && data[0].data[0].hasOwnProperty('x') &&
|
||||
data[0].data[0].hasOwnProperty('y') && data[0].data[0].hasOwnProperty('r')) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Otherwise, create a default bubble dataset
|
||||
const bubbleDatasets: ChartDataset[] = [
|
||||
{
|
||||
data: [
|
||||
{ x: 10, y: 10, r: 10 },
|
||||
{ x: 15, y: 5, r: 15 },
|
||||
{ x: 26, y: 12, r: 23 },
|
||||
{ x: 7, y: 8, r: 8 },
|
||||
],
|
||||
label: 'Dataset 1',
|
||||
backgroundColor: 'rgba(255, 0, 0, 0.6)',
|
||||
borderColor: 'blue',
|
||||
hoverBackgroundColor: 'purple',
|
||||
hoverBorderColor: 'red',
|
||||
}
|
||||
];
|
||||
|
||||
return bubbleDatasets;
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -160,7 +530,49 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/bubble?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -206,6 +618,7 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
||||
this.bubbleChartData = [];
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
// Keep default data in case of error
|
||||
}
|
||||
);
|
||||
} else {
|
||||
@@ -307,6 +720,35 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/bubble?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Drilldown data URL:', url);
|
||||
@@ -326,7 +768,6 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For bubble charts, we need to transform the data into bubble format
|
||||
// Bubble charts expect data in the format: {x: number, y: number, r: number}
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.bubbleChartData = this.transformToBubbleData(data.chartLabels, data.chartData);
|
||||
console.log('Updated bubble chart with drilldown data:', this.bubbleChartData);
|
||||
@@ -345,39 +786,11 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
||||
console.error('Error fetching drilldown data:', error);
|
||||
this.noDataAvailable = true;
|
||||
this.bubbleChartData = [];
|
||||
// Keep current data in case of error
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Transform chart data to bubble chart format
|
||||
private transformToBubbleData(labels: string[], datasets: any[]): ChartDataset[] {
|
||||
// For bubble charts, we need to transform the data into bubble format
|
||||
// Bubble charts expect data in the format: {x: number, y: number, r: number}
|
||||
|
||||
// This is a simple transformation - in a real implementation, you might want to
|
||||
// create a more sophisticated mapping based on your data structure
|
||||
return datasets.map((dataset, index) => {
|
||||
// Create bubble data points
|
||||
const bubbleData = labels.map((label, i) => {
|
||||
// Use x-axis data as x coordinate, y-axis data as y coordinate, and a fixed radius
|
||||
const xValue = dataset.data[i] || 0;
|
||||
const yValue = i < datasets.length ? (datasets[i].data[index] || 0) : 0;
|
||||
const radius = 10; // Fixed radius for now
|
||||
|
||||
return { x: xValue, y: yValue, r: radius };
|
||||
});
|
||||
|
||||
return {
|
||||
data: bubbleData,
|
||||
label: dataset.label || `Dataset ${index + 1}`,
|
||||
backgroundColor: dataset.backgroundColor || `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.6)`,
|
||||
borderColor: dataset.borderColor || 'rgba(0, 0, 0, 1)',
|
||||
hoverBackgroundColor: dataset.hoverBackgroundColor || 'rgba(255, 255, 255, 0.8)',
|
||||
hoverBorderColor: dataset.hoverBorderColor || 'rgba(0, 0, 0, 1)'
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Reset to original data (go back to base level)
|
||||
resetToOriginalData(): void {
|
||||
console.log('Resetting to original data');
|
||||
@@ -438,16 +851,18 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
||||
// Get the index of the clicked element
|
||||
const clickedIndex = e.active[0].index;
|
||||
|
||||
// Get the label of the clicked element
|
||||
// For bubble charts, we might not have labels in the same way as other charts
|
||||
const clickedLabel = `Bubble ${clickedIndex}`;
|
||||
// Get the dataset index
|
||||
const datasetIndex = e.active[0].datasetIndex;
|
||||
|
||||
console.log('Clicked on bubble:', { index: clickedIndex, label: clickedLabel });
|
||||
// Get the data point
|
||||
const dataPoint = this.bubbleChartData[datasetIndex].data[clickedIndex];
|
||||
|
||||
console.log('Clicked on bubble:', { datasetIndex, clickedIndex, dataPoint });
|
||||
|
||||
// If we're not at the base level, store original data
|
||||
if (this.currentDrilldownLevel === 0) {
|
||||
// Store original data before entering drilldown mode
|
||||
this.originalBubbleChartData = [...this.bubbleChartData];
|
||||
this.originalBubbleChartData = JSON.parse(JSON.stringify(this.bubbleChartData));
|
||||
console.log('Stored original data for drilldown');
|
||||
}
|
||||
|
||||
@@ -489,9 +904,10 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
||||
// Add this click to the drilldown stack
|
||||
const stackEntry = {
|
||||
level: nextDrilldownLevel,
|
||||
datasetIndex: datasetIndex,
|
||||
clickedIndex: clickedIndex,
|
||||
clickedLabel: clickedLabel,
|
||||
clickedValue: clickedLabel // Using label as value for now
|
||||
dataPoint: dataPoint,
|
||||
clickedValue: dataPoint // Using data point as value for now
|
||||
};
|
||||
|
||||
this.drilldownStack.push(stackEntry);
|
||||
@@ -515,6 +931,6 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
console.log('Bubble chart hovered:', e);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,283 @@
|
||||
<div class="doughnut-chart-container">
|
||||
<!-- Compact Filters -->
|
||||
<div class="compact-filters-container" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<app-compact-filter
|
||||
*ngFor="let filter of baseFilters"
|
||||
[filterKey]="filter.field"
|
||||
(filterChange)="onFilterChange($event)">
|
||||
</app-compact-filter>
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator">
|
||||
<span class="drilldown-text">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button class="btn btn-secondary btn-sm" (click)="navigateBack()">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" (click)="resetToOriginalData()">
|
||||
Back to Main View
|
||||
</button>
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="chart-header">
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-wrapper">
|
||||
<div class="chart-content" [class.loading]="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable">
|
||||
|
||||
@@ -245,6 +245,186 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Filter section styles
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: left;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.doughnut-chart-container {
|
||||
@@ -287,4 +467,18 @@
|
||||
.compact-filters-container {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { Subscription } from 'rxjs';
|
||||
templateUrl: './doughnut-chart.component.html',
|
||||
styleUrls: ['./doughnut-chart.component.scss']
|
||||
})
|
||||
export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
|
||||
@Input() xAxis: string;
|
||||
@Input() yAxis: string | string[];
|
||||
@Input() table: string;
|
||||
@@ -102,6 +102,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
@@ -164,6 +169,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('DoughnutChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -198,12 +209,318 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
// Clean up document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
|
||||
// Handle filter changes from compact filters
|
||||
onFilterChange(event: { filterId: string, value: any }): void {
|
||||
console.log('Compact filter changed:', event);
|
||||
// The filter service will automatically trigger chart updates through the subscription
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
@@ -289,7 +606,7 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Doughnut chart data URL:', url);
|
||||
console.log('Chart data URL:', url);
|
||||
|
||||
// Fetch data from the dashboard service with parameter field and value
|
||||
// For base level, we pass empty parameter and value, but now also pass filters
|
||||
@@ -297,12 +614,10 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
(data: any) => {
|
||||
console.log('Received doughnut chart data:', data);
|
||||
if (data === null) {
|
||||
console.warn('Doughnut chart API returned null data. Check if the API endpoint is working correctly.');
|
||||
console.warn('API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
return;
|
||||
@@ -310,50 +625,26 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For doughnut charts, we need to extract the data differently
|
||||
// The first dataset's data array contains the values for the doughnut chart
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.doughnutChartLabels = data.chartLabels || [];
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
this.doughnutChartData = data.chartData[0].data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
} else {
|
||||
this.doughnutChartData = [];
|
||||
}
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
this.doughnutChartLabels = data.chartLabels;
|
||||
this.doughnutChartData = data.chartData;
|
||||
// Trigger change detection
|
||||
this.doughnutChartData = [...this.doughnutChartData];
|
||||
console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
||||
} else if (data && data.labels && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.doughnutChartLabels = data.labels || [];
|
||||
this.doughnutChartData = data.data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
this.doughnutChartLabels = data.labels;
|
||||
this.doughnutChartData = data.datasets[0]?.data || [];
|
||||
// Trigger change detection
|
||||
this.doughnutChartData = [...this.doughnutChartData];
|
||||
console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
||||
} else {
|
||||
console.warn('Doughnut chart received data does not have expected structure', data);
|
||||
// Reset to default data
|
||||
console.warn('Received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
}
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
@@ -363,21 +654,16 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
// Keep default data in case of error
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log('Missing required data for doughnut chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
// Don't set noDataAvailable to true when there's no required data
|
||||
// This allows static data to be displayed
|
||||
this.noDataAvailable = false;
|
||||
// Validate the chart data to ensure we have some data to display
|
||||
this.validateChartData();
|
||||
// Force a redraw to ensure the chart displays
|
||||
this.doughnutChartData = [...this.doughnutChartData];
|
||||
console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}
|
||||
@@ -475,6 +761,35 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/doughnut?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Drilldown data URL:', url);
|
||||
@@ -494,39 +809,18 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For doughnut charts, we need to extract the data differently
|
||||
// The first dataset's data array contains the values for the doughnut chart
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.doughnutChartLabels = data.chartLabels || [];
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
this.doughnutChartData = data.chartData[0].data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
} else {
|
||||
this.doughnutChartData = [];
|
||||
}
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
this.doughnutChartLabels = data.chartLabels;
|
||||
this.doughnutChartData = data.chartData;
|
||||
// Trigger change detection
|
||||
this.doughnutChartData = [...this.doughnutChartData];
|
||||
console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
||||
} else if (data && data.labels && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.doughnutChartLabels = data.labels || [];
|
||||
this.doughnutChartData = data.data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
this.doughnutChartLabels = data.labels;
|
||||
this.doughnutChartData = data.datasets[0]?.data || [];
|
||||
// Trigger change detection
|
||||
this.doughnutChartData = [...this.doughnutChartData];
|
||||
console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
|
||||
@@ -535,8 +829,6 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
this.noDataAvailable = true;
|
||||
this.doughnutChartLabels = [];
|
||||
this.doughnutChartData = [];
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
@@ -605,44 +897,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for legend item
|
||||
* @param index Index of the legend item
|
||||
*/
|
||||
public getLegendColor(index: number): string {
|
||||
// Get legend color for a specific index
|
||||
getLegendColor(index: number): string {
|
||||
return this.chartColors[index % this.chartColors.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure labels and data arrays have the same length
|
||||
*/
|
||||
private syncLabelAndDataArrays(): void {
|
||||
// Handle empty arrays
|
||||
if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length);
|
||||
|
||||
// Pad the shorter array with default values
|
||||
while (this.doughnutChartLabels.length < maxLength) {
|
||||
this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
|
||||
}
|
||||
|
||||
while (this.doughnutChartData.length < maxLength) {
|
||||
this.doughnutChartData.push(0);
|
||||
}
|
||||
|
||||
// Truncate the longer array if needed
|
||||
if (this.doughnutChartLabels.length > maxLength) {
|
||||
this.doughnutChartLabels = this.doughnutChartLabels.slice(0, maxLength);
|
||||
}
|
||||
|
||||
if (this.doughnutChartData.length > maxLength) {
|
||||
this.doughnutChartData = this.doughnutChartData.slice(0, maxLength);
|
||||
}
|
||||
}
|
||||
|
||||
// events
|
||||
public chartClicked(e: any): void {
|
||||
console.log('Doughnut chart clicked:', e);
|
||||
@@ -729,6 +988,6 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
||||
}
|
||||
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
console.log('Doughnut chart hovered:', e);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,285 @@
|
||||
<div class="dynamic-chart-container">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Dynamic Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing content -->
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { Component, OnInit, ViewChild, Input, OnChanges, SimpleChanges } from '@
|
||||
import { ChartConfiguration, ChartData, ChartDataset } from 'chart.js';
|
||||
import { BaseChartDirective } from 'ng2-charts';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dynamic-chart',
|
||||
@@ -37,9 +39,20 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
||||
|
||||
@ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined;
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -47,6 +60,12 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('DynamicChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -107,6 +126,14 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -139,7 +166,49 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/dynamic?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -504,4 +573,322 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
this.dynamicChartData = _dynamicChartData;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
|
||||
// Remove document click handler if it exists
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,285 @@
|
||||
<div class="financial-chart-container">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Financial Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing content -->
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
|
||||
@@ -1,108 +1,192 @@
|
||||
.financial-chart-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 400px;
|
||||
min-height: 400px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.financial-chart-container:hover {
|
||||
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #3498db;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 250px;
|
||||
margin: 15px 0;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-wrapper canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
|
||||
}
|
||||
|
||||
.chart-wrapper canvas:hover {
|
||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15));
|
||||
transform: scale(1.02);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-indicator, .no-data-message {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
font-style: italic;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-indicator p, .no-data-message p {
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.financial-chart-container {
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
font-size: 20px;
|
||||
margin-bottom: 15px;
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
min-height: 200px;
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-financial-chart',
|
||||
@@ -33,9 +35,20 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
||||
// Multi-layer drilldown configuration inputs
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -43,6 +56,12 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('FinancialChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -86,6 +105,14 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -118,7 +145,49 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/financial?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -496,4 +565,322 @@ export class FinancialChartComponent implements OnInit, OnChanges {
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
|
||||
// Remove document click handler if it exists
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,284 @@
|
||||
<div class="pie-chart-container">
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
</button>
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-wrapper">
|
||||
<!-- Show loading indicator -->
|
||||
<div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable">
|
||||
|
||||
@@ -149,10 +149,192 @@
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
// Filter section styles
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: left;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.pie-chart-container {
|
||||
padding: 15px;
|
||||
height: auto;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
@@ -179,4 +361,18 @@
|
||||
font-size: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { Subscription } from 'rxjs';
|
||||
templateUrl: './pie-chart.component.html',
|
||||
styleUrls: ['./pie-chart.component.scss']
|
||||
})
|
||||
export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
|
||||
@Input() xAxis: string;
|
||||
@Input() yAxis: string | string[];
|
||||
@Input() table: string;
|
||||
@@ -101,6 +101,11 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
@@ -133,6 +138,12 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('PieChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -158,6 +169,318 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
// Clean up document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Public method to refresh data when filters change
|
||||
@@ -243,7 +566,7 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Pie chart data URL:', url);
|
||||
console.log('Chart data URL:', url);
|
||||
|
||||
// Fetch data from the dashboard service with parameter field and value
|
||||
// For base level, we pass empty parameter and value, but now also pass filters
|
||||
@@ -251,12 +574,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
(data: any) => {
|
||||
console.log('Received pie chart data:', data);
|
||||
if (data === null) {
|
||||
console.warn('Pie chart API returned null data. Check if the API endpoint is working correctly.');
|
||||
console.warn('API returned null data. Check if the API endpoint is working correctly.');
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
return;
|
||||
@@ -264,50 +585,26 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For pie charts, we need to extract the data differently
|
||||
// The first dataset's data array contains the values for the pie chart
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.pieChartLabels = data.chartLabels || [];
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
this.pieChartData = data.chartData[0].data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
} else {
|
||||
this.pieChartData = [];
|
||||
}
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
this.pieChartLabels = data.chartLabels;
|
||||
this.pieChartData = data.chartData;
|
||||
// Trigger change detection
|
||||
this.pieChartData = [...this.pieChartData];
|
||||
console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
} else if (data && data.labels && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.pieChartLabels = data.labels || [];
|
||||
this.pieChartData = data.data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
this.pieChartLabels = data.labels;
|
||||
this.pieChartData = data.datasets[0]?.data || [];
|
||||
// Trigger change detection
|
||||
this.pieChartData = [...this.pieChartData];
|
||||
console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
} else {
|
||||
console.warn('Pie chart received data does not have expected structure', data);
|
||||
// Reset to default data
|
||||
console.warn('Received data does not have expected structure', data);
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
}
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
@@ -317,21 +614,16 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
// Validate and sanitize data to show default data
|
||||
this.validateChartData();
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
// Keep default data in case of error
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log('Missing required data for pie chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
// Don't set noDataAvailable to true when there's no required data
|
||||
// This allows static data to be displayed
|
||||
this.noDataAvailable = false;
|
||||
// Validate the chart data to ensure we have some data to display
|
||||
this.validateChartData();
|
||||
// Force a redraw to ensure the chart displays
|
||||
this.pieChartData = [...this.pieChartData];
|
||||
console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
// Reset flag after fetching
|
||||
this.isFetchingData = false;
|
||||
}
|
||||
@@ -477,39 +769,18 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For pie charts, we need to extract the data differently
|
||||
// The first dataset's data array contains the values for the pie chart
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.pieChartLabels = data.chartLabels || [];
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
this.pieChartData = data.chartData[0].data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
} else {
|
||||
this.pieChartData = [];
|
||||
}
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
this.pieChartLabels = data.chartLabels;
|
||||
this.pieChartData = data.chartData;
|
||||
// Trigger change detection
|
||||
this.pieChartData = [...this.pieChartData];
|
||||
console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
} else if (data && data.labels && data.data) {
|
||||
// Handle the original expected format as fallback
|
||||
} else if (data && data.labels && data.datasets) {
|
||||
// Backend has already filtered the data, just display it
|
||||
this.noDataAvailable = data.labels.length === 0;
|
||||
this.pieChartLabels = data.labels || [];
|
||||
this.pieChartData = data.data.map(value => {
|
||||
// Convert to number if it's not already
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
this.pieChartLabels = data.labels;
|
||||
this.pieChartData = data.datasets[0]?.data || [];
|
||||
// Trigger change detection
|
||||
this.pieChartData = [...this.pieChartData];
|
||||
console.log('Updated pie chart with drilldown legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
@@ -518,8 +789,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
this.noDataAvailable = true;
|
||||
this.pieChartLabels = [];
|
||||
this.pieChartData = [];
|
||||
// Validate and sanitize data
|
||||
this.validateChartData();
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
@@ -588,82 +857,32 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for legend item
|
||||
* @param index Index of the legend item
|
||||
*/
|
||||
public getLegendColor(index: number): string {
|
||||
return this.chartColors[index % this.chartColors.length];
|
||||
}
|
||||
// Validate chart data to ensure labels and data arrays have the same length
|
||||
private validateChartData(): void {
|
||||
if (this.pieChartLabels && this.pieChartData) {
|
||||
// For pie charts, we need to ensure labels and data arrays have the same length
|
||||
const labelCount = this.pieChartLabels.length;
|
||||
const dataCount = this.pieChartData.length;
|
||||
|
||||
/**
|
||||
* Ensure labels and data arrays have the same length
|
||||
*/
|
||||
private syncLabelAndDataArrays(): void {
|
||||
// Ensure we have matching arrays
|
||||
if (this.pieChartLabels.length !== this.pieChartData.length) {
|
||||
const maxLength = Math.max(this.pieChartLabels.length, this.pieChartData.length);
|
||||
while (this.pieChartLabels.length < maxLength) {
|
||||
this.pieChartLabels.push(`Label ${this.pieChartLabels.length + 1}`);
|
||||
}
|
||||
while (this.pieChartData.length < maxLength) {
|
||||
if (labelCount !== dataCount) {
|
||||
console.warn('Pie chart labels and data arrays have different lengths:', { labels: labelCount, data: dataCount });
|
||||
// Pad or truncate data array to match label count
|
||||
if (dataCount < labelCount) {
|
||||
// Pad with zeros
|
||||
while (this.pieChartData.length < labelCount) {
|
||||
this.pieChartData.push(0);
|
||||
}
|
||||
} else if (dataCount > labelCount) {
|
||||
// Truncate data array
|
||||
this.pieChartData = this.pieChartData.slice(0, labelCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and sanitize chart data
|
||||
*/
|
||||
private validateChartData(): void {
|
||||
console.log('Validating chart data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
|
||||
// Ensure we have valid arrays
|
||||
if (!Array.isArray(this.pieChartLabels)) {
|
||||
this.pieChartLabels = [];
|
||||
}
|
||||
|
||||
if (!Array.isArray(this.pieChartData)) {
|
||||
this.pieChartData = [];
|
||||
}
|
||||
|
||||
// Ensure we have some data to display
|
||||
if (this.pieChartLabels.length === 0 && this.pieChartData.length === 0) {
|
||||
// Add default data to ensure chart visibility
|
||||
this.pieChartLabels = ['Category A', 'Category B', 'Category C'];
|
||||
this.pieChartData = [30, 50, 20];
|
||||
console.log('Added default data for chart display');
|
||||
}
|
||||
|
||||
// Ensure labels and data arrays have the same length
|
||||
this.syncLabelAndDataArrays();
|
||||
|
||||
// Ensure all data values are numbers
|
||||
this.pieChartData = this.pieChartData.map(value => {
|
||||
const numValue = Number(value);
|
||||
return isNaN(numValue) ? 0 : numValue;
|
||||
});
|
||||
|
||||
console.log('After validation:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||
}
|
||||
|
||||
ngAfterViewChecked() {
|
||||
// Debugging: Log component state after view checks
|
||||
console.log('PieChartComponent state:', {
|
||||
labels: this.pieChartLabels,
|
||||
data: this.pieChartData,
|
||||
hasData: this.pieChartLabels.length > 0 && this.pieChartData.length > 0
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if chart data is valid and ready to display
|
||||
*/
|
||||
public isChartDataValid(): boolean {
|
||||
return this.pieChartLabels && this.pieChartData &&
|
||||
Array.isArray(this.pieChartLabels) && Array.isArray(this.pieChartData) &&
|
||||
this.pieChartLabels.length > 0 && this.pieChartData.length > 0 &&
|
||||
this.pieChartLabels.length === this.pieChartData.length;
|
||||
// Get legend color for a specific index
|
||||
getLegendColor(index: number): string {
|
||||
return this.chartColors[index % this.chartColors.length];
|
||||
}
|
||||
|
||||
// events
|
||||
@@ -752,6 +971,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
||||
}
|
||||
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
console.log('Pie chart hovered:', e);
|
||||
}
|
||||
|
||||
ngAfterViewChecked(): void {
|
||||
// This lifecycle hook can be used if needed for post-render operations
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,285 @@
|
||||
<div style="display: block; height: 100%; width: 100%;">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<div style="display: block">
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Polar Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position: relative; height: calc(100% - 50px);">
|
||||
<canvas baseChart
|
||||
[datasets]="polarAreaChartData"
|
||||
[labels]="polarAreaChartLabels"
|
||||
@@ -8,3 +288,4 @@
|
||||
(chartClick)="chartClicked($event)">
|
||||
</canvas>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,18 +1,192 @@
|
||||
// Polar Chart Component Styles
|
||||
div[style*="display: block"] {
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// Ensure the chart container has proper sizing
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-polar-chart',
|
||||
@@ -33,9 +35,20 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
// Multi-layer drilldown configuration inputs
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -43,6 +56,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('PolarChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -85,6 +104,324 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -117,7 +454,49 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/polar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -287,6 +666,35 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/polar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Drilldown data URL:', url);
|
||||
@@ -307,7 +715,6 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For polar charts, we need to extract the data differently
|
||||
// The first dataset's data array contains the values for the polar chart
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.polarAreaChartLabels = data.chartLabels;
|
||||
if (data.chartData && data.chartData.length > 0) {
|
||||
@@ -417,13 +824,13 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
// Get the label of the clicked element
|
||||
const clickedLabel = this.polarAreaChartLabels[clickedIndex];
|
||||
|
||||
console.log('Clicked on polar area:', { index: clickedIndex, label: clickedLabel });
|
||||
console.log('Clicked on polar point:', { index: clickedIndex, label: clickedLabel });
|
||||
|
||||
// If we're not at the base level, store original data
|
||||
if (this.currentDrilldownLevel === 0) {
|
||||
// Store original data before entering drilldown mode
|
||||
this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels];
|
||||
this.originalPolarAreaChartData = [...this.polarAreaChartData];
|
||||
this.originalPolarAreaChartData = JSON.parse(JSON.stringify(this.polarAreaChartData));
|
||||
console.log('Stored original data for drilldown');
|
||||
}
|
||||
|
||||
@@ -491,6 +898,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
console.log('Polar chart hovered:', e);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
// Clean up document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,283 @@
|
||||
<div style="display: block">
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
<div style="display: block; height: 100%; width: 100%;">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Radar Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data message -->
|
||||
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
||||
@@ -16,7 +285,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Chart display -->
|
||||
<div *ngIf="!noDataAvailable">
|
||||
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
|
||||
<canvas baseChart
|
||||
[datasets]="radarChartData"
|
||||
[labels]="radarChartLabels"
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-radar-chart',
|
||||
@@ -62,15 +64,40 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('RadarChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -94,6 +121,316 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -126,7 +463,49 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/radar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -298,6 +677,35 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/radar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
console.log('Drilldown data URL:', url);
|
||||
@@ -321,7 +729,6 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.radarChartLabels = data.chartLabels;
|
||||
// For radar charts, we need to ensure the data is properly formatted
|
||||
// Each dataset should have a data array with numeric values
|
||||
this.radarChartData = data.chartData.map(dataset => ({
|
||||
...dataset,
|
||||
data: dataset.data ? dataset.data.map(value => {
|
||||
@@ -358,6 +765,7 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
||||
this.noDataAvailable = true;
|
||||
this.radarChartLabels = [];
|
||||
this.radarChartData = [];
|
||||
// Keep current data in case of error
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -436,7 +844,7 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
||||
if (this.currentDrilldownLevel === 0) {
|
||||
// Store original data before entering drilldown mode
|
||||
this.originalRadarChartLabels = [...this.radarChartLabels];
|
||||
this.originalRadarChartData = [...this.radarChartData];
|
||||
this.originalRadarChartData = JSON.parse(JSON.stringify(this.radarChartData));
|
||||
console.log('Stored original data for drilldown');
|
||||
}
|
||||
|
||||
@@ -504,6 +912,12 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
console.log('Radar chart hovered:', e);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
// Clean up document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,283 @@
|
||||
<div style="display: block">
|
||||
<!-- Drilldown mode indicator -->
|
||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Level {{currentDrilldownLevel - 1}}
|
||||
</button>
|
||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
||||
Back to Main View
|
||||
<div style="display: block; height: 100%; width: 100%;">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'Scatter Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data message -->
|
||||
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
||||
@@ -16,7 +285,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Chart display -->
|
||||
<div *ngIf="!noDataAvailable">
|
||||
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
|
||||
<canvas baseChart
|
||||
[datasets]="scatterChartData"
|
||||
[type]="scatterChartType"
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1 1 300px;
|
||||
min-width: 250px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
|
||||
.filter-text-input,
|
||||
.filter-select,
|
||||
.filter-date {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multiselect-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
|
||||
.multiselect-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.multiselect-value {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.checkbox-group {
|
||||
padding: 8px;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.checkbox-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-range {
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
margin: 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.toggle-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// New header row styling
|
||||
.header-row {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { ChartData,ChartDataset } from 'chart.js';
|
||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||
import { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-scatter-chart',
|
||||
@@ -34,9 +36,20 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
// Multi-layer drilldown configuration inputs
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
constructor(private dashboardService: Dashboard3Service) { }
|
||||
constructor(
|
||||
private dashboardService: Dashboard3Service,
|
||||
private filterService: FilterService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
this.fetchChartData();
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize with default data
|
||||
this.fetchChartData();
|
||||
}
|
||||
@@ -44,6 +57,12 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('ScatterChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -107,6 +126,367 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
// Flag to prevent infinite loops
|
||||
private isFetchingData: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
this.fetchChartData();
|
||||
}
|
||||
|
||||
// Transform data to scatter chart format
|
||||
private transformToScatterData(labels: any[], data: any[]): ChartDataset[] {
|
||||
// For scatter charts, we need to transform the data into scatter format
|
||||
// Scatter charts expect data in the format: {x: number, y: number}
|
||||
console.log('Transforming data to scatter format:', { labels, data });
|
||||
|
||||
// If we have the expected scatter data format, return it as is
|
||||
if (data && data.length > 0 && data[0].data && data[0].data.length > 0 &&
|
||||
typeof data[0].data[0] === 'object' && data[0].data[0].hasOwnProperty('x') &&
|
||||
data[0].data[0].hasOwnProperty('y')) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Otherwise, create a default scatter dataset
|
||||
const scatterDatasets: ChartDataset[] = [
|
||||
{
|
||||
data: [
|
||||
{ x: 1, y: 1 },
|
||||
{ x: 2, y: 3 },
|
||||
{ x: 3, y: -2 },
|
||||
{ x: 4, y: 4 },
|
||||
{ x: 5, y: -3 },
|
||||
],
|
||||
label: 'Dataset 1',
|
||||
pointRadius: 10,
|
||||
backgroundColor: [
|
||||
'red',
|
||||
'green',
|
||||
'blue',
|
||||
'purple',
|
||||
'yellow',
|
||||
'brown',
|
||||
'magenta',
|
||||
'cyan',
|
||||
'orange',
|
||||
'pink'
|
||||
],
|
||||
}
|
||||
];
|
||||
|
||||
return scatterDatasets;
|
||||
}
|
||||
|
||||
fetchChartData(): void {
|
||||
// Set flag to prevent recursive calls
|
||||
this.isFetchingData = true;
|
||||
@@ -139,7 +519,49 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
filterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Base filter parameters:', filterParams);
|
||||
|
||||
// Add common filters to filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
console.log('Common filters from service:', commonFilters);
|
||||
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with base filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add base filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const baseFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, baseFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse base filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters using the field name as the key, not the filter id
|
||||
Object.keys(commonFilters).forEach(filterId => {
|
||||
const filterValue = commonFilters[filterId];
|
||||
// Find the filter definition to get the field name
|
||||
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||
if (filterDef && filterDef.field) {
|
||||
const fieldName = filterDef.field;
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[fieldName] = filterValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to using filterId as field name if no field is defined
|
||||
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||
mergedFilterObj[filterId] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Final filter parameters:', filterParams);
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/scatter?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -287,24 +709,34 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
// Convert drilldownFilters to filter parameters for drilldown level
|
||||
let drilldownFilterParams = '';
|
||||
if (this.drilldownFilters && this.drilldownFilters.length > 0) {
|
||||
const filterObj = {};
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.field && filter.value) {
|
||||
filterObj[filter.field] = filter.value;
|
||||
// Add common filters to drilldown filter parameters
|
||||
const commonFilters = this.filterService.getFilterValues();
|
||||
if (Object.keys(commonFilters).length > 0) {
|
||||
// Merge common filters with drilldown filters
|
||||
const mergedFilterObj = {};
|
||||
|
||||
// Add drilldown filters first
|
||||
if (filterParams) {
|
||||
try {
|
||||
const drilldownFilterObj = JSON.parse(filterParams);
|
||||
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add common filters
|
||||
Object.keys(commonFilters).forEach(key => {
|
||||
const value = commonFilters[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
mergedFilterObj[key] = value;
|
||||
}
|
||||
});
|
||||
if (Object.keys(filterObj).length > 0) {
|
||||
drilldownFilterParams = JSON.stringify(filterObj);
|
||||
}
|
||||
}
|
||||
console.log('Drilldown filter parameters:', drilldownFilterParams);
|
||||
|
||||
// Use drilldown filters if available, otherwise use layer filters
|
||||
const finalFilterParams = drilldownFilterParams || filterParams;
|
||||
console.log('Final filter parameters:', finalFilterParams);
|
||||
if (Object.keys(mergedFilterObj).length > 0) {
|
||||
filterParams = JSON.stringify(mergedFilterObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the URL that will be called
|
||||
const url = `chart/getdashjson/scatter?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||
@@ -312,7 +744,7 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
|
||||
// Fetch data from the dashboard service with parameter field and value
|
||||
// Backend handles filtering, we just pass the parameter field and value
|
||||
this.dashboardService.getChartData(actualApiUrl, 'scatter', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, finalFilterParams).subscribe(
|
||||
this.dashboardService.getChartData(actualApiUrl, 'scatter', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, filterParams).subscribe(
|
||||
(data: any) => {
|
||||
console.log('Received drilldown data:', data);
|
||||
if (data === null) {
|
||||
@@ -325,7 +757,6 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
// Handle the actual data structure returned by the API
|
||||
if (data && data.chartLabels && data.chartData) {
|
||||
// For scatter charts, we need to transform the data into scatter format
|
||||
// Scatter charts expect data in the format: {x: number, y: number}
|
||||
this.noDataAvailable = data.chartLabels.length === 0;
|
||||
this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData);
|
||||
console.log('Updated scatter chart with drilldown data:', this.scatterChartData);
|
||||
@@ -349,33 +780,6 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
);
|
||||
}
|
||||
|
||||
// Transform chart data to scatter chart format
|
||||
private transformToScatterData(labels: string[], datasets: any[]): ChartDataset[] {
|
||||
// For scatter charts, we need to transform the data into scatter format
|
||||
// Scatter charts expect data in the format: {x: number, y: number}
|
||||
|
||||
// This is a simple transformation - in a real implementation, you might want to
|
||||
// create a more sophisticated mapping based on your data structure
|
||||
return datasets.map((dataset, index) => {
|
||||
// Create scatter data points
|
||||
const scatterData = labels.map((label, i) => {
|
||||
// Use x-axis data as x coordinate, y-axis data as y coordinate
|
||||
const xValue = dataset.data[i] || 0;
|
||||
const yValue = i < datasets.length ? (datasets[i].data[index] || 0) : 0;
|
||||
|
||||
return { x: xValue, y: yValue };
|
||||
});
|
||||
|
||||
return {
|
||||
data: scatterData,
|
||||
label: dataset.label || `Dataset ${index + 1}`,
|
||||
backgroundColor: dataset.backgroundColor || `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.6)`,
|
||||
borderColor: dataset.borderColor || 'rgba(0, 0, 0, 1)',
|
||||
pointRadius: dataset.pointRadius || 5
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Reset to original data (go back to base level)
|
||||
resetToOriginalData(): void {
|
||||
console.log('Resetting to original data');
|
||||
@@ -436,16 +840,18 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
// Get the index of the clicked element
|
||||
const clickedIndex = e.active[0].index;
|
||||
|
||||
// Get the label of the clicked element
|
||||
// For scatter charts, we might not have labels in the same way as other charts
|
||||
const clickedLabel = `Point ${clickedIndex}`;
|
||||
// Get the dataset index
|
||||
const datasetIndex = e.active[0].datasetIndex;
|
||||
|
||||
console.log('Clicked on scatter point:', { index: clickedIndex, label: clickedLabel });
|
||||
// Get the data point
|
||||
const dataPoint = this.scatterChartData[datasetIndex].data[clickedIndex];
|
||||
|
||||
console.log('Clicked on scatter point:', { datasetIndex, clickedIndex, dataPoint });
|
||||
|
||||
// If we're not at the base level, store original data
|
||||
if (this.currentDrilldownLevel === 0) {
|
||||
// Store original data before entering drilldown mode
|
||||
this.originalScatterChartData = [...this.scatterChartData];
|
||||
this.originalScatterChartData = JSON.parse(JSON.stringify(this.scatterChartData));
|
||||
console.log('Stored original data for drilldown');
|
||||
}
|
||||
|
||||
@@ -487,9 +893,10 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
// Add this click to the drilldown stack
|
||||
const stackEntry = {
|
||||
level: nextDrilldownLevel,
|
||||
datasetIndex: datasetIndex,
|
||||
clickedIndex: clickedIndex,
|
||||
clickedLabel: clickedLabel,
|
||||
clickedValue: clickedLabel // Using label as value for now
|
||||
dataPoint: dataPoint,
|
||||
clickedValue: dataPoint // Using data point as value for now
|
||||
};
|
||||
|
||||
this.drilldownStack.push(stackEntry);
|
||||
@@ -513,6 +920,12 @@ export class ScatterChartComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
public chartHovered(e: any): void {
|
||||
console.log(e);
|
||||
console.log('Scatter chart hovered:', e);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
// Clean up document click handler
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,286 @@
|
||||
<table class="table">
|
||||
<div class="to-do-chart-container">
|
||||
<!-- Filter Controls Section -->
|
||||
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||
<!-- Base Filters -->
|
||||
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||
<h4>Base Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onBaseFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'base-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drilldown Filters -->
|
||||
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||
<h4>Drilldown Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'drilldown-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Filters -->
|
||||
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||
<h4>Layer Filters</h4>
|
||||
<div class="filter-controls">
|
||||
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||
|
||||
<!-- Text Filter -->
|
||||
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||
<input type="text"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
[placeholder]="filter.field"
|
||||
class="clr-input filter-text-input">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Filter -->
|
||||
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||
<select [(ngModel)]="filter.value"
|
||||
(ngModelChange)="onLayerFilterChange(filter)"
|
||||
class="clr-select filter-select">
|
||||
<option value="">Select {{ filter.field }}</option>
|
||||
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||
<span class="multiselect-label">{{ filter.field }}</span>
|
||||
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||
({{ getSelectedOptionsCount(filter) }} selected)
|
||||
</span>
|
||||
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||
</div>
|
||||
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||
<div class="checkbox-group">
|
||||
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||
<input type="checkbox"
|
||||
[checked]="isOptionSelected(filter, option)"
|
||||
(change)="onMultiSelectChange(filter, option, $event)"
|
||||
[id]="'layer-' + filter.field + '-' + i"
|
||||
class="clr-checkbox">
|
||||
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||
<div class="date-input-group">
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.start"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||
placeholder="Start Date"
|
||||
class="clr-input filter-date">
|
||||
<span class="date-separator">to</span>
|
||||
<input type="date"
|
||||
[(ngModel)]="filter.value.end"
|
||||
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||
placeholder="End Date"
|
||||
class="clr-input filter-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter -->
|
||||
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||
<input type="checkbox"
|
||||
[(ngModel)]="filter.value"
|
||||
(ngModelChange)="onToggleChange(filter, $event)"
|
||||
clrToggle
|
||||
class="clr-toggle">
|
||||
<label class="toggle-label">{{ filter.field }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters Button -->
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header row with chart title and drilldown navigation -->
|
||||
<div class="clr-row header-row">
|
||||
<div class="clr-col-6">
|
||||
<h3 class="chart-title">{{charttitle || 'To Do Chart'}}</h3>
|
||||
</div>
|
||||
<div class="clr-col-6" style="text-align: right;">
|
||||
<!-- Add drilldown navigation controls -->
|
||||
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show current drilldown level -->
|
||||
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||
<div class="clr-col-12">
|
||||
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Drilldown Level: {{currentDrilldownLevel}}
|
||||
<span *ngIf="drilldownStack.length > 0">
|
||||
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing content -->
|
||||
<table class="table todo-table">
|
||||
<thead>
|
||||
<th class="c-col">#</th>
|
||||
<th>Item</th>
|
||||
@@ -16,7 +298,7 @@
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<input [(ngModel)]="todo" placeholder="Add Todo" class="clr-input">
|
||||
<input [(ngModel)]="todo" placeholder="Add Todo" class="clr-input todo-input">
|
||||
</td>
|
||||
<td style="text-align:right">
|
||||
<a routerLink="." color='primary' (click)="addTodo(todo)">
|
||||
@@ -25,3 +307,4 @@
|
||||
</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 { FilterService } from '../../common-filter/filter.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-to-do-chart',
|
||||
@@ -21,15 +23,43 @@ export class ToDoChartComponent implements OnInit, OnChanges {
|
||||
@Input() datasource: string;
|
||||
@Input() fieldName: string;
|
||||
@Input() connection: number; // Add connection input
|
||||
// Drilldown configuration inputs
|
||||
@Input() drilldownEnabled: boolean = false;
|
||||
@Input() drilldownApiUrl: string;
|
||||
@Input() drilldownXAxis: string;
|
||||
@Input() drilldownYAxis: string;
|
||||
@Input() drilldownParameter: string; // Add drilldown parameter input
|
||||
@Input() baseFilters: any[] = []; // Add base filters input
|
||||
@Input() drilldownFilters: any[] = []; // Add drilldown filters input
|
||||
// Multi-layer drilldown configuration inputs
|
||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||
|
||||
constructor() { }
|
||||
// Multi-layer drilldown state tracking
|
||||
drilldownStack: any[] = []; // Stack to track drilldown navigation history
|
||||
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
|
||||
originalTodoList: string[] = [];
|
||||
|
||||
constructor(private filterService: FilterService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to filter changes
|
||||
this.subscriptions.push(
|
||||
this.filterService.filterState$.subscribe(filters => {
|
||||
// When filters change, refresh the chart data
|
||||
// For To Do chart, this would trigger a refresh of the todo list
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log('ToDoChartComponent input changes:', changes);
|
||||
|
||||
// Initialize filter values if they haven't been initialized yet
|
||||
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||
this.initializeFilterValues();
|
||||
this.filtersInitialized = true;
|
||||
}
|
||||
|
||||
// Check if any of the key properties have changed
|
||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||
@@ -48,6 +78,14 @@ export class ToDoChartComponent implements OnInit, OnChanges {
|
||||
todo: string;
|
||||
todoList = ['todo 1'];
|
||||
|
||||
// Add properties for filter functionality
|
||||
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
private filtersInitialized: boolean = false;
|
||||
|
||||
// Subscriptions to unsubscribe on destroy
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
fetchToDoData(): void {
|
||||
// If we have the necessary data, fetch to-do data from the service
|
||||
if (this.table) {
|
||||
@@ -73,4 +111,322 @@ export class ToDoChartComponent implements OnInit, OnChanges {
|
||||
this.todoList.splice(todoIx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize filter values with proper default values based on type
|
||||
private initializeFilterValues(): void {
|
||||
console.log('Initializing filter values');
|
||||
|
||||
// Initialize base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.value === undefined || filter.value === null) {
|
||||
switch (filter.type) {
|
||||
case 'multiselect':
|
||||
filter.value = [];
|
||||
break;
|
||||
case 'date-range':
|
||||
filter.value = { start: null, end: null };
|
||||
break;
|
||||
case 'toggle':
|
||||
filter.value = false;
|
||||
break;
|
||||
default:
|
||||
filter.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Filter values initialized:', {
|
||||
baseFilters: this.baseFilters,
|
||||
drilldownFilters: this.drilldownFilters,
|
||||
drilldownLayers: this.drilldownLayers
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are active filters
|
||||
hasActiveFilters(): boolean {
|
||||
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||
this.hasActiveLayerFilters();
|
||||
}
|
||||
|
||||
// Check if there are active layer filters for current drilldown level
|
||||
hasActiveLayerFilters(): boolean {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
return layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters &&
|
||||
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get active layer filters for current drilldown level
|
||||
getActiveLayerFilters(): any[] {
|
||||
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||
if (layerIndex < this.drilldownLayers.length &&
|
||||
this.drilldownLayers[layerIndex].filters) {
|
||||
return this.drilldownLayers[layerIndex].filters;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get filter options for dropdown/multiselect filters
|
||||
getFilterOptions(filter: any): string[] {
|
||||
if (filter.options) {
|
||||
if (Array.isArray(filter.options)) {
|
||||
return filter.options;
|
||||
} else if (typeof filter.options === 'string') {
|
||||
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if an option is selected for multiselect filters
|
||||
isOptionSelected(filter: any, option: string): boolean {
|
||||
if (!filter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.includes(option);
|
||||
}
|
||||
|
||||
return filter.value === option;
|
||||
}
|
||||
|
||||
// Handle base filter changes
|
||||
onBaseFilterChange(filter: any): void {
|
||||
console.log('Base filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
// For To Do chart, this would trigger a refresh of the todo list
|
||||
}
|
||||
|
||||
// Handle drilldown filter changes
|
||||
onDrilldownFilterChange(filter: any): void {
|
||||
console.log('Drilldown filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
// For To Do chart, this would trigger a refresh of the todo list
|
||||
}
|
||||
|
||||
// Handle layer filter changes
|
||||
onLayerFilterChange(filter: any): void {
|
||||
console.log('Layer filter changed:', filter);
|
||||
// Refresh data when filter changes
|
||||
// For To Do chart, this would trigger a refresh of the todo list
|
||||
}
|
||||
|
||||
// Handle multiselect changes
|
||||
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||
const checked = event.target.checked;
|
||||
|
||||
// Initialize filter.value as array if it's not already
|
||||
if (!Array.isArray(filter.value)) {
|
||||
filter.value = [];
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Add option to array if not already present
|
||||
if (!filter.value.includes(option)) {
|
||||
filter.value.push(option);
|
||||
}
|
||||
} else {
|
||||
// Remove option from array
|
||||
filter.value = filter.value.filter((item: string) => item !== option);
|
||||
}
|
||||
|
||||
// Refresh data when filter changes
|
||||
// For To Do chart, this would trigger a refresh of the todo list
|
||||
}
|
||||
|
||||
// Handle date range changes
|
||||
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||
filter.value = dateRange;
|
||||
// Refresh data when filter changes
|
||||
// For To Do chart, this would trigger a refresh of the todo list
|
||||
}
|
||||
|
||||
// Handle toggle changes
|
||||
onToggleChange(filter: any, checked: boolean): void {
|
||||
filter.value = checked;
|
||||
// Refresh data when filter changes
|
||||
// For To Do chart, this would trigger a refresh of the todo list
|
||||
}
|
||||
|
||||
// Toggle multiselect dropdown visibility
|
||||
toggleMultiselect(filter: any, context: string): void {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
if (this.isMultiselectOpen(filter, context)) {
|
||||
this.openMultiselects.delete(filterId);
|
||||
} else {
|
||||
// Close all other multiselects first
|
||||
this.openMultiselects.clear();
|
||||
this.openMultiselects.set(filterId, context);
|
||||
|
||||
// Add document click handler to close dropdown when clicking outside
|
||||
this.addDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Add document click handler to close dropdowns when clicking outside
|
||||
private addDocumentClickHandler(): void {
|
||||
if (!this.documentClickHandler) {
|
||||
this.documentClickHandler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if click is outside any multiselect dropdown
|
||||
if (!target.closest('.multiselect-container')) {
|
||||
this.openMultiselects.clear();
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.documentClickHandler!);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
private removeDocumentClickHandler(): void {
|
||||
if (this.documentClickHandler) {
|
||||
document.removeEventListener('click', this.documentClickHandler);
|
||||
this.documentClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if multiselect dropdown is open
|
||||
isMultiselectOpen(filter: any, context: string): boolean {
|
||||
const filterId = `${context}-${filter.field}`;
|
||||
return this.openMultiselects.has(filterId);
|
||||
}
|
||||
|
||||
// Get count of selected options for a multiselect filter
|
||||
getSelectedOptionsCount(filter: any): number {
|
||||
if (!filter.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
clearAllFilters(): void {
|
||||
// Clear base filters
|
||||
if (this.baseFilters) {
|
||||
this.baseFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear drilldown filters
|
||||
if (this.drilldownFilters) {
|
||||
this.drilldownFilters.forEach(filter => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layer filters
|
||||
if (this.drilldownLayers) {
|
||||
this.drilldownLayers.forEach(layer => {
|
||||
if (layer.filters) {
|
||||
layer.filters.forEach((filter: any) => {
|
||||
if (filter.type === 'multiselect') {
|
||||
filter.value = [];
|
||||
} else if (filter.type === 'date-range') {
|
||||
filter.value = { start: null, end: null };
|
||||
} else if (filter.type === 'toggle') {
|
||||
filter.value = false;
|
||||
} else {
|
||||
filter.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all multiselect dropdowns
|
||||
this.openMultiselects.clear();
|
||||
|
||||
// Refresh data
|
||||
// For To Do chart, this would trigger a refresh of the todo list
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
|
||||
// Remove document click handler if it exists
|
||||
this.removeDocumentClickHandler();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user