17 Commits

Author SHA1 Message Date
Gaurav Kumar
9b775a8c63 fin 2025-10-29 12:59:20 +05:30
Gaurav Kumar
cf4fc1be93 todo 2025-10-29 12:21:24 +05:30
Gaurav Kumar
4c135c4901 to do 2025-10-29 12:16:54 +05:30
Gaurav Kumar
0b738ca7ca scatter 2025-10-29 11:35:09 +05:30
Gaurav Kumar
1b17bb706d scatter 2025-10-29 11:11:36 +05:30
Gaurav Kumar
f740076d60 bar chart 2025-10-28 17:04:02 +05:30
Gaurav Kumar
bedcc0822d chart 2025-10-28 16:31:45 +05:30
Gaurav Kumar
87810acc9e line 2025-10-28 15:41:36 +05:30
Gaurav Kumar
96b90e5dbd barchart 2025-10-28 15:32:37 +05:30
Gaurav Kumar
ced99e0940 grid view 2025-10-28 15:11:40 +05:30
Gaurav Kumar
4f82ae8698 grid view 2025-10-28 12:40:01 +05:30
Gaurav Kumar
e6779e8f34 filter 2025-10-28 12:29:10 +05:30
Gaurav Kumar
82425d5377 Update editnewdash.component.html 2025-10-28 12:13:15 +05:30
Gaurav Kumar
2995328ec1 compact filter 2025-10-27 20:36:22 +05:30
Gaurav Kumar
afc2c1f8a1 filter with runner 2025-10-27 20:12:22 +05:30
Gaurav Kumar
418b02acd7 checkbox multislect filter, in edit new dash 2025-10-27 19:02:47 +05:30
Gaurav Kumar
cdd752469c builder 2025-10-27 18:48:16 +05:30
60 changed files with 14576 additions and 1969 deletions

View File

@@ -79,12 +79,16 @@
<!-- Multi-Select Filter --> <!-- Multi-Select Filter -->
<div class="filter-control" *ngIf="filterType === 'multiselect'"> <div class="filter-control" *ngIf="filterType === 'multiselect'">
<select [(ngModel)]="filterValue" <div class="compact-multiselect-checkboxes" style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;">
(ngModelChange)="onFilterValueChange($event)" <div *ngFor="let option of filterOptions" class="clr-checkbox-wrapper" style="margin-bottom: 5px;">
multiple <input type="checkbox"
class="clr-select compact-multiselect"> [id]="'multiselect-' + option"
<option *ngFor="let option of filterOptions" [value]="option">{{ option }}</option> [value]="option"
</select> [checked]="isOptionSelected(option)"
(change)="onMultiselectOptionChange($event, option)">
<label [for]="'multiselect-' + option" class="clr-control-label">{{ option }}</label>
</div>
</div>
</div> </div>
<!-- Date Range Filter --> <!-- Date Range Filter -->

View File

@@ -70,6 +70,41 @@
min-height: 24px; min-height: 24px;
} }
.multiselect-container {
max-height: 150px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
padding: 5px;
background: white;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 3px;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 5px;
}
.checkbox-label {
font-size: 12px;
margin: 0;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.clr-checkbox {
margin: 0;
cursor: pointer;
}
.toggle-label { .toggle-label {
margin: 0; margin: 0;
font-size: 12px; font-size: 12px;

View File

@@ -38,19 +38,6 @@ export class CompactFilterComponent implements OnInit, OnChanges {
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter definitions to get available filters
this.filterService.filters$.subscribe(filters => {
this.availableFilters = filters;
this.updateSelectedFilter();
});
// Subscribe to filter state changes
this.filterService.filterState$.subscribe(state => {
if (this.selectedFilter && state.hasOwnProperty(this.selectedFilter.id)) {
this.filterValue = state[this.selectedFilter.id];
}
});
// Initialize configuration from inputs // Initialize configuration from inputs
this.configFilterKey = this.filterKey; this.configFilterKey = this.filterKey;
this.configFilterType = this.filterType; this.configFilterType = this.filterType;
@@ -70,26 +57,53 @@ export class CompactFilterComponent implements OnInit, OnChanges {
// Register this filter with the filter service // Register this filter with the filter service
this.registerFilter(); this.registerFilter();
// Subscribe to filter definitions to get available filters
this.filterService.filters$.subscribe(filters => {
this.availableFilters = filters;
this.updateSelectedFilter();
});
// Subscribe to filter state changes
this.filterService.filterState$.subscribe(state => {
if (this.selectedFilter && state.hasOwnProperty(this.selectedFilter.id)) {
this.filterValue = state[this.selectedFilter.id];
}
});
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
// If filterKey or filterType changes, re-register the filter // If filterKey or filterType changes, re-register the filter
if (changes.filterKey || changes.filterType) { if (changes.filterKey || changes.filterType) {
// Load available values for the current filter key if it's a dropdown or multiselect
if ((this.filterType === 'dropdown' || this.filterType === 'multiselect') && this.filterKey) {
this.loadAvailableValues(this.filterKey);
}
this.registerFilter(); this.registerFilter();
} }
// Handle API URL changes
if (changes.apiUrl && !changes.apiUrl.firstChange) {
if (this.apiUrl) {
this.loadAvailableKeys();
}
}
} }
// Register this filter with the filter service // Register this filter with the filter service
registerFilter(): void { registerFilter(): void {
if (this.filterKey) { if (this.filterKey) {
// Get current filter values from the service
const currentFilterValues = this.filterService.getFilterValues();
// Create a filter definition for this compact filter // Create a filter definition for this compact filter
const filterDef: Filter = { const filterDef: Filter = {
id: `compact-filter-${this.filterKey}`, id: `${this.filterKey}`,
field: this.filterKey, field: this.filterKey,
label: this.filterLabel || this.filterKey, label: this.filterLabel || this.filterKey,
type: this.filterType as any, type: this.filterType as any,
options: this.filterOptions, options: this.filterOptions,
value: this.filterValue value: this.filterValue // Use the current filter value
}; };
// Get current filters // Get current filters
@@ -99,9 +113,32 @@ export class CompactFilterComponent implements OnInit, OnChanges {
const existingFilterIndex = currentFilters.findIndex(f => f.id === filterDef.id); const existingFilterIndex = currentFilters.findIndex(f => f.id === filterDef.id);
if (existingFilterIndex >= 0) { if (existingFilterIndex >= 0) {
// Preserve the existing filter configuration
const existingFilter = currentFilters[existingFilterIndex];
// Preserve the existing filter value if it exists in the service
if (currentFilterValues.hasOwnProperty(existingFilter.id)) {
filterDef.value = currentFilterValues[existingFilter.id];
this.filterValue = filterDef.value; // Update local value
} else if (existingFilter.value !== undefined) {
// Fallback to existing filter's value if no service value
filterDef.value = existingFilter.value;
this.filterValue = filterDef.value;
}
// Preserve other configuration properties
filterDef.label = existingFilter.label;
filterDef.options = existingFilter.options || this.filterOptions;
// Update existing filter // Update existing filter
currentFilters[existingFilterIndex] = filterDef; currentFilters[existingFilterIndex] = filterDef;
} else { } else {
// For new filters, check if there's already a value in the service
if (currentFilterValues.hasOwnProperty(filterDef.id)) {
filterDef.value = currentFilterValues[filterDef.id];
this.filterValue = filterDef.value; // Update local value
}
// Add new filter // Add new filter
currentFilters.push(filterDef); currentFilters.push(filterDef);
} }
@@ -118,9 +155,24 @@ export class CompactFilterComponent implements OnInit, OnChanges {
if (this.filterKey && this.availableFilters.length > 0) { if (this.filterKey && this.availableFilters.length > 0) {
this.selectedFilter = this.availableFilters.find(f => f.field === this.filterKey) || null; this.selectedFilter = this.availableFilters.find(f => f.field === this.filterKey) || null;
if (this.selectedFilter) { if (this.selectedFilter) {
// Get current value for this filter // Get current value for this filter from the service
const currentState = this.filterService.getFilterValues(); const currentState = this.filterService.getFilterValues();
this.filterValue = currentState[this.selectedFilter.id] || ''; const filterValue = currentState[this.selectedFilter.id];
if (filterValue !== undefined) {
this.filterValue = filterValue;
} else if (this.selectedFilter.value !== undefined) {
// Use the filter's default value if no service value
this.filterValue = this.selectedFilter.value;
} else {
// Use the current filter value as fallback
this.filterValue = this.filterValue || '';
}
// Also update configuration properties from the selected filter
this.configFilterKey = this.selectedFilter.field;
this.configFilterType = this.selectedFilter.type;
this.configFilterLabel = this.selectedFilter.label;
this.configFilterOptions = (this.selectedFilter.options || []).join(',');
} }
} }
} }
@@ -130,6 +182,14 @@ export class CompactFilterComponent implements OnInit, OnChanges {
this.filterValue = value; this.filterValue = value;
this.filterService.updateFilterValue(this.selectedFilter.id, value); this.filterService.updateFilterValue(this.selectedFilter.id, value);
this.filterChange.emit({ filterId: this.selectedFilter.id, value: value }); this.filterChange.emit({ filterId: this.selectedFilter.id, value: value });
// Update the filter definition in the service to reflect the new value
const currentFilters = this.filterService.getFilters();
const filterIndex = currentFilters.findIndex(f => f.id === this.selectedFilter.id);
if (filterIndex >= 0) {
currentFilters[filterIndex].value = value;
this.filterService.setFilters(currentFilters);
}
} }
} }
@@ -179,11 +239,19 @@ export class CompactFilterComponent implements OnInit, OnChanges {
toggleConfigMode(): void { toggleConfigMode(): void {
this.isConfigMode = !this.isConfigMode; this.isConfigMode = !this.isConfigMode;
if (this.isConfigMode) { if (this.isConfigMode) {
// Initialize config values // Initialize config values from current filter if available
if (this.selectedFilter) {
this.configFilterKey = this.selectedFilter.field;
this.configFilterType = this.selectedFilter.type;
this.configFilterLabel = this.selectedFilter.label;
this.configFilterOptions = (this.selectedFilter.options || []).join(',');
} else {
// Fallback to current properties
this.configFilterKey = this.filterKey; this.configFilterKey = this.filterKey;
this.configFilterType = this.filterType; this.configFilterType = this.filterType;
this.configFilterLabel = this.filterLabel; this.configFilterLabel = this.filterLabel;
this.configFilterOptions = this.filterOptions.join(','); this.configFilterOptions = this.filterOptions.join(',');
}
this.configApiUrl = this.apiUrl; this.configApiUrl = this.apiUrl;
this.configConnectionId = this.connectionId; this.configConnectionId = this.connectionId;
} }
@@ -216,8 +284,8 @@ export class CompactFilterComponent implements OnInit, OnChanges {
} }
// Load available values for the selected key if it's a dropdown or multiselect // Load available values for the selected key if it's a dropdown or multiselect
if ((this.filterType === 'dropdown' || this.filterType === 'multiselect') && this.filterKey) { if ((this.configFilterType === 'dropdown' || this.configFilterType === 'multiselect') && this.configFilterKey) {
this.loadAvailableValues(this.filterKey); this.loadAvailableValues(this.configFilterKey);
} }
// Register the updated filter with the filter service // Register the updated filter with the filter service
@@ -263,4 +331,48 @@ export class CompactFilterComponent implements OnInit, OnChanges {
this.loadAvailableValues(this.configFilterKey); this.loadAvailableValues(this.configFilterKey);
} }
} }
// Add method to check if an option is selected for checkboxes
isOptionSelected(option: string): boolean {
if (!this.filterValue) {
return false;
}
// Ensure filterValue is an array for multiselect
if (!Array.isArray(this.filterValue)) {
this.filterValue = [];
return false;
}
return this.filterValue.includes(option);
}
// need to check this
// Add method to handle multiselect option change
onMultiselectOptionChange(event: any, option: string): void {
// Initialize filterValue array if it doesn't exist
if (!this.filterValue) {
this.filterValue = [];
}
// Ensure filterValue is an array
if (!Array.isArray(this.filterValue)) {
this.filterValue = [];
}
if (event.target.checked) {
// Add option if not already in array
if (!this.filterValue.includes(option)) {
this.filterValue.push(option);
}
} else {
// Remove option from array
const index = this.filterValue.indexOf(option);
if (index > -1) {
this.filterValue.splice(index, 1);
}
}
// Emit the change event
this.onFilterValueChange(this.filterValue);
}
} }

View File

@@ -90,11 +90,106 @@
</div> </div>
</div> </div>
<!-- Compact Filter Configuration (shown only for Compact Filter components) -->
<div class="clr-row" *ngIf="gadgetsEditdata?.fieldName === 'Compact Filter'">
<div class="clr-col-sm-12">
<h4>Compact Filter Configuration</h4>
<div class="clr-row">
<div class="clr-col-sm-12">
<label for="compactFilterConnection">Connection</label>
<select id="compactFilterConnection" class="clr-select" [(ngModel)]="gadgetsEditdata.connection"
(ngModelChange)="onCompactFilterConnectionChange($event)" [ngModelOptions]="{standalone: true}">
<option value="">Select Connection</option>
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
{{conn.connection_name || conn.id}}
</option>
</select>
<div class="clr-subtext">Select a connection for this compact filter</div>
</div>
</div>
<div class="clr-row" style="margin-top: 10px;">
<div class="clr-col-sm-12">
<label for="compactFilterApiUrl">API URL</label>
<div>
<input type="text" id="compactFilterApiUrl" class="clr-input" [(ngModel)]="gadgetsEditdata.table"
(ngModelChange)="onCompactFilterApiUrlChange($event)" [ngModelOptions]="{standalone: true}"
placeholder="Enter API URL">
<span>
<button class="btn btn-icon btn-primary" style="margin: 0px;"
(click)="loadAvailableKeys(gadgetsEditdata.table, gadgetsEditdata.connection)"
[disabled]="!gadgetsEditdata.table">
<clr-icon shape="redo"></clr-icon>
</button>
</span>
</div>
<div class="clr-subtext">Enter the API URL to fetch data for this filter</div>
</div>
</div>
<div class="clr-row" style="margin-top: 10px;">
<div class="clr-col-sm-12">
<label for="filterKey">Filter Key</label>
<select id="filterKey" class="clr-select" [(ngModel)]="gadgetsEditdata.filterKey"
(ngModelChange)="onFilterKeyChange($event)" [ngModelOptions]="{standalone: true}">
<option value="">Select Filter Key</option>
<option *ngFor="let key of availableKeys" [value]="key">{{ key }}</option>
</select>
<div class="clr-subtext">Select the field name to filter on</div>
</div>
</div>
<div class="clr-row" style="margin-top: 10px;">
<div class="clr-col-sm-12">
<label for="filterType">Filter Type</label>
<select id="filterType" class="clr-select" [(ngModel)]="gadgetsEditdata.filterType"
(ngModelChange)="onFilterTypeChange($event)" [ngModelOptions]="{standalone: true}">
<option value="text">Text</option>
<option value="dropdown">Dropdown</option>
<option value="multiselect">Multi-Select</option>
<option value="date-range">Date Range</option>
<option value="toggle">Toggle</option>
</select>
<div class="clr-subtext">Select the type of filter control to display</div>
</div>
</div>
<div class="clr-row" style="margin-top: 10px;">
<div class="clr-col-sm-12">
<label for="filterLabel">Filter Label (Optional)</label>
<input type="text" id="filterLabel" class="clr-input" [(ngModel)]="gadgetsEditdata.filterLabel"
[ngModelOptions]="{standalone: true}" placeholder="Enter filter label">
<div class="clr-subtext">Label to display for this filter in the UI (if not provided, filter key will be
used)</div>
</div>
</div>
<div class="clr-row" style="margin-top: 10px;"
*ngIf="gadgetsEditdata.filterType === 'dropdown' || gadgetsEditdata.filterType === 'multiselect'">
<div class="clr-col-sm-12">
<label for="filterOptions">Filter Options (comma separated)</label>
<input type="text" id="filterOptions" class="clr-input" [(ngModel)]="filterOptionsString"
[ngModelOptions]="{standalone: true}" placeholder="Option1,Option2,Option3">
<div class="clr-subtext">Comma-separated list of options for dropdown/multiselect filters</div>
<div class="clr-subtext" *ngIf="gadgetsEditdata.filterKey">
<strong>Available values for "{{ gadgetsEditdata.filterKey }}":</strong> {{ filterOptionsString }}
</div>
</div>
</div>
</div>
</div>
<div class="clr-row" *ngIf="gadgetsEditdata?.fieldName !== 'Compact Filter'"
style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
<!-- Add Connection Selection Field --> <!-- Add Connection Selection Field -->
<div class="clr-row"> <div class="clr-row">
<div class="clr-col-sm-12"> <div class="clr-col-sm-12">
<label for="connection">Connection</label> <label for="connection">Connection</label>
<select id="connection" formControlName="connection" [(ngModel)]="gadgetsEditdata.connection" class="clr-select"> <select id="connection" formControlName="connection" [(ngModel)]="gadgetsEditdata.connection"
class="clr-select">
<option value="">Select Connection</option> <option value="">Select Connection</option>
<option *ngFor="let conn of sureconnectData" [value]="conn.id"> <option *ngFor="let conn of sureconnectData" [value]="conn.id">
{{conn.connection_name || conn.id}} {{conn.connection_name || conn.id}}
@@ -149,8 +244,8 @@
<div class="clr-col-sm-12"> <div class="clr-col-sm-12">
<label for="table">Api Url</label> <label for="table">Api Url</label>
<div><input type="urk" id="table" formControlName="table" class="clr-input" <div><input type="urk" id="table" formControlName="table" class="clr-input"
[(ngModel)]="gadgetsEditdata.table" style="width:90%">&nbsp;<span><button class="btn btn-icon btn-primary" [(ngModel)]="gadgetsEditdata.table" style="width:90%">&nbsp;<span><button
style="margin: 0px;" (click)="callApi(gadgetsEditdata.table)"> class="btn btn-icon btn-primary" style="margin: 0px;" (click)="callApi(gadgetsEditdata.table)">
<clr-icon shape="redo"></clr-icon> </button></span></div> <clr-icon shape="redo"></clr-icon> </button></span></div>
<!-- <select id="table" formControlName="table" [(ngModel)]="gadgetsEditdata.table" (change)="tablename($event.target.value)"> <!-- <select id="table" formControlName="table" [(ngModel)]="gadgetsEditdata.table" (change)="tablename($event.target.value)">
<option value="null">choose Table</option> <option value="null">choose Table</option>
@@ -223,8 +318,8 @@
</div> </div>
<!-- Add Base Filter Button --> <!-- Add Base Filter Button -->
<button class="btn btn-sm btn-primary" (click)="addBaseFilter()" style="margin-top: 10px; margin-bottom: 10px;" <button class="btn btn-sm btn-primary" (click)="addBaseFilter()"
[disabled]="gadgetsEditdata.commonFilterEnabled"> style="margin-top: 10px; margin-bottom: 10px;" [disabled]="gadgetsEditdata.commonFilterEnabled">
<clr-icon shape="plus"></clr-icon> Add Filter <clr-icon shape="plus"></clr-icon> Add Filter
</button> </button>
@@ -240,19 +335,38 @@
</div> </div>
<div class="clr-row" style="margin-top: 8px;"> <div class="clr-row" style="margin-top: 8px;">
<div class="clr-col-sm-5"> <div class="clr-col-sm-4">
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select" <select [(ngModel)]="filter.field" (ngModelChange)="onBaseFilterFieldChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="gadgetsEditdata.commonFilterEnabled"> [disabled]="gadgetsEditdata.commonFilterEnabled">
<option value="">Select Field</option> <option value="">Select Field</option>
<!-- Base API filters should always use columnData, not drilldownColumnData --> <!-- Base API filters should always use columnData, not drilldownColumnData -->
<option *ngFor="let column of getAvailableFields(gadgetsEditdata.baseFilters, i, columnData)" [value]="column">{{column}}</option> <option *ngFor="let column of getAvailableFields(gadgetsEditdata.baseFilters, i, columnData)"
[value]="column">{{column}}</option>
</select> </select>
</div> </div>
<div class="clr-col-sm-5"> <div class="clr-col-sm-3">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" <select [(ngModel)]="filter.type" (ngModelChange)="onBaseFilterTypeChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
class="clr-input" placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabled">
[disabled]="gadgetsEditdata.commonFilterEnabled"/> <option value="text">Text</option>
<option value="dropdown">Dropdown</option>
<option value="multiselect">Multi-Select</option>
<option value="date-range">Date Range</option>
<option value="toggle">Toggle</option>
</select>
</div>
<div class="clr-col-sm-3" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
<input type="text" [(ngModel)]="filter.options" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Option1,Option2,Option3" [disabled]="gadgetsEditdata.commonFilterEnabled" />
<div class="clr-subtext" *ngIf="filter.availableValues">
Available: {{ filter.availableValues }}
</div>
</div>
<div class="clr-col-sm-3" *ngIf="filter.type !== 'dropdown' && filter.type !== 'multiselect'">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabled" />
</div> </div>
<div class="clr-col-sm-2"> <div class="clr-col-sm-2">
@@ -288,22 +402,25 @@
<label for="drilldownApiUrl">Base Drilldown API URL</label> <label for="drilldownApiUrl">Base Drilldown API URL</label>
<div> <div>
<input type="text" id="drilldownApiUrl" formControlName="drilldownApiUrl" class="clr-input" <input type="text" id="drilldownApiUrl" formControlName="drilldownApiUrl" class="clr-input"
[(ngModel)]="gadgetsEditdata.drilldownApiUrl" style="width:90%" [ngModelOptions]="{standalone: true}">&nbsp; [(ngModel)]="gadgetsEditdata.drilldownApiUrl" style="width:90%"
[ngModelOptions]="{standalone: true}">&nbsp;
<span> <span>
<button class="btn btn-icon btn-primary" style="margin: 0px;" <button class="btn btn-icon btn-primary" style="margin: 0px;" (click)="refreshBaseDrilldownColumns()"
(click)="refreshBaseDrilldownColumns()" [disabled]="!gadgetsEditdata.drilldownApiUrl"> [disabled]="!gadgetsEditdata.drilldownApiUrl">
<clr-icon shape="redo"></clr-icon> <clr-icon shape="redo"></clr-icon>
</button> </button>
</span> </span>
</div> </div>
<div class="clr-subtext">Enter the API URL for base drilldown data. Use angle brackets for parameters, e.g., http://api.example.com/data/&lt;country&gt;</div> <div class="clr-subtext">Enter the API URL for base drilldown data. Use angle brackets for parameters, e.g.,
http://api.example.com/data/&lt;country&gt;</div>
</div> </div>
</div> </div>
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled"> <div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled">
<div class="clr-col-sm-12"> <div class="clr-col-sm-12">
<label for="drilldownXAxis">Base Drilldown X-Axis</label> <label for="drilldownXAxis">Base Drilldown X-Axis</label>
<select id="drilldownXAxis" formControlName="drilldownXAxis" [(ngModel)]="gadgetsEditdata.drilldownXAxis" [ngModelOptions]="{standalone: true}"> <select id="drilldownXAxis" formControlName="drilldownXAxis" [(ngModel)]="gadgetsEditdata.drilldownXAxis"
[ngModelOptions]="{standalone: true}">
<option value="">Select X-Axis Column</option> <option value="">Select X-Axis Column</option>
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option> <option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
</select> </select>
@@ -317,7 +434,8 @@
gadgetsEditdata?.fieldName !== 'To Do Chart'"> gadgetsEditdata?.fieldName !== 'To Do Chart'">
<div class="clr-col-sm-12"> <div class="clr-col-sm-12">
<label for="drilldownYAxis">Base Drilldown Y-Axis</label> <label for="drilldownYAxis">Base Drilldown Y-Axis</label>
<select id="drilldownYAxis" formControlName="drilldownYAxis" [(ngModel)]="gadgetsEditdata.drilldownYAxis" [ngModelOptions]="{standalone: true}"> <select id="drilldownYAxis" formControlName="drilldownYAxis" [(ngModel)]="gadgetsEditdata.drilldownYAxis"
[ngModelOptions]="{standalone: true}">
<option value="">Select Y-Axis Column</option> <option value="">Select Y-Axis Column</option>
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option> <option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
</select> </select>
@@ -329,11 +447,14 @@
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled"> <div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled">
<div class="clr-col-sm-12"> <div class="clr-col-sm-12">
<label for="drilldownParameter">Base Drilldown Parameter</label> <label for="drilldownParameter">Base Drilldown Parameter</label>
<select id="drilldownParameter" [(ngModel)]="gadgetsEditdata.drilldownParameter" [ngModelOptions]="{standalone: true}"> <select id="drilldownParameter" [(ngModel)]="gadgetsEditdata.drilldownParameter"
[ngModelOptions]="{standalone: true}">
<option value="">Select Parameter Column</option> <option value="">Select Parameter Column</option>
<option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option> <option *ngFor="let column of drilldownColumnData" [value]="column">{{column}}</option>
</select> </select>
<div class="clr-subtext">Select the column to use as parameter for URL template replacement in base drilldown</div> <div class="clr-subtext">Select the column to use as parameter for URL template replacement in base
drilldown
</div>
</div> </div>
</div> </div>
@@ -347,16 +468,18 @@
<div class="clr-form-control" style="margin-top: 10px;"> <div class="clr-form-control" style="margin-top: 10px;">
<div class="clr-control-container"> <div class="clr-control-container">
<div class="clr-checkbox-wrapper"> <div class="clr-checkbox-wrapper">
<input type="checkbox" id="commonFilterToggleDrilldown" [(ngModel)]="gadgetsEditdata.commonFilterEnabledDrilldown" <input type="checkbox" id="commonFilterToggleDrilldown"
(change)="onCommonFilterToggleDrilldown()" [ngModelOptions]="{standalone: true}" class="clr-checkbox" /> [(ngModel)]="gadgetsEditdata.commonFilterEnabledDrilldown"
(change)="onCommonFilterToggleDrilldown()" [ngModelOptions]="{standalone: true}"
class="clr-checkbox" />
<label for="commonFilterToggleDrilldown" class="clr-control-label">Use Common Filter</label> <label for="commonFilterToggleDrilldown" class="clr-control-label">Use Common Filter</label>
</div> </div>
</div> </div>
</div> </div>
<!-- Add Drilldown Filter Button --> <!-- Add Drilldown Filter Button -->
<button class="btn btn-sm btn-primary" (click)="addDrilldownFilter()" style="margin-top: 10px; margin-bottom: 10px;" <button class="btn btn-sm btn-primary" (click)="addDrilldownFilter()"
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown"> style="margin-top: 10px; margin-bottom: 10px;" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
<clr-icon shape="plus"></clr-icon> Add Filter <clr-icon shape="plus"></clr-icon> Add Filter
</button> </button>
@@ -372,18 +495,38 @@
</div> </div>
<div class="clr-row" style="margin-top: 8px;"> <div class="clr-row" style="margin-top: 8px;">
<div class="clr-col-sm-5"> <div class="clr-col-sm-4">
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select" <select [(ngModel)]="filter.field" (ngModelChange)="onDrilldownFilterFieldChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown"> [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
<option value="">Select Field</option> <option value="">Select Field</option>
<option *ngFor="let column of getAvailableFields(gadgetsEditdata.drilldownFilters, i, drilldownColumnData)" [value]="column">{{column}}</option> <option
*ngFor="let column of getAvailableFields(gadgetsEditdata.drilldownFilters, i, drilldownColumnData)"
[value]="column">{{column}}</option>
</select> </select>
</div> </div>
<div class="clr-col-sm-5"> <div class="clr-col-sm-3">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" <select [(ngModel)]="filter.type" (ngModelChange)="onDrilldownFilterTypeChange(i, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
class="clr-input" placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown">
[disabled]="gadgetsEditdata.commonFilterEnabledDrilldown"/> <option value="text">Text</option>
<option value="dropdown">Dropdown</option>
<option value="multiselect">Multi-Select</option>
<option value="date-range">Date Range</option>
<option value="toggle">Toggle</option>
</select>
</div>
<div class="clr-col-sm-3" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
<input type="text" [(ngModel)]="filter.options" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Option1,Option2,Option3" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown" />
<div class="clr-subtext" *ngIf="filter.availableValues">
Available: {{ filter.availableValues }}
</div>
</div>
<div class="clr-col-sm-3" *ngIf="filter.type !== 'dropdown' && filter.type !== 'multiselect'">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Filter Value" [disabled]="gadgetsEditdata.commonFilterEnabledDrilldown" />
</div> </div>
<div class="clr-col-sm-2"> <div class="clr-col-sm-2">
@@ -400,7 +543,8 @@
<!-- Multi-Layer Drilldown Configurations --> <!-- Multi-Layer Drilldown Configurations -->
<div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled" style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;"> <div class="clr-row" *ngIf="gadgetsEditdata.drilldownEnabled"
style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
<div class="clr-col-sm-12"> <div class="clr-col-sm-12">
<h4>Multi-Layer Drilldown Configurations</h4> <h4>Multi-Layer Drilldown Configurations</h4>
<button class="btn btn-sm btn-primary" (click)="addDrilldownLayer()"> <button class="btn btn-sm btn-primary" (click)="addDrilldownLayer()">
@@ -412,7 +556,8 @@
<!-- Dynamic Drilldown Layers --> <!-- Dynamic Drilldown Layers -->
<div class="clr-row" *ngFor="let layer of gadgetsEditdata.drilldownLayers; let i = index"> <div class="clr-row" *ngFor="let layer of gadgetsEditdata.drilldownLayers; let i = index">
<div class="clr-col-sm-12" style="margin-top: 15px; padding: 10px; border: 1px solid #eee; border-radius: 4px;"> <div class="clr-col-sm-12"
style="margin-top: 15px; padding: 10px; border: 1px solid #eee; border-radius: 4px;">
<div style="display: flex; justify-content: space-between; align-items: center;"> <div style="display: flex; justify-content: space-between; align-items: center;">
<h5>Drilldown Layer {{i + 1}}</h5> <h5>Drilldown Layer {{i + 1}}</h5>
<button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownLayer(i)"> <button class="btn btn-icon btn-danger btn-sm" (click)="removeDrilldownLayer(i)">
@@ -423,7 +568,8 @@
<div class="clr-form-control"> <div class="clr-form-control">
<div class="clr-control-container"> <div class="clr-control-container">
<div class="clr-checkbox-wrapper"> <div class="clr-checkbox-wrapper">
<input type="checkbox" [id]="'layerEnabled' + i" [(ngModel)]="layer.enabled" class="clr-checkbox" [ngModelOptions]="{standalone: true}" /> <input type="checkbox" [id]="'layerEnabled' + i" [(ngModel)]="layer.enabled" class="clr-checkbox"
[ngModelOptions]="{standalone: true}" />
<label [for]="'layerEnabled' + i" class="clr-control-label">Enable Layer {{i + 1}} Drilldown</label> <label [for]="'layerEnabled' + i" class="clr-control-label">Enable Layer {{i + 1}} Drilldown</label>
</div> </div>
</div> </div>
@@ -433,8 +579,8 @@
<div class="clr-col-sm-12"> <div class="clr-col-sm-12">
<label [for]="'layerApiUrl' + i">Layer {{i + 1}} API URL</label> <label [for]="'layerApiUrl' + i">Layer {{i + 1}} API URL</label>
<div> <div>
<input type="text" [id]="'layerApiUrl' + i" class="clr-input" <input type="text" [id]="'layerApiUrl' + i" class="clr-input" [(ngModel)]="layer.apiUrl"
[(ngModel)]="layer.apiUrl" style="width:90%" [ngModelOptions]="{standalone: true}">&nbsp; style="width:90%" [ngModelOptions]="{standalone: true}">&nbsp;
<span> <span>
<button class="btn btn-icon btn-primary" style="margin: 0px;" <button class="btn btn-icon btn-primary" style="margin: 0px;"
(click)="refreshDrilldownLayerColumns(i)" [disabled]="!layer.apiUrl"> (click)="refreshDrilldownLayerColumns(i)" [disabled]="!layer.apiUrl">
@@ -442,7 +588,8 @@
</button> </button>
</span> </span>
</div> </div>
<div class="clr-subtext">Enter the API URL for layer {{i + 1}} drilldown data. Use angle brackets for parameters, e.g., http://api.example.com/data/&lt;state&gt;</div> <div class="clr-subtext">Enter the API URL for layer {{i + 1}} drilldown data. Use angle brackets for
parameters, e.g., http://api.example.com/data/&lt;state&gt;</div>
</div> </div>
</div> </div>
@@ -478,7 +625,9 @@
<option value="">Select Parameter Column</option> <option value="">Select Parameter Column</option>
<option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option> <option *ngFor="let column of layerColumnData[i] || []" [value]="column">{{column}}</option>
</select> </select>
<div class="clr-subtext">Select the column to use as parameter for URL template replacement in layer {{i + 1}} drilldown</div> <div class="clr-subtext">Select the column to use as parameter for URL template replacement in layer {{i
+
1}} drilldown</div>
</div> </div>
</div> </div>
@@ -492,16 +641,17 @@
<div class="clr-form-control" style="margin-top: 10px;"> <div class="clr-form-control" style="margin-top: 10px;">
<div class="clr-control-container"> <div class="clr-control-container">
<div class="clr-checkbox-wrapper"> <div class="clr-checkbox-wrapper">
<input type="checkbox" [id]="'commonFilterToggleLayer' + i" [(ngModel)]="layer.commonFilterEnabled" <input type="checkbox" [id]="'commonFilterToggleLayer' + i"
(change)="onCommonFilterToggleLayer(i)" [ngModelOptions]="{standalone: true}" class="clr-checkbox" /> [(ngModel)]="layer.commonFilterEnabled" (change)="onCommonFilterToggleLayer(i)"
[ngModelOptions]="{standalone: true}" class="clr-checkbox" />
<label [for]="'commonFilterToggleLayer' + i" class="clr-control-label">Use Common Filter</label> <label [for]="'commonFilterToggleLayer' + i" class="clr-control-label">Use Common Filter</label>
</div> </div>
</div> </div>
</div> </div>
<!-- Add Layer Filter Button --> <!-- Add Layer Filter Button -->
<button class="btn btn-sm btn-primary" (click)="addLayerFilter(i)" style="margin-top: 10px; margin-bottom: 10px;" <button class="btn btn-sm btn-primary" (click)="addLayerFilter(i)"
[disabled]="layer.commonFilterEnabled"> style="margin-top: 10px; margin-bottom: 10px;" [disabled]="layer.commonFilterEnabled">
<clr-icon shape="plus"></clr-icon> Add Filter <clr-icon shape="plus"></clr-icon> Add Filter
</button> </button>
@@ -517,18 +667,37 @@
</div> </div>
<div class="clr-row" style="margin-top: 8px;"> <div class="clr-row" style="margin-top: 8px;">
<div class="clr-col-sm-5"> <div class="clr-col-sm-4">
<select [(ngModel)]="filter.field" [ngModelOptions]="{standalone: true}" class="clr-select" <select [(ngModel)]="filter.field" (ngModelChange)="onLayerFilterFieldChange(i, j, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
[disabled]="layer.commonFilterEnabled"> [disabled]="layer.commonFilterEnabled">
<option value="">Select Field</option> <option value="">Select Field</option>
<option *ngFor="let column of getAvailableFields(layer.filters, j, layerColumnData[i] || [])" [value]="column">{{column}}</option> <option *ngFor="let column of getAvailableFields(layer.filters, j, layerColumnData[i] || [])"
[value]="column">{{column}}</option>
</select> </select>
</div> </div>
<div class="clr-col-sm-5"> <div class="clr-col-sm-3">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" <select [(ngModel)]="filter.type" (ngModelChange)="onLayerFilterTypeChange(i, j, $event)" [ngModelOptions]="{standalone: true}" class="clr-select"
class="clr-input" placeholder="Filter Value" [disabled]="layer.commonFilterEnabled">
[disabled]="layer.commonFilterEnabled"/> <option value="text">Text</option>
<option value="dropdown">Dropdown</option>
<option value="multiselect">Multi-Select</option>
<option value="date-range">Date Range</option>
<option value="toggle">Toggle</option>
</select>
</div>
<div class="clr-col-sm-3" *ngIf="filter.type === 'dropdown' || filter.type === 'multiselect'">
<input type="text" [(ngModel)]="filter.options" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Option1,Option2,Option3" [disabled]="layer.commonFilterEnabled" />
<div class="clr-subtext" *ngIf="filter.availableValues">
Available: {{ filter.availableValues }}
</div>
</div>
<div class="clr-col-sm-3" *ngIf="filter.type !== 'dropdown' && filter.type !== 'multiselect'">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
placeholder="Filter Value" [disabled]="layer.commonFilterEnabled" />
</div> </div>
<div class="clr-col-sm-2"> <div class="clr-col-sm-2">
@@ -550,6 +719,9 @@
<input id="chartparameter" type="text" formControlName="chartparameter" class="clr-input" [(ngModel)]="gadgetsEditdata.chartparameter"> <input id="chartparameter" type="text" formControlName="chartparameter" class="clr-input" [(ngModel)]="gadgetsEditdata.chartparameter">
</div> </div>
</div> --> </div> -->
</div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="modeledit = false">Cancel</button> <button type="button" class="btn btn-outline" (click)="modeledit = false">Cancel</button>
<button type="button" class="btn btn-primary" (click)="applyChanges(modelid)">Apply</button> <button type="button" class="btn btn-primary" (click)="applyChanges(modelid)">Apply</button>
@@ -568,7 +740,8 @@
<div class="clr-row"> <div class="clr-row">
<div class="clr-col-sm-12"> <div class="clr-col-sm-12">
<label for="commonFilterConnection">Connection</label> <label for="commonFilterConnection">Connection</label>
<select id="commonFilterConnection" formControlName="connection" [(ngModel)]="commonFilterData.connection" class="clr-select"> <select id="commonFilterConnection" formControlName="connection" [(ngModel)]="commonFilterData.connection"
class="clr-select">
<option value="">Select Connection</option> <option value="">Select Connection</option>
<option *ngFor="let conn of sureconnectData" [value]="conn.id"> <option *ngFor="let conn of sureconnectData" [value]="conn.id">
{{conn.connection_name || conn.id}} {{conn.connection_name || conn.id}}
@@ -584,8 +757,8 @@
<input type="text" id="commonFilterApiUrl" formControlName="apiUrl" class="clr-input" <input type="text" id="commonFilterApiUrl" formControlName="apiUrl" class="clr-input"
[(ngModel)]="commonFilterData.apiUrl" style="width:90%">&nbsp; [(ngModel)]="commonFilterData.apiUrl" style="width:90%">&nbsp;
<span> <span>
<button class="btn btn-icon btn-primary" style="margin: 0px;" <button class="btn btn-icon btn-primary" style="margin: 0px;" (click)="refreshCommonFilterColumns()"
(click)="refreshCommonFilterColumns()" [disabled]="!commonFilterData.apiUrl"> [disabled]="!commonFilterData.apiUrl">
<clr-icon shape="redo"></clr-icon> <clr-icon shape="redo"></clr-icon>
</button> </button>
</span> </span>
@@ -599,7 +772,8 @@
<h5>Common Filters</h5> <h5>Common Filters</h5>
<!-- Add Common Filter Button --> <!-- Add Common Filter Button -->
<button class="btn btn-sm btn-primary" (click)="addCommonFilter()" style="margin-top: 10px; margin-bottom: 10px;"> <button class="btn btn-sm btn-primary" (click)="addCommonFilter()"
style="margin-top: 10px; margin-bottom: 10px;">
<clr-icon shape="plus"></clr-icon> Add Filter <clr-icon shape="plus"></clr-icon> Add Filter
</button> </button>
@@ -622,8 +796,8 @@
</div> </div>
<div class="clr-col-sm-5"> <div class="clr-col-sm-5">
<input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" <input type="text" [(ngModel)]="filter.value" [ngModelOptions]="{standalone: true}" class="clr-input"
class="clr-input" placeholder="Filter Value" /> placeholder="Filter Value" />
</div> </div>
<div class="clr-col-sm-2"> <div class="clr-col-sm-2">

View File

@@ -48,6 +48,12 @@ export class EditnewdashComponent implements OnInit {
public entryForm: FormGroup; public entryForm: FormGroup;
public commonFilterForm: FormGroup; // Add common filter form public commonFilterForm: FormGroup; // Add common filter form
// Add filterOptionsString property for compact filter
filterOptionsString: string = '';
// Add availableKeys property for compact filter
availableKeys: string[] = [];
WidgetsMock: WidgetModel[] = [ WidgetsMock: WidgetModel[] = [
{ {
name: 'Common Filter', name: 'Common Filter',
@@ -85,14 +91,14 @@ export class EditnewdashComponent implements OnInit {
name: 'Scatter Chart', name: 'Scatter Chart',
identifier: 'scatter_chart' identifier: 'scatter_chart'
}, },
// { {
// name: 'Dynamic Chart', name: 'Dynamic Chart',
// identifier: 'dynamic_chart' identifier: 'dynamic_chart'
// }, },
// { {
// name: 'Financial Chart', name: 'Financial Chart',
// identifier: 'financial_chart' identifier: 'financial_chart'
// }, },
{ {
name: 'To Do', name: 'To Do',
identifier: 'to_do_chart' identifier: 'to_do_chart'
@@ -360,6 +366,16 @@ export class EditnewdashComponent implements OnInit {
dashboard.component = component.componentInstance; dashboard.component = component.componentInstance;
} }
}); });
// Ensure compact filter configuration properties are properly initialized
if (dashboard.component === 'Compact Filter' || dashboard.name === 'Compact Filter') {
// Make sure all compact filter properties exist
if (dashboard.filterKey === undefined) dashboard.filterKey = '';
if (dashboard.filterType === undefined) dashboard.filterType = 'text';
if (dashboard.filterLabel === undefined) dashboard.filterLabel = '';
if (dashboard.filterOptions === undefined) dashboard.filterOptions = [];
// table and connection properties should already exist for all components
}
}); });
} }
@@ -374,6 +390,16 @@ export class EditnewdashComponent implements OnInit {
dashboard.component = component.name; dashboard.component = component.name;
} }
}); });
// Ensure compact filter configuration properties are preserved
if (dashboard.name === 'Compact Filter') {
// Make sure all compact filter properties exist
if (dashboard.filterKey === undefined) dashboard.filterKey = '';
if (dashboard.filterType === undefined) dashboard.filterType = 'text';
if (dashboard.filterLabel === undefined) dashboard.filterLabel = '';
if (dashboard.filterOptions === undefined) dashboard.filterOptions = [];
// table and connection properties should already exist for all components
}
}); });
} }
// Add method to get available fields for a filter dropdown (excluding already selected fields) // Add method to get available fields for a filter dropdown (excluding already selected fields)
@@ -571,6 +597,7 @@ export class EditnewdashComponent implements OnInit {
} }
modelid: number; modelid: number;
// Update the editGadget method to initialize filter properties
editGadget(item) { editGadget(item) {
this.modeledit = true; this.modeledit = true;
this.modelid = item.chartid; this.modelid = item.chartid;
@@ -601,6 +628,77 @@ export class EditnewdashComponent implements OnInit {
if (item['filterOptions'] === undefined) { if (item['filterOptions'] === undefined) {
this.gadgetsEditdata['filterOptions'] = []; this.gadgetsEditdata['filterOptions'] = [];
} }
// Initialize filterOptionsString for compact filter
if (item.name === 'Compact Filter') {
this.filterOptionsString = this.gadgetsEditdata['filterOptions'].join(', ');
// Load available keys when editing a compact filter
if (this.gadgetsEditdata['table']) {
this.loadAvailableKeys(this.gadgetsEditdata['table'], this.gadgetsEditdata['connection']);
}
} else {
this.filterOptionsString = '';
}
// Initialize base filters with type and options if not present
if (item['baseFilters'] === undefined) {
this.gadgetsEditdata['baseFilters'] = [];
} else {
// Ensure each base filter has type and options properties
this.gadgetsEditdata['baseFilters'] = this.gadgetsEditdata['baseFilters'].map(filter => ({
field: filter.field || '',
value: filter.value || '',
type: filter.type || 'text',
options: filter.options || '',
availableValues: filter.availableValues || ''
}));
}
// Initialize drilldown filters with type and options if not present
if (item['drilldownFilters'] === undefined) {
this.gadgetsEditdata['drilldownFilters'] = [];
} else {
// Ensure each drilldown filter has type and options properties
this.gadgetsEditdata['drilldownFilters'] = this.gadgetsEditdata['drilldownFilters'].map(filter => ({
field: filter.field || '',
value: filter.value || '',
type: filter.type || 'text',
options: filter.options || '',
availableValues: filter.availableValues || ''
}));
}
// Initialize drilldown layers with proper filter structure if not present
if (item['drilldownLayers'] === undefined) {
this.gadgetsEditdata['drilldownLayers'] = [];
} else {
// Ensure each layer has proper filter structure
this.gadgetsEditdata['drilldownLayers'] = this.gadgetsEditdata['drilldownLayers'].map(layer => {
// Initialize parameter if not present
if (layer['parameter'] === undefined) {
layer['parameter'] = '';
}
// Initialize filters if not present
if (layer['filters'] === undefined) {
layer['filters'] = [];
} else {
// Ensure each layer filter has type and options properties
layer['filters'] = layer['filters'].map(filter => ({
field: filter.field || '',
value: filter.value || '',
type: filter.type || 'text',
options: filter.options || '',
availableValues: filter.availableValues || ''
}));
}
// Initialize common filter property for layer if not present
if (layer['commonFilterEnabled'] === undefined) {
layer['commonFilterEnabled'] = false;
}
return layer;
});
}
this.getStores(); this.getStores();
// Set default connection if none is set and we have connections // Set default connection if none is set and we have connections
@@ -628,37 +726,6 @@ export class EditnewdashComponent implements OnInit {
this.gadgetsEditdata['drilldownParameter'] = ''; this.gadgetsEditdata['drilldownParameter'] = '';
} }
// Initialize base filters if not present
if (item['baseFilters'] === undefined) {
this.gadgetsEditdata['baseFilters'] = [];
}
// Initialize drilldown filters if not present
if (item['drilldownFilters'] === undefined) {
this.gadgetsEditdata['drilldownFilters'] = [];
}
// Initialize drilldown layers if not present
if (item['drilldownLayers'] === undefined) {
this.gadgetsEditdata['drilldownLayers'] = [];
} else {
// Ensure each layer has proper structure (removed parameterKey, added parameter)
this.gadgetsEditdata['drilldownLayers'].forEach((layer, index) => {
// Initialize parameter if not present
if (layer['parameter'] === undefined) {
layer['parameter'] = '';
}
// Initialize filters if not present
if (layer['filters'] === undefined) {
layer['filters'] = [];
}
// Initialize common filter property for layer if not present
if (layer['commonFilterEnabled'] === undefined) {
layer['commonFilterEnabled'] = false;
}
});
}
// Reset drilldown column data // Reset drilldown column data
this.drilldownColumnData = []; this.drilldownColumnData = [];
@@ -699,6 +766,9 @@ export class EditnewdashComponent implements OnInit {
//https://www.w3schools.com/js/tryit.asp?filename=tryjson_stringify_function_tostring //https://www.w3schools.com/js/tryit.asp?filename=tryjson_stringify_function_tostring
// First serialize the dashboard collection to ensure component names are properly set
this.serialize(this.dashboardCollection.dashboard);
let cmp = this.dashboardCollection.dashboard.forEach(dashboard => { let cmp = this.dashboardCollection.dashboard.forEach(dashboard => {
this.componentCollection.forEach(component => { this.componentCollection.forEach(component => {
if (dashboard.name === component.name) { if (dashboard.name === component.name) {
@@ -719,8 +789,6 @@ export class EditnewdashComponent implements OnInit {
//console.log(merged); //console.log(merged);
console.log("temp data", typeof tmp); console.log("temp data", typeof tmp);
console.log(tmp); console.log(tmp);
let parsed = JSON.parse(tmp);
this.serialize(parsed.dashboard);
this.dashbord1_Line.model = tmp; this.dashbord1_Line.model = tmp;
// let obj = this.dashboardCollection; // let obj = this.dashboardCollection;
@@ -745,6 +813,7 @@ export class EditnewdashComponent implements OnInit {
// } // }
} }
// Update the onSubmit method to properly save filter data
onSubmit(id) { onSubmit(id) {
console.log(id); console.log(id);
if (!isNullArray(this.selectedyAxis)) { if (!isNullArray(this.selectedyAxis)) {
@@ -777,12 +846,19 @@ export class EditnewdashComponent implements OnInit {
xyz.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property xyz.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property
// For compact filter, preserve filter configuration properties // For compact filter, preserve filter configuration properties
if (item.component && item.component.name === 'CompactFilterComponent') { if (item.name === 'Compact Filter') {
xyz.filterKey = this.gadgetsEditdata.filterKey || ''; xyz.filterKey = this.gadgetsEditdata.filterKey || '';
xyz.filterType = this.gadgetsEditdata.filterType || 'text'; xyz.filterType = this.gadgetsEditdata.filterType || 'text';
xyz.filterLabel = this.gadgetsEditdata.filterLabel || ''; xyz.filterLabel = this.gadgetsEditdata.filterLabel || '';
// Convert filterOptionsString to array
if (this.gadgetsEditdata.fieldName === 'Compact Filter') {
xyz.filterOptions = this.filterOptionsString.split(',').map(opt => opt.trim()).filter(opt => opt);
} else {
xyz.filterOptions = this.gadgetsEditdata.filterOptions || []; xyz.filterOptions = this.gadgetsEditdata.filterOptions || [];
} }
xyz.table = this.gadgetsEditdata.table || '';
xyz.connection = this.gadgetsEditdata.connection || undefined;
}
console.log(xyz); console.log(xyz);
return xyz; return xyz;
@@ -818,8 +894,58 @@ export class EditnewdashComponent implements OnInit {
* This prevents errors when trying to set properties that don't exist on the components * This prevents errors when trying to set properties that don't exist on the components
*/ */
getChartInputs(item: any): any { getChartInputs(item: any): any {
// Only pass properties that are relevant to chart components // For CompactFilterComponent, pass only filter configuration properties
const chartInputs = { if (item.name === 'Compact Filter') {
const filterInputs = {
filterKey: item['filterKey'] || '',
filterType: item['filterType'] || 'text',
filterLabel: item['filterLabel'] || '',
filterOptions: item['filterOptions'] || [],
apiUrl: item['table'] || '', // Use table as API URL
connectionId: item['connection'] ? parseInt(item['connection'], 10) : undefined
};
// Preserve configuration in the item itself
item['filterKey'] = filterInputs['filterKey'];
item['filterType'] = filterInputs['filterType'];
item['filterLabel'] = filterInputs['filterLabel'];
item['filterOptions'] = filterInputs['filterOptions'];
item['table'] = filterInputs['apiUrl'];
item['connection'] = item['connection'];
// Remove undefined properties to avoid passing unnecessary data
Object.keys(filterInputs).forEach(key => {
if (filterInputs[key] === undefined) {
delete filterInputs[key];
}
});
return filterInputs;
}
// For CommonFilterComponent, pass only filter-related properties
if (item.component && item.component.name === 'CommonFilterComponent') {
const commonFilterInputs = {
baseFilters: item['baseFilters'] || [],
drilldownFilters: item['drilldownFilters'] || [],
drilldownLayers: item['drilldownLayers'] || [],
fieldName: item['name'] || '',
connection: item['connection'] || undefined
};
// Remove undefined properties to avoid passing unnecessary data
Object.keys(commonFilterInputs).forEach(key => {
if (commonFilterInputs[key] === undefined) {
delete commonFilterInputs[key];
}
});
return commonFilterInputs;
}
// For GridViewComponent, pass chart properties with drilldown support
if (item.component && item.component.name === 'GridViewComponent') {
const gridInputs = {
xAxis: item.xAxis, xAxis: item.xAxis,
yAxis: item.yAxis, yAxis: item.yAxis,
table: item.table, table: item.table,
@@ -848,24 +974,45 @@ export class EditnewdashComponent implements OnInit {
drilldownLayers: item['drilldownLayers'] || [] drilldownLayers: item['drilldownLayers'] || []
}; };
// For CommonFilterComponent, also pass baseFilters, drilldownFilters, drilldownLayers, fieldName, and connection // Remove undefined properties to avoid passing unnecessary data
if (item.component && item.component.name === 'CommonFilterComponent') { Object.keys(gridInputs).forEach(key => {
chartInputs['baseFilters'] = item['baseFilters'] || []; if (gridInputs[key] === undefined) {
chartInputs['drilldownFilters'] = item['drilldownFilters'] || []; delete gridInputs[key];
chartInputs['drilldownLayers'] = item['drilldownLayers'] || []; }
chartInputs['fieldName'] = item['name'] || ''; });
chartInputs['connection'] = item['connection'] || undefined;
return gridInputs;
} }
// For CompactFilterComponent, pass filter configuration properties // For all other chart components, pass chart-specific properties
if (item.component && item.component.name === 'CompactFilterComponent') { const chartInputs = {
chartInputs['filterKey'] = item['filterKey'] || ''; xAxis: item.xAxis,
chartInputs['filterType'] = item['filterType'] || 'text'; yAxis: item.yAxis,
chartInputs['filterLabel'] = item['filterLabel'] || ''; table: item.table,
chartInputs['filterOptions'] = item['filterOptions'] || []; datastore: item.datastore,
chartInputs['apiUrl'] = item['table'] || ''; // Use table as API URL charttitle: item.charttitle,
chartInputs['connectionId'] = item['connection'] ? parseInt(item['connection'], 10) : undefined; chartlegend: item.chartlegend,
} showlabel: item.showlabel,
chartcolor: item.chartcolor,
slices: item.slices,
donut: item.donut,
charturl: item.charturl,
chartparameter: item.chartparameter,
datasource: item.datasource,
fieldName: item.name, // Using item.name as fieldName
connection: item['connection'], // Add connection field using bracket notation
// Base drilldown configuration properties
drilldownEnabled: item['drilldownEnabled'],
drilldownApiUrl: item['drilldownApiUrl'],
// Removed drilldownParameterKey since we're using URL templates
drilldownXAxis: item['drilldownXAxis'],
drilldownYAxis: item['drilldownYAxis'],
drilldownParameter: item['drilldownParameter'], // Add drilldown parameter
baseFilters: item['baseFilters'] || [], // Add base filters with type information
drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters with type information
// Multi-layer drilldown configurations
drilldownLayers: item['drilldownLayers'] || []
};
// Remove undefined properties to avoid passing unnecessary data // Remove undefined properties to avoid passing unnecessary data
Object.keys(chartInputs).forEach(key => { Object.keys(chartInputs).forEach(key => {
@@ -877,6 +1024,7 @@ export class EditnewdashComponent implements OnInit {
return chartInputs; return chartInputs;
} }
// Update the applyChanges method to properly save filter data
applyChanges(id) { applyChanges(id) {
console.log('Apply changes for chart ID:', id); console.log('Apply changes for chart ID:', id);
@@ -922,13 +1070,24 @@ export class EditnewdashComponent implements OnInit {
updatedItem.commonFilterEnabledDrilldown = this.gadgetsEditdata.commonFilterEnabledDrilldown; // Add drilldown common filter property updatedItem.commonFilterEnabledDrilldown = this.gadgetsEditdata.commonFilterEnabledDrilldown; // Add drilldown common filter property
// For compact filter, preserve filter configuration properties // For compact filter, preserve filter configuration properties
if (item.component && item.component.name === 'CompactFilterComponent') { if (item.name === 'Compact Filter') {
updatedItem.filterKey = this.gadgetsEditdata.filterKey || ''; updatedItem.filterKey = this.gadgetsEditdata.filterKey || '';
updatedItem.filterType = this.gadgetsEditdata.filterType || 'text'; updatedItem.filterType = this.gadgetsEditdata.filterType || 'text';
updatedItem.filterLabel = this.gadgetsEditdata.filterLabel || ''; updatedItem.filterLabel = this.gadgetsEditdata.filterLabel || '';
// Convert filterOptionsString to array
if (this.gadgetsEditdata.fieldName === 'Compact Filter') {
updatedItem.filterOptions = this.filterOptionsString.split(',').map(opt => opt.trim()).filter(opt => opt);
} else {
updatedItem.filterOptions = this.gadgetsEditdata.filterOptions || []; updatedItem.filterOptions = this.gadgetsEditdata.filterOptions || [];
}
updatedItem.table = this.gadgetsEditdata.table || ''; // API URL updatedItem.table = this.gadgetsEditdata.table || ''; // API URL
updatedItem.connection = this.gadgetsEditdata.connection || undefined; // Connection ID updatedItem.connection = this.gadgetsEditdata.connection || undefined; // Connection ID
// Also preserve these properties in gadgetsEditdata for consistency
this.gadgetsEditdata.filterKey = updatedItem.filterKey;
this.gadgetsEditdata.filterType = updatedItem.filterType;
this.gadgetsEditdata.filterLabel = updatedItem.filterLabel;
this.gadgetsEditdata.filterOptions = updatedItem.filterOptions;
} }
console.log('Updated item:', updatedItem); console.log('Updated item:', updatedItem);
@@ -1165,46 +1324,239 @@ export class EditnewdashComponent implements OnInit {
// We're now using removeBaseFilter and removeLayerFilter methods instead // We're now using removeBaseFilter and removeLayerFilter methods instead
} }
// Add method to add a base filter // Add method to handle base filter field change
onBaseFilterFieldChange(index: number, field: string) {
const filter = this.gadgetsEditdata.baseFilters[index];
if (filter) {
filter.field = field;
// If field changes, reset value and options
filter.value = '';
filter.options = '';
filter.availableValues = '';
// If we have a field and table URL, load available values
if (field && this.gadgetsEditdata.table) {
this.loadFilterValuesForField(
this.gadgetsEditdata.table,
this.gadgetsEditdata.connection,
field,
index,
'base'
);
}
}
}
// Add method to handle base filter type change
onBaseFilterTypeChange(index: number, type: string) {
const filter = this.gadgetsEditdata.baseFilters[index];
if (filter) {
filter.type = type;
// If type changes to dropdown/multiselect and we have a field, load available values
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.gadgetsEditdata.table) {
this.loadFilterValuesForField(
this.gadgetsEditdata.table,
this.gadgetsEditdata.connection,
filter.field,
index,
'base'
);
}
}
}
// Add method to handle drilldown filter field change
onDrilldownFilterFieldChange(index: number, field: string) {
const filter = this.gadgetsEditdata.drilldownFilters[index];
if (filter) {
filter.field = field;
// If field changes, reset value and options
filter.value = '';
filter.options = '';
filter.availableValues = '';
// If we have a field and drilldown API URL, load available values
if (field && this.gadgetsEditdata.drilldownApiUrl) {
this.loadFilterValuesForField(
this.gadgetsEditdata.drilldownApiUrl,
this.gadgetsEditdata.connection,
field,
index,
'drilldown'
);
}
}
}
// Add method to handle drilldown filter type change
onDrilldownFilterTypeChange(index: number, type: string) {
const filter = this.gadgetsEditdata.drilldownFilters[index];
if (filter) {
filter.type = type;
// If type changes to dropdown/multiselect and we have a field, load available values
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.gadgetsEditdata.drilldownApiUrl) {
this.loadFilterValuesForField(
this.gadgetsEditdata.drilldownApiUrl,
this.gadgetsEditdata.connection,
filter.field,
index,
'drilldown'
);
}
}
}
// Add method to handle layer filter field change
onLayerFilterFieldChange(layerIndex: number, filterIndex: number, field: string) {
const layer = this.gadgetsEditdata.drilldownLayers[layerIndex];
if (layer && layer.filters) {
const filter = layer.filters[filterIndex];
if (filter) {
filter.field = field;
// If field changes, reset value and options
filter.value = '';
filter.options = '';
filter.availableValues = '';
// If we have a field and layer API URL, load available values
if (field && layer.apiUrl) {
this.loadFilterValuesForField(
layer.apiUrl,
this.gadgetsEditdata.connection,
field,
filterIndex,
'layer',
layerIndex
);
}
}
}
}
// Add method to handle layer filter type change
onLayerFilterTypeChange(layerIndex: number, filterIndex: number, type: string) {
const layer = this.gadgetsEditdata.drilldownLayers[layerIndex];
if (layer && layer.filters) {
const filter = layer.filters[filterIndex];
if (filter) {
filter.type = type;
// If type changes to dropdown/multiselect and we have a field, load available values
if ((type === 'dropdown' || type === 'multiselect') && filter.field && layer.apiUrl) {
this.loadFilterValuesForField(
layer.apiUrl,
this.gadgetsEditdata.connection,
filter.field,
filterIndex,
'layer',
layerIndex
);
}
}
}
}
// Add method to load filter values for a specific field
loadFilterValuesForField(
apiUrl: string,
connectionId: string | undefined,
field: string,
filterIndex: number,
filterType: 'base' | 'drilldown' | 'layer',
layerIndex?: number
) {
if (apiUrl && field) {
const connectionIdNum = connectionId ? parseInt(connectionId, 10) : undefined;
this.alertService.getValuesFromUrl(apiUrl, connectionIdNum, field).subscribe(
(values: string[]) => {
// Update the filter with available values
if (filterType === 'base') {
const filter = this.gadgetsEditdata.baseFilters[filterIndex];
if (filter) {
filter.availableValues = values.join(', ');
// For dropdown/multiselect types, also update the options
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
filter.options = filter.availableValues;
}
}
} else if (filterType === 'drilldown') {
const filter = this.gadgetsEditdata.drilldownFilters[filterIndex];
if (filter) {
filter.availableValues = values.join(', ');
// For dropdown/multiselect types, also update the options
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
filter.options = filter.availableValues;
}
}
} else if (filterType === 'layer' && layerIndex !== undefined) {
const layer = this.gadgetsEditdata.drilldownLayers[layerIndex];
if (layer && layer.filters) {
const filter = layer.filters[filterIndex];
if (filter) {
filter.availableValues = values.join(', ');
// For dropdown/multiselect types, also update the options
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
filter.options = filter.availableValues;
}
}
}
}
},
(error) => {
console.error('Error loading available values for field:', field, error);
}
);
}
}
// Add method to add a base filter with default properties
addBaseFilter() { addBaseFilter() {
const newFilter = { const newFilter = {
field: '', field: '',
value: '' value: '',
type: 'text',
options: '',
availableValues: ''
}; };
this.gadgetsEditdata.baseFilters.push(newFilter); this.gadgetsEditdata.baseFilters.push(newFilter);
} }
// Add method to add a drilldown filter with default properties
addDrilldownFilter() {
const newFilter = {
field: '',
value: '',
type: 'text',
options: '',
availableValues: ''
};
this.gadgetsEditdata.drilldownFilters.push(newFilter);
}
// Add method to add a layer filter with default properties
addLayerFilter(layerIndex: number) {
const newFilter = {
field: '',
value: '',
type: 'text',
options: '',
availableValues: ''
};
if (!this.gadgetsEditdata.drilldownLayers[layerIndex].filters) {
this.gadgetsEditdata.drilldownLayers[layerIndex].filters = [];
}
this.gadgetsEditdata.drilldownLayers[layerIndex].filters.push(newFilter);
}
// Add method to remove a base filter // Add method to remove a base filter
removeBaseFilter(index: number) { removeBaseFilter(index: number) {
this.gadgetsEditdata.baseFilters.splice(index, 1); this.gadgetsEditdata.baseFilters.splice(index, 1);
} }
// Add method to add a drilldown filter
addDrilldownFilter() {
const newFilter = {
field: '',
value: ''
};
this.gadgetsEditdata.drilldownFilters.push(newFilter);
}
// Add method to remove a drilldown filter // Add method to remove a drilldown filter
removeDrilldownFilter(index: number) { removeDrilldownFilter(index: number) {
this.gadgetsEditdata.drilldownFilters.splice(index, 1); this.gadgetsEditdata.drilldownFilters.splice(index, 1);
} }
// Add method to add a layer filter
addLayerFilter(layerIndex: number) {
const newFilter = {
field: '',
value: ''
};
if (!this.gadgetsEditdata.drilldownLayers[layerIndex].filters) {
this.gadgetsEditdata.drilldownLayers[layerIndex].filters = [];
}
this.gadgetsEditdata.drilldownLayers[layerIndex].filters.push(newFilter);
}
// Add method to remove a layer filter // Add method to remove a layer filter
removeLayerFilter(layerIndex: number, filterIndex: number) { removeLayerFilter(layerIndex: number, filterIndex: number) {
this.gadgetsEditdata.drilldownLayers[layerIndex].filters.splice(filterIndex, 1); this.gadgetsEditdata.drilldownLayers[layerIndex].filters.splice(filterIndex, 1);
@@ -1356,4 +1708,80 @@ export class EditnewdashComponent implements OnInit {
// This would require the chart component to have a public resize method // This would require the chart component to have a public resize method
} }
} }
// Add method to load available keys for compact filter
loadAvailableKeys(apiUrl: string, connectionId: string | undefined) {
if (apiUrl) {
const connectionIdNum = connectionId ? parseInt(connectionId, 10) : undefined;
this.alertService.getColumnfromurl(apiUrl, connectionIdNum).subscribe(
(keys: string[]) => {
this.availableKeys = keys;
},
(error) => {
console.error('Error loading available keys:', error);
this.availableKeys = [];
}
);
}
}
// Add method to load available values for a specific key
loadAvailableValues(key: string) {
if (key && this.gadgetsEditdata['table']) {
const connectionIdNum = this.gadgetsEditdata['connection'] ?
parseInt(this.gadgetsEditdata['connection'], 10) : undefined;
this.alertService.getValuesFromUrl(this.gadgetsEditdata['table'], connectionIdNum, key).subscribe(
(values: string[]) => {
// Update filter options string for dropdown/multiselect
if (this.gadgetsEditdata['filterType'] === 'dropdown' ||
this.gadgetsEditdata['filterType'] === 'multiselect') {
this.filterOptionsString = values.join(', ');
// Also update the gadgetsEditdata filterOptions array
this.gadgetsEditdata['filterOptions'] = values;
}
},
(error) => {
console.error('Error loading available values:', error);
}
);
}
}
// Add method to handle filter key change
onFilterKeyChange(key: string) {
this.gadgetsEditdata['filterKey'] = key;
// Load available values when filter key changes
if (key && (this.gadgetsEditdata['filterType'] === 'dropdown' ||
this.gadgetsEditdata['filterType'] === 'multiselect')) {
this.loadAvailableValues(key);
}
}
// Add method to handle filter type change
onFilterTypeChange(type: string) {
this.gadgetsEditdata['filterType'] = type;
// Load available values when filter type changes to dropdown or multiselect
if ((type === 'dropdown' || type === 'multiselect') && this.gadgetsEditdata['filterKey']) {
this.loadAvailableValues(this.gadgetsEditdata['filterKey']);
}
}
// Add method to handle API URL change for compact filter
onCompactFilterApiUrlChange(url: string) {
this.gadgetsEditdata['table'] = url;
// Load available keys when API URL changes
if (url) {
this.loadAvailableKeys(url, this.gadgetsEditdata['connection']);
}
}
// Add method to handle connection change for compact filter
onCompactFilterConnectionChange(connectionId: string) {
this.gadgetsEditdata['connection'] = connectionId;
// Reload available keys when connection changes
if (this.gadgetsEditdata['table']) {
this.loadAvailableKeys(this.gadgetsEditdata['table'], connectionId);
}
}
} }

View File

@@ -1,38 +1,296 @@
<div class="chart-container"> <div class="chart-container">
<!-- Compact Filters --> <!-- Filter Controls Section -->
<div class="compact-filters-container" *ngIf="baseFilters && baseFilters.length > 0"> <div class="filter-section" *ngIf="hasActiveFilters()">
<app-compact-filter <!-- Base Filters -->
*ngFor="let filter of baseFilters" <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
[filterKey]="filter.field" <h4>Base Filters</h4>
(filterChange)="onFilterChange($event)"> <div class="filter-controls">
</app-compact-filter> <div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div> </div>
<!-- Drilldown mode indicator --> <!-- Dropdown Filter -->
<div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator"> <div *ngIf="filter.type === 'dropdown'" class="filter-input">
<span class="drilldown-text">Drilldown Level: {{currentDrilldownLevel}}</span> <select [(ngModel)]="filter.value"
<button class="btn btn-secondary btn-sm" (click)="navigateBack()"> (ngModelChange)="onBaseFilterChange(filter)"
Back to Level {{currentDrilldownLevel - 1}} class="clr-select filter-select">
</button> <option value="">Select {{ filter.field }}</option>
<button class="btn btn-danger btn-sm" (click)="resetToOriginalData()"> <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
Back to Main View </select>
</button>
</div> </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="chart-header"> <div class="chart-header">
<h3>{{ charttitle || 'Bar Chart' }}</h3> <div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Bar 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> </div>
<div class="chart-wrapper"> <div class="chart-wrapper">
<div class="chart-content" [class.loading]="barChartData.length === 0 && barChartLabels.length === 0 && !noDataAvailable"> <div class="chart-content" [class.loading]="isLoading">
<!-- No data message -->
<div class="no-data-message" *ngIf="noDataAvailable"> <div *ngIf="noDataAvailable" class="no-data-message">
<p>No data available</p> No data available
</div> </div>
<!-- Chart display -->
<div *ngIf="!noDataAvailable" class="chart-display">
<canvas baseChart <canvas baseChart
*ngIf="!noDataAvailable"
[datasets]="barChartData" [datasets]="barChartData"
[labels]="barChartLabels" [labels]="barChartLabels"
[type]="barChartType" [type]="barChartType"
@@ -40,11 +298,37 @@
(chartHover)="chartHovered($event)" (chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)"> (chartClick)="chartClicked($event)">
</canvas> </canvas>
</div>
<!-- Loading overlay --> <div class="loading-overlay" *ngIf="isLoading">
<div class="loading-overlay" *ngIf="barChartData.length === 0 && barChartLabels.length === 0 && !noDataAvailable">
<div class="shimmer-bar"></div> <div class="shimmer-bar"></div>
</div> </div>
</div> </div>
</div> </div>
<!-- sheield dashboard -->
<!--
<div class="chart-container">
<div class="chart-header">
<h3>Deal Stage Wise Progress</h3>
</div>
<div class="chart-wrapper">
<div class="chart-content" [class.loading]="isLoading">
<canvas
baseChart
[data]="barChartData"
[options]="barChartOptions"
[type]="barChartType"
(chartClick)="chartClicked($event)"
(chartHover)="chartHovered($event)">
</canvas>
<div class="loading-overlay" *ngIf="isLoading">
<div class="shimmer-bar"></div>
</div>
</div>
</div>
</div> -->
</div> </div>

View File

@@ -1,102 +1,191 @@
// Bar Chart Component Styles // Chart container structure
:host {
display: block;
height: 100%;
width: 100%;
}
.chart-container { .chart-container {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border-radius: 12px; // Filter section styling
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); .filter-section {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin-bottom: 20px;
transition: all 0.3s ease; padding: 15px;
border: 1px solid #eaeaea; border: 1px solid #ddd;
padding: 20px; border-radius: 4px;
background-color: #f9f9f9;
} }
.chart-container:hover { .filter-group {
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2); margin-bottom: 15px;
transform: translateY(-2px);
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
} }
.compact-filters-container { .filter-controls {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 5px; gap: 15px;
margin-bottom: 10px;
padding: 5px;
background: #ffffff;
border: 1px solid #e9ecef;
border-radius: 6px;
min-height: 40px;
} }
.drilldown-indicator { .filter-item {
background-color: #e0e0e0; flex: 1 1 300px;
min-width: 250px;
padding: 10px; padding: 10px;
margin-bottom: 15px; background: white;
border-radius: 8px; border: 1px solid #e0e0e0;
text-align: center; border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.drilldown-text { .filter-label {
font-weight: bold; font-weight: 500;
color: #333; margin-bottom: 8px;
font-size: 16px; 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 { .btn {
padding: 6px 12px; font-size: 13px;
border: none; }
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
.btn-secondary {
background-color: #007cba;
color: white;
}
.btn-danger {
background-color: #dc3545;
color: white;
} }
// Chart header styling
.chart-header { .chart-header {
margin-bottom: 20px; margin-bottom: 20px;
h3 { .header-row {
font-size: 22px; margin-bottom: 15px;
font-weight: 600;
color: #0a192f;
margin: 0;
text-align: center;
padding-bottom: 10px; padding-bottom: 10px;
border-bottom: 2px solid #3498db; border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
} }
} }
// Chart wrapper and content
.chart-wrapper { .chart-wrapper {
flex: 1; flex: 1;
position: relative; position: relative;
@@ -104,51 +193,30 @@
.chart-content { .chart-content {
position: relative; position: relative;
height: 100%; height: 100%;
background: #f8f9fa; min-height: 300px; // Ensure minimum height for chart
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
&.loading { &.loading {
opacity: 0.7; opacity: 0.7;
canvas { .chart-display {
filter: blur(2px); filter: blur(2px);
} }
} }
canvas { .no-data-message {
text-align: center;
padding: 20px;
color: #666;
font-style: italic;
}
.chart-display {
position: relative;
height: 100%;
width: 100%;
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
transition: filter 0.3s ease; transition: filter 0.3s ease;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
}
canvas:hover {
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15));
transform: scale(1.02);
transition: all 0.3s ease;
}
.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%;
width: 100%;
}
.no-data-message p {
margin: 0;
} }
.loading-overlay { .loading-overlay {
@@ -173,6 +241,7 @@
} }
} }
} }
}
@keyframes shimmer { @keyframes shimmer {
0% { 0% {
@@ -183,36 +252,27 @@
} }
} }
// Responsive design for chart container // Responsive design
@media (max-width: 768px) { @media (max-width: 768px) {
.chart-container { .chart-container {
padding: 15px; .filter-controls {
}
.chart-header h3 {
font-size: 18px;
}
.drilldown-indicator {
flex-direction: column; flex-direction: column;
gap: 5px;
} }
.drilldown-text { .filter-item {
font-size: 14px; min-width: 100%;
} }
.compact-filters-container { .chart-header {
flex-wrap: wrap; .header-row {
} .chart-title {
}
@media (max-width: 480px) {
.chart-container {
padding: 10px;
}
.chart-header h3 {
font-size: 16px; font-size: 16px;
} }
} }
}
.chart-content {
min-height: 250px; // Adjust for mobile
}
}
}

View File

@@ -57,21 +57,46 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
ticks: { ticks: {
autoSkip: false, autoSkip: false,
maxRotation: 45, maxRotation: 45,
minRotation: 45 minRotation: 45,
padding: 15,
font: {
size: 12
}
},
grid: {
display: false
} }
}, },
y: { y: {
beginAtZero: true beginAtZero: true,
ticks: {
font: {
size: 12
}
}
} }
}, },
plugins: { plugins: {
legend: { legend: {
display: true, display: true,
position: 'top', position: 'top',
labels: {
font: {
size: 12
}
}
}, },
tooltip: { tooltip: {
enabled: true enabled: true
} }
},
layout: {
padding: {
bottom: 60,
left: 15,
right: 15,
top: 15
}
} }
}; };
@@ -84,12 +109,20 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// No data state // No data state
noDataAvailable: boolean = false; noDataAvailable: boolean = false;
// Loading state
isLoading: boolean = false;
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
// Subscriptions to unsubscribe on destroy // Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
constructor( constructor(
private dashboardService: Dashboard3Service, private dashboardService: Dashboard3Service,
private filterService: FilterService private filterService: FilterService
@@ -111,6 +144,12 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('BarChartComponent input changes:', changes); console.log('BarChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -141,21 +180,329 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
} }
} }
// Handle filter changes from compact filters // Initialize filter values with proper default values based on type
onFilterChange(event: { filterId: string, value: any }): void { private initializeFilterValues(): void {
console.log('Compact filter changed:', event); console.log('Initializing filter values');
// The filter service will automatically trigger chart updates through the subscription
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
} }
fetchChartData(): void { fetchChartData(): void {
// Set loading state
this.isLoading = true;
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
// If we're in drilldown mode, fetch the appropriate drilldown data // If we're in drilldown mode, fetch the appropriate drilldown data
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) { if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
this.fetchDrilldownData(); this.fetchDrilldownData();
// Reset flag after fetching // Reset flags after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
return; return;
} }
@@ -179,73 +526,41 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
console.log('Bar chart data URL:', url); console.log('Bar chart data URL:', url);
// Convert baseFilters to filter parameters // Convert baseFilters to filter parameters
let filterParams = '';
if (this.baseFilters && this.baseFilters.length > 0) {
const filterObj = {}; const filterObj = {};
// Add base filters
if (this.baseFilters && this.baseFilters.length > 0) {
this.baseFilters.forEach(filter => { this.baseFilters.forEach(filter => {
if (filter.field && filter.value) { if (filter.field && filter.value) {
filterObj[filter.field] = filter.value; filterObj[filter.field] = filter.value;
} }
}); });
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
} }
}
console.log('Base filters:', this.baseFilters);
console.log('Base filter params:', filterParams);
// Add common filters to filter parameters // Add common filters
const commonFilters = this.filterService.getFilterValues(); const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters(); const filterDefinitions = this.filterService.getFilters();
console.log('Common filters from service:', commonFilters);
console.log('Filter definitions:', filterDefinitions);
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 => { Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId]; const filterValue = commonFilters[filterId];
console.log(`Processing filter ID: ${filterId}, Value:`, filterValue);
// Find the filter definition to get the field name // Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId); const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
console.log(`Filter definition for ${filterId}:`, filterDef);
if (filterDef && filterDef.field) { if (filterDef && filterDef.field) {
const fieldName = filterDef.field; const fieldName = filterDef.field;
console.log(`Mapping filter ID ${filterId} to field name: ${fieldName}`);
if (filterValue !== undefined && filterValue !== null && filterValue !== '') { if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue; filterObj[fieldName] = filterValue;
console.log(`Added to merged filters: ${fieldName} =`, filterValue);
}
} else {
// Fallback to using filterId as field name if no field is defined
console.log(`No field name found for filter ID ${filterId}, using ID as field name`);
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
console.log(`Added to merged filters: ${filterId} =`, filterValue);
} }
} }
}); });
if (Object.keys(mergedFilterObj).length > 0) { // Convert to JSON string for API call
filterParams = JSON.stringify(mergedFilterObj); let filterParams = '';
} if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
} }
console.log('Final merged filter object:', filterParams); console.log('Final filter object:', filterObj);
// Fetch data from the dashboard service with parameter field and value // Fetch data from the dashboard service with parameter field and value
// For base level, we pass empty parameter and value, but now also pass filters // For base level, we pass empty parameter and value, but now also pass filters
const subscription = this.dashboardService.getChartData(this.table, 'bar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe( const subscription = this.dashboardService.getChartData(this.table, 'bar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
@@ -257,8 +572,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.noDataAvailable = true; this.noDataAvailable = true;
this.barChartLabels = []; this.barChartLabels = [];
this.barChartData = []; this.barChartData = [];
// Reset flag after fetching // Reset flags after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
return; return;
} }
@@ -287,8 +603,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.barChartLabels = []; this.barChartLabels = [];
this.barChartData = []; this.barChartData = [];
} }
// Reset flag after fetching // Reset flags after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
}, },
(error) => { (error) => {
console.error('=== BAR CHART ERROR ==='); console.error('=== BAR CHART ERROR ===');
@@ -296,8 +613,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.noDataAvailable = true; this.noDataAvailable = true;
this.barChartLabels = []; this.barChartLabels = [];
this.barChartData = []; this.barChartData = [];
// Reset flag after fetching // Reset flags after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
// Keep default data in case of error // Keep default data in case of error
} }
); );
@@ -309,8 +627,9 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.noDataAvailable = true; this.noDataAvailable = true;
this.barChartLabels = []; this.barChartLabels = [];
this.barChartData = []; this.barChartData = [];
// Reset flag after fetching // Reset flags after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
} }
} }
@@ -400,61 +719,47 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
console.log('Drilldown data URL:', url); console.log('Drilldown data URL:', url);
// Convert drilldown layer filters to filter parameters (if applicable) // Convert drilldown layer filters to filter parameters (if applicable)
let filterParams = '';
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
const filterObj = {}; const filterObj = {};
// Add drilldown layer filters
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
drilldownConfig.filters.forEach((filter: any) => { drilldownConfig.filters.forEach((filter: any) => {
if (filter.field && filter.value) { if (filter.field && filter.value) {
filterObj[filter.field] = filter.value; filterObj[filter.field] = filter.value;
} }
}); });
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
} }
}
console.log('Drilldown layer filter parameters:', filterParams);
// Convert drilldownFilters to filter parameters for drilldown level // Add drilldownFilters
let drilldownFilterParams = '';
if (this.drilldownFilters && this.drilldownFilters.length > 0) { if (this.drilldownFilters && this.drilldownFilters.length > 0) {
const filterObj = {};
this.drilldownFilters.forEach(filter => { this.drilldownFilters.forEach(filter => {
if (filter.field && filter.value) { if (filter.field && filter.value) {
filterObj[filter.field] = filter.value; filterObj[filter.field] = filter.value;
} }
}); });
if (Object.keys(filterObj).length > 0) {
drilldownFilterParams = JSON.stringify(filterObj);
}
}
// 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 (drilldownFilterParams) {
try {
const drilldownFilterObj = JSON.parse(drilldownFilterParams);
Object.assign(mergedFilterObj, drilldownFilterObj);
} catch (e) {
console.warn('Failed to parse drilldown filter parameters:', e);
}
} }
// Add common filters // Add common filters
Object.keys(commonFilters).forEach(key => { const commonFilters = this.filterService.getFilterValues();
const value = commonFilters[key]; const filterDefinitions = this.filterService.getFilters();
if (value !== undefined && value !== null && value !== '') { Object.keys(commonFilters).forEach(filterId => {
mergedFilterObj[key] = value; 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 !== '') {
filterObj[fieldName] = filterValue;
}
} }
}); });
if (Object.keys(mergedFilterObj).length > 0) { // Convert to JSON string for API call
drilldownFilterParams = JSON.stringify(mergedFilterObj); let drilldownFilterParams = '';
} if (Object.keys(filterObj).length > 0) {
drilldownFilterParams = JSON.stringify(filterObj);
} }
console.log('Drilldown filter parameters:', drilldownFilterParams); console.log('Drilldown filter parameters:', drilldownFilterParams);
@@ -486,17 +791,23 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
// Trigger change detection // Trigger change detection
// this.barChartData = [...this.barChartData]; // this.barChartData = [...this.barChartData];
console.log('Updated bar chart with drilldown data:', { labels: this.barChartLabels, data: this.barChartData }); console.log('Updated bar chart with drilldown data:', { labels: this.barChartLabels, data: this.barChartData });
// Set loading state to false
this.isLoading = false;
} else if (data && data.labels && data.datasets) { } else if (data && data.labels && data.datasets) {
// Backend has already filtered the data, just display it // Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0; this.noDataAvailable = data.labels.length === 0;
this.barChartLabels = data.labels; this.barChartLabels = data.labels;
this.barChartData = data.datasets; this.barChartData = data.datasets;
console.log('Updated bar chart with drilldown legacy data format:', { labels: this.barChartLabels, data: this.barChartData }); console.log('Updated bar chart with drilldown legacy data format:', { labels: this.barChartLabels, data: this.barChartData });
// Set loading state to false
this.isLoading = false;
} else { } else {
console.warn('Drilldown received data does not have expected structure', data); console.warn('Drilldown received data does not have expected structure', data);
this.noDataAvailable = true; this.noDataAvailable = true;
this.barChartLabels = []; this.barChartLabels = [];
this.barChartData = []; this.barChartData = [];
// Set loading state to false
this.isLoading = false;
} }
}, },
(error) => { (error) => {
@@ -504,12 +815,17 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.noDataAvailable = true; this.noDataAvailable = true;
this.barChartLabels = []; this.barChartLabels = [];
this.barChartData = []; this.barChartData = [];
// Set loading state to false
this.isLoading = false;
// Keep current data in case of error // Keep current data in case of error
} }
); );
// Add subscription to array for cleanup // Add subscription to array for cleanup
this.subscriptions.push(subscription); this.subscriptions.push(subscription);
// Set loading state
this.isLoading = true;
} }
// Reset to original data (go back to base level) // Reset to original data (go back to base level)
@@ -713,6 +1029,12 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
this.originalBarChartLabels = []; this.originalBarChartLabels = [];
this.originalBarChartData = []; this.originalBarChartData = [];
// Clear multiselect tracking
this.openMultiselects.clear();
// Remove document click handler
this.removeDocumentClickHandler();
console.log('BarChartComponent destroyed and cleaned up'); console.log('BarChartComponent destroyed and cleaned up');
} }
} }

View File

@@ -1,14 +1,283 @@
<div style="display:block"> <div style="display:block; height: 100%; width: 100%;">
<!-- Drilldown mode indicator --> <!-- Filter Controls Section -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> <div class="filter-section" *ngIf="hasActiveFilters()">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <!-- Base Filters -->
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
Back to Level {{currentDrilldownLevel - 1}} <h4>Base Filters</h4>
</button> <div class="filter-controls">
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div *ngFor="let filter of baseFilters" class="filter-item">
Back to Main View <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- 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> </button>
</div> </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 --> <!-- No data message -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;"> <div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
@@ -16,7 +285,7 @@
</div> </div>
<!-- Chart display --> <!-- Chart display -->
<div *ngIf="!noDataAvailable"> <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
<canvas baseChart <canvas baseChart
[datasets]="bubbleChartData" [datasets]="bubbleChartData"
[type]="bubbleChartType" [type]="bubbleChartType"

View File

@@ -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;
}
}
}

View File

@@ -1,6 +1,8 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { ChartConfiguration, ChartDataset, ChartOptions } from 'chart.js'; import { ChartConfiguration, ChartDataset, ChartOptions } from 'chart.js';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-bubble-chart', selector: 'app-bubble-chart',
@@ -96,15 +98,40 @@ export class BubbleChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; 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 { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.fetchChartData(); this.fetchChartData();
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('BubbleChartComponent input changes:', changes); console.log('BubbleChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -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 { fetchChartData(): void {
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
@@ -160,7 +530,49 @@ export class BubbleChartComponent implements OnInit, OnChanges {
filterParams = JSON.stringify(filterObj); filterParams = JSON.stringify(filterObj);
} }
} }
console.log('Base filter parameters:', filterParams);
// Add common filters to filter parameters
const commonFilters = this.filterService.getFilterValues();
console.log('Common filters from service:', commonFilters);
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with base filters
const mergedFilterObj = {};
// Add base filters first
if (filterParams) {
try {
const baseFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, baseFilterObj);
} catch (e) {
console.warn('Failed to parse base filter parameters:', e);
}
}
// Add common filters using the field name as the key, not the filter id
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
}
} else {
// Fallback to using filterId as field name if no field is defined
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Final filter parameters:', filterParams);
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/bubble?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/bubble?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -206,6 +618,7 @@ export class BubbleChartComponent implements OnInit, OnChanges {
this.bubbleChartData = []; this.bubbleChartData = [];
// Reset flag after fetching // Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
// Keep default data in case of error
} }
); );
} else { } else {
@@ -307,6 +720,35 @@ export class BubbleChartComponent implements OnInit, OnChanges {
} }
} }
// Add common filters to drilldown filter parameters
const commonFilters = this.filterService.getFilterValues();
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with drilldown filters
const mergedFilterObj = {};
// Add drilldown filters first
if (filterParams) {
try {
const drilldownFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, drilldownFilterObj);
} catch (e) {
console.warn('Failed to parse drilldown filter parameters:', e);
}
}
// Add common filters
Object.keys(commonFilters).forEach(key => {
const value = commonFilters[key];
if (value !== undefined && value !== null && value !== '') {
mergedFilterObj[key] = value;
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/bubble?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/bubble?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Drilldown data URL:', url); console.log('Drilldown data URL:', url);
@@ -326,7 +768,6 @@ export class BubbleChartComponent implements OnInit, OnChanges {
// Handle the actual data structure returned by the API // Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) { if (data && data.chartLabels && data.chartData) {
// For bubble charts, we need to transform the data into bubble format // For bubble charts, we need to transform the data into bubble format
// Bubble charts expect data in the format: {x: number, y: number, r: number}
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.bubbleChartData = this.transformToBubbleData(data.chartLabels, data.chartData); this.bubbleChartData = this.transformToBubbleData(data.chartLabels, data.chartData);
console.log('Updated bubble chart with drilldown data:', this.bubbleChartData); console.log('Updated bubble chart with drilldown data:', this.bubbleChartData);
@@ -345,39 +786,11 @@ export class BubbleChartComponent implements OnInit, OnChanges {
console.error('Error fetching drilldown data:', error); console.error('Error fetching drilldown data:', error);
this.noDataAvailable = true; this.noDataAvailable = true;
this.bubbleChartData = []; this.bubbleChartData = [];
// Keep current data in case of error
} }
); );
} }
// Transform chart data to bubble chart format
private transformToBubbleData(labels: string[], datasets: any[]): ChartDataset[] {
// For bubble charts, we need to transform the data into bubble format
// Bubble charts expect data in the format: {x: number, y: number, r: number}
// This is a simple transformation - in a real implementation, you might want to
// create a more sophisticated mapping based on your data structure
return datasets.map((dataset, index) => {
// Create bubble data points
const bubbleData = labels.map((label, i) => {
// Use x-axis data as x coordinate, y-axis data as y coordinate, and a fixed radius
const xValue = dataset.data[i] || 0;
const yValue = i < datasets.length ? (datasets[i].data[index] || 0) : 0;
const radius = 10; // Fixed radius for now
return { x: xValue, y: yValue, r: radius };
});
return {
data: bubbleData,
label: dataset.label || `Dataset ${index + 1}`,
backgroundColor: dataset.backgroundColor || `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.6)`,
borderColor: dataset.borderColor || 'rgba(0, 0, 0, 1)',
hoverBackgroundColor: dataset.hoverBackgroundColor || 'rgba(255, 255, 255, 0.8)',
hoverBorderColor: dataset.hoverBorderColor || 'rgba(0, 0, 0, 1)'
};
});
}
// Reset to original data (go back to base level) // Reset to original data (go back to base level)
resetToOriginalData(): void { resetToOriginalData(): void {
console.log('Resetting to original data'); console.log('Resetting to original data');
@@ -438,16 +851,18 @@ export class BubbleChartComponent implements OnInit, OnChanges {
// Get the index of the clicked element // Get the index of the clicked element
const clickedIndex = e.active[0].index; const clickedIndex = e.active[0].index;
// Get the label of the clicked element // Get the dataset index
// For bubble charts, we might not have labels in the same way as other charts const datasetIndex = e.active[0].datasetIndex;
const clickedLabel = `Bubble ${clickedIndex}`;
console.log('Clicked on bubble:', { index: clickedIndex, label: clickedLabel }); // Get the data point
const dataPoint = this.bubbleChartData[datasetIndex].data[clickedIndex];
console.log('Clicked on bubble:', { datasetIndex, clickedIndex, dataPoint });
// If we're not at the base level, store original data // If we're not at the base level, store original data
if (this.currentDrilldownLevel === 0) { if (this.currentDrilldownLevel === 0) {
// Store original data before entering drilldown mode // Store original data before entering drilldown mode
this.originalBubbleChartData = [...this.bubbleChartData]; this.originalBubbleChartData = JSON.parse(JSON.stringify(this.bubbleChartData));
console.log('Stored original data for drilldown'); console.log('Stored original data for drilldown');
} }
@@ -489,9 +904,10 @@ export class BubbleChartComponent implements OnInit, OnChanges {
// Add this click to the drilldown stack // Add this click to the drilldown stack
const stackEntry = { const stackEntry = {
level: nextDrilldownLevel, level: nextDrilldownLevel,
datasetIndex: datasetIndex,
clickedIndex: clickedIndex, clickedIndex: clickedIndex,
clickedLabel: clickedLabel, dataPoint: dataPoint,
clickedValue: clickedLabel // Using label as value for now clickedValue: dataPoint // Using data point as value for now
}; };
this.drilldownStack.push(stackEntry); this.drilldownStack.push(stackEntry);
@@ -515,6 +931,6 @@ export class BubbleChartComponent implements OnInit, OnChanges {
} }
public chartHovered(e: any): void { public chartHovered(e: any): void {
console.log(e); console.log('Bubble chart hovered:', e);
} }
} }

View File

@@ -1,30 +1,288 @@
<div class="doughnut-chart-container"> <div class="chart-container">
<!-- Compact Filters --> <!-- Filter Controls Section -->
<div class="compact-filters-container" *ngIf="baseFilters && baseFilters.length > 0"> <div class="filter-section" *ngIf="hasActiveFilters()">
<app-compact-filter <!-- Base Filters -->
*ngFor="let filter of baseFilters" <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
[filterKey]="filter.field" <h4>Base Filters</h4>
(filterChange)="onFilterChange($event)"> <div class="filter-controls">
</app-compact-filter> <div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div> </div>
<!-- Drilldown mode indicator --> <!-- Dropdown Filter -->
<div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator"> <div *ngIf="filter.type === 'dropdown'" class="filter-input">
<span class="drilldown-text">Drilldown Level: {{currentDrilldownLevel}}</span> <select [(ngModel)]="filter.value"
<button class="btn btn-secondary btn-sm" (click)="navigateBack()"> (ngModelChange)="onBaseFilterChange(filter)"
Back to Level {{currentDrilldownLevel - 1}} class="clr-select filter-select">
</button> <option value="">Select {{ filter.field }}</option>
<button class="btn btn-danger btn-sm" (click)="resetToOriginalData()"> <option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
Back to Main View </select>
</button>
</div> </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="chart-header"> <div class="chart-header">
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3> <h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
</div> </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>
<div class="chart-wrapper"> <div class="chart-wrapper">
<div class="chart-content" [class.loading]="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable"> <div class="chart-content" [class.loading]="isLoading">
<!-- Show no data message --> <!-- Show no data message -->
<div class="no-data-message" *ngIf="noDataAvailable"> <div class="no-data-message" *ngIf="noDataAvailable">
<p>No chart data available</p> <p>No chart data available</p>
@@ -33,7 +291,7 @@
<!-- Show chart when data is available --> <!-- Show chart when data is available -->
<canvas baseChart <canvas baseChart
*ngIf="!noDataAvailable && doughnutChartLabels.length > 0 && doughnutChartData.length > 0" *ngIf="!noDataAvailable && doughnutChartLabels.length > 0 && doughnutChartData.length > 0"
[data]="doughnutChartData" [datasets]="doughnutChartData"
[labels]="doughnutChartLabels" [labels]="doughnutChartLabels"
[type]="doughnutChartType" [type]="doughnutChartType"
[options]="doughnutChartOptions" [options]="doughnutChartOptions"
@@ -42,7 +300,7 @@
</canvas> </canvas>
<!-- Loading overlay --> <!-- Loading overlay -->
<div class="loading-overlay" *ngIf="doughnutChartLabels.length === 0 && doughnutChartData.length === 0 && !noDataAvailable"> <div class="loading-overlay" *ngIf="isLoading">
<div class="shimmer-donut"></div> <div class="shimmer-donut"></div>
</div> </div>
</div> </div>
@@ -52,7 +310,7 @@
<div class="legend-item" *ngFor="let label of doughnutChartLabels; let i = index"> <div class="legend-item" *ngFor="let label of doughnutChartLabels; let i = index">
<span class="legend-color" [style.background-color]="getLegendColor(i)"></span> <span class="legend-color" [style.background-color]="getLegendColor(i)"></span>
<span class="legend-label">{{ label }}</span> <span class="legend-label">{{ label }}</span>
<span class="legend-value">{{ doughnutChartData && doughnutChartData[i] !== undefined ? doughnutChartData[i] : 0 }}</span> <span class="legend-value">{{ doughnutChartData && doughnutChartData[0] && doughnutChartData[0].data && doughnutChartData[0].data[i] !== undefined ? doughnutChartData[0].data[i] : 0 }}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,129 +1,200 @@
.doughnut-chart-container { // Chart container structure - simplified to match shield dashboard
.chart-container {
height: 100%;
min-height: 400px; // Ensure minimum height
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 400px;
min-height: 400px; // Filter section styling
padding: 20px; .filter-section {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); margin-bottom: 20px;
border-radius: 12px; padding: 15px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); border: 1px solid #ddd;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; border-radius: 4px;
transition: all 0.3s ease; background-color: #f9f9f9;
border: 1px solid #eaeaea;
} }
.doughnut-chart-container:hover { .filter-group {
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2); margin-bottom: 15px;
transform: translateY(-2px);
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
} }
.compact-filters-container { .filter-controls {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 5px; gap: 15px;
margin-bottom: 10px;
padding: 5px;
background: #ffffff;
border: 1px solid #e9ecef;
border-radius: 6px;
min-height: 40px;
} }
.drilldown-indicator { .filter-item {
background-color: #e0e0e0; flex: 1 1 300px;
min-width: 250px;
padding: 10px; padding: 10px;
margin-bottom: 15px; background: white;
border-radius: 8px; border: 1px solid #e0e0e0;
text-align: center; border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.drilldown-text { .filter-label {
font-weight: bold; font-weight: 500;
color: #333; margin-bottom: 8px;
font-size: 16px; 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 { .btn {
padding: 6px 12px; font-size: 13px;
border: none; }
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
.btn-secondary {
background-color: #007cba;
color: white;
}
.btn-danger {
background-color: #dc3545;
color: white;
} }
// Chart header styling
.chart-header { .chart-header {
margin-bottom: 20px; margin-bottom: 20px;
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title { .chart-title {
font-size: 22px; margin: 0;
font-size: 18px;
font-weight: 600; font-weight: 600;
color: #0a192f; color: #0a192f;
margin: 0; }
text-align: center;
padding-bottom: 10px;
border-bottom: 2px solid #3498db;
} }
} }
// Chart wrapper and content - simplified to match shield dashboard
.chart-wrapper { .chart-wrapper {
position: relative;
flex: 1; flex: 1;
min-height: 250px; position: relative;
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;
}
.chart-content { .chart-content {
position: relative; position: relative;
height: 100%; height: 100%;
width: 100%; min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
&.loading { &.loading {
opacity: 0.7; opacity: 0.7;
@@ -135,20 +206,15 @@
.no-data-message { .no-data-message {
text-align: center; text-align: center;
padding: 30px; padding: 20px;
color: #666; color: #666;
font-size: 18px;
font-style: italic; font-style: italic;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
} }
.no-data-message p { canvas {
margin: 0; max-width: 100%;
max-height: calc(100% - 40px); // Leave space for legend
transition: filter 0.3s ease;
} }
.loading-overlay { .loading-overlay {
@@ -172,68 +238,55 @@
} }
} }
} }
}
// Chart legend - simplified
.chart-legend { .chart-legend {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
gap: 15px; gap: 15px;
margin-top: 20px; margin: 20px 0;
padding: 20px; padding: 15px;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 8px;
border: 1px solid #dee2e6;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
}
.legend-item { .legend-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 12px 20px; padding: 8px 15px;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); background: #f8f9fa;
border-radius: 25px; border-radius: 20px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); border: 1px solid #e9ecef;
transition: all 0.3s ease; min-width: 120px;
border: 1px solid #eaeaea; justify-content: space-between;
cursor: pointer;
}
.legend-item:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
border-color: #3498db;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
} }
.legend-color { .legend-color {
width: 20px; width: 15px;
height: 20px; height: 15px;
border-radius: 50%; border-radius: 50%;
margin-right: 12px; margin-right: 8px;
display: inline-block; display: inline-block;
border: 2px solid white; flex-shrink: 0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
} }
.legend-label { .legend-label {
font-size: 16px; font-size: 14px;
font-weight: 600; font-weight: 500;
color: #2c3e50; color: #2c3e50;
margin-right: 15px; margin-right: 10px;
white-space: nowrap; white-space: nowrap;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); overflow: hidden;
text-overflow: ellipsis;
} }
.legend-value { .legend-value {
font-size: 16px; font-size: 14px;
font-weight: 700; font-weight: 600;
color: #3498db; color: #3498db;
background: linear-gradient(135deg, #e9ecef 0%, #dde1e5 100%); min-width: 30px;
padding: 6px 12px; text-align: right;
border-radius: 12px; }
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); }
min-width: 40px;
text-align: center;
} }
@keyframes shimmer { @keyframes shimmer {
@@ -245,46 +298,46 @@
} }
} }
/* Responsive design */ // Responsive design
@media (max-width: 768px) { @media (max-width: 768px) {
.doughnut-chart-container { .chart-container {
padding: 15px; .filter-controls {
}
.chart-header .chart-title {
font-size: 18px;
}
.drilldown-indicator {
flex-direction: column; flex-direction: column;
gap: 5px;
} }
.drilldown-text { .filter-item {
font-size: 14px; min-width: 100%;
} }
.chart-wrapper { .chart-header {
min-height: 200px; .header-row {
.chart-title {
font-size: 16px;
}
}
}
.chart-content {
min-height: 250px;
} }
.chart-legend { .chart-legend {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
}
.legend-item { .legend-item {
width: 100%; width: 100%;
max-width: 300px; max-width: 300px;
justify-content: space-between; justify-content: space-between;
} }
.no-data-message {
font-size: 16px;
padding: 20px;
} }
.compact-filters-container { .chart-content {
flex-wrap: wrap; min-height: 250px;
canvas {
max-height: calc(100% - 60px); // More space for legend on mobile
}
}
} }
} }

View File

@@ -8,7 +8,7 @@ import { Subscription } from 'rxjs';
templateUrl: './doughnut-chart.component.html', templateUrl: './doughnut-chart.component.html',
styleUrls: ['./doughnut-chart.component.scss'] styleUrls: ['./doughnut-chart.component.scss']
}) })
export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked { export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
@Input() xAxis: string; @Input() xAxis: string;
@Input() yAxis: string | string[]; @Input() yAxis: string | string[];
@Input() table: string; @Input() table: string;
@@ -36,7 +36,21 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
public doughnutChartLabels: string[] = ["Category A", "Category B", "Category C"]; public doughnutChartLabels: string[] = ["Category A", "Category B", "Category C"];
public doughnutChartData: number[] = [30, 50, 20]; public doughnutChartData: any[] = [
{
data: [30, 50, 20],
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
],
hoverBackgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
]
}
];
public doughnutChartType: string = "doughnut"; public doughnutChartType: string = "doughnut";
public doughnutChartOptions: any = { public doughnutChartOptions: any = {
responsive: true, responsive: true,
@@ -72,6 +86,14 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
borderWidth: 2, borderWidth: 2,
borderColor: '#fff' borderColor: '#fff'
} }
},
layout: {
padding: {
top: 20,
bottom: 20,
left: 20,
right: 20
}
} }
}; };
@@ -96,12 +118,20 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
// No data state // No data state
noDataAvailable: boolean = false; noDataAvailable: boolean = false;
// Loading state
isLoading: boolean = false;
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
// Subscriptions to unsubscribe on destroy // Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
constructor( constructor(
private dashboardService: Dashboard3Service, private dashboardService: Dashboard3Service,
private filterService: FilterService private filterService: FilterService
@@ -138,17 +168,33 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) { if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) {
// Add default data to ensure chart visibility // Add default data to ensure chart visibility
this.doughnutChartLabels = ["Category A", "Category B", "Category C"]; this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
this.doughnutChartData = [30, 50, 20]; this.doughnutChartData = [
{
data: [30, 50, 20],
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
],
hoverBackgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56'
]
}
];
} }
// Ensure we have matching arrays // Ensure we have matching arrays
if (this.doughnutChartLabels.length !== this.doughnutChartData.length) { if (this.doughnutChartLabels.length !== (this.doughnutChartData[0]?.data?.length || 0)) {
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length); const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData[0]?.data?.length || 0);
while (this.doughnutChartLabels.length < maxLength) { while (this.doughnutChartLabels.length < maxLength) {
this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`); this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
} }
while (this.doughnutChartData.length < maxLength) { if (this.doughnutChartData[0]) {
this.doughnutChartData.push(0); while (this.doughnutChartData[0].data.length < maxLength) {
this.doughnutChartData[0].data.push(0);
}
} }
} }
} }
@@ -164,6 +210,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('DoughnutChartComponent input changes:', changes); console.log('DoughnutChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -198,12 +250,318 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
ngOnDestroy(): void { ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
} }
// Handle filter changes from compact filters // Initialize filter values with proper default values based on type
onFilterChange(event: { filterId: string, value: any }): void { private initializeFilterValues(): void {
console.log('Compact filter changed:', event); console.log('Initializing filter values');
// The filter service will automatically trigger chart updates through the subscription
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
} }
// Public method to refresh data when filters change // Public method to refresh data when filters change
@@ -212,14 +570,18 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
} }
fetchChartData(): void { fetchChartData(): void {
// Set loading state
this.isLoading = true;
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
// If we're in drilldown mode, fetch the appropriate drilldown data // If we're in drilldown mode, fetch the appropriate drilldown data
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) { if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
this.fetchDrilldownData(); this.fetchDrilldownData();
// Reset flag after fetching // Reset flags after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
return; return;
} }
@@ -289,7 +651,7 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Doughnut chart data URL:', url); console.log('Chart data URL:', url);
// Fetch data from the dashboard service with parameter field and value // Fetch data from the dashboard service with parameter field and value
// For base level, we pass empty parameter and value, but now also pass filters // For base level, we pass empty parameter and value, but now also pass filters
@@ -297,89 +659,70 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
(data: any) => { (data: any) => {
console.log('Received doughnut chart data:', data); console.log('Received doughnut chart data:', data);
if (data === null) { if (data === null) {
console.warn('Doughnut chart API returned null data. Check if the API endpoint is working correctly.'); console.warn('API returned null data. Check if the API endpoint is working correctly.');
this.noDataAvailable = true; this.noDataAvailable = true;
this.doughnutChartLabels = []; this.doughnutChartLabels = [];
this.doughnutChartData = []; this.doughnutChartData = [];
// Validate and sanitize data to show default data // Reset flags after fetching
this.validateChartData();
// Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
return; return;
} }
// Handle the actual data structure returned by the API // Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) { if (data && data.chartLabels && data.chartData) {
// For doughnut charts, we need to extract the data differently // Backend has already filtered the data, just display it
// The first dataset's data array contains the values for the doughnut chart
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.doughnutChartLabels = data.chartLabels || []; this.doughnutChartLabels = data.chartLabels;
if (data.chartData && data.chartData.length > 0) { this.doughnutChartData = [
this.doughnutChartData = data.chartData[0].data.map(value => { {
// Convert to number if it's not already data: data.chartData,
const numValue = Number(value); backgroundColor: this.chartColors.slice(0, data.chartData.length),
return isNaN(numValue) ? 0 : numValue; hoverBackgroundColor: this.chartColors.slice(0, data.chartData.length)
});
} else {
this.doughnutChartData = [];
} }
// Ensure labels and data arrays have the same length ];
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection
this.doughnutChartData = [...this.doughnutChartData];
console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
} else if (data && data.labels && data.data) { } else if (data && data.labels && data.datasets) {
// Handle the original expected format as fallback // Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0; this.noDataAvailable = data.labels.length === 0;
this.doughnutChartLabels = data.labels || []; this.doughnutChartLabels = data.labels;
this.doughnutChartData = data.data.map(value => { this.doughnutChartData = data.datasets;
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection
this.doughnutChartData = [...this.doughnutChartData];
console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
} else { } else {
console.warn('Doughnut chart received data does not have expected structure', data); console.warn('Received data does not have expected structure', data);
// Reset to default data
this.noDataAvailable = true; this.noDataAvailable = true;
this.doughnutChartLabels = []; this.doughnutChartLabels = [];
this.doughnutChartData = []; this.doughnutChartData = [
// Validate and sanitize data to show default data {
this.validateChartData(); data: [],
backgroundColor: [],
hoverBackgroundColor: []
} }
// Reset flag after fetching ];
}
// Reset flags after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
}, },
(error) => { (error) => {
console.error('Error fetching doughnut chart data:', error); console.error('Error fetching doughnut chart data:', error);
this.noDataAvailable = true; this.noDataAvailable = true;
this.doughnutChartLabels = []; this.doughnutChartLabels = [];
this.doughnutChartData = []; this.doughnutChartData = [];
// Validate and sanitize data to show default data // Reset flags after fetching
this.validateChartData();
// Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
// Keep default data in case of error
} }
); );
} else { } else {
console.log('Missing required data for doughnut chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
// Don't set noDataAvailable to true when there's no required data this.noDataAvailable = true;
// This allows static data to be displayed this.doughnutChartLabels = [];
this.noDataAvailable = false; this.doughnutChartData = [];
// Validate the chart data to ensure we have some data to display // Reset flags after fetching
this.validateChartData();
// Force a redraw to ensure the chart displays
this.doughnutChartData = [...this.doughnutChartData];
// Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
this.isLoading = false;
} }
} }
@@ -475,6 +818,35 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
} }
} }
// Add common filters to drilldown filter parameters
const commonFilters = this.filterService.getFilterValues();
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with drilldown filters
const mergedFilterObj = {};
// Add drilldown filters first
if (filterParams) {
try {
const drilldownFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, drilldownFilterObj);
} catch (e) {
console.warn('Failed to parse drilldown filter parameters:', e);
}
}
// Add common filters
Object.keys(commonFilters).forEach(key => {
const value = commonFilters[key];
if (value !== undefined && value !== null && value !== '') {
mergedFilterObj[key] = value;
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/doughnut?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/doughnut?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Drilldown data URL:', url); console.log('Drilldown data URL:', url);
@@ -494,49 +866,40 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
// Handle the actual data structure returned by the API // Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) { if (data && data.chartLabels && data.chartData) {
// For doughnut charts, we need to extract the data differently // Backend has already filtered the data, just display it
// The first dataset's data array contains the values for the doughnut chart
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.doughnutChartLabels = data.chartLabels || []; this.doughnutChartLabels = data.chartLabels;
if (data.chartData && data.chartData.length > 0) { this.doughnutChartData = [
this.doughnutChartData = data.chartData[0].data.map(value => { {
// Convert to number if it's not already data: data.chartData,
const numValue = Number(value); backgroundColor: this.chartColors.slice(0, data.chartData.length),
return isNaN(numValue) ? 0 : numValue; hoverBackgroundColor: this.chartColors.slice(0, data.chartData.length)
});
} else {
this.doughnutChartData = [];
} }
// Ensure labels and data arrays have the same length ];
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection
this.doughnutChartData = [...this.doughnutChartData];
console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
} else if (data && data.labels && data.data) { // Set loading state to false
// Handle the original expected format as fallback this.isLoading = false;
} else if (data && data.labels && data.datasets) {
// Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0; this.noDataAvailable = data.labels.length === 0;
this.doughnutChartLabels = data.labels || []; this.doughnutChartLabels = data.labels;
this.doughnutChartData = data.data.map(value => { this.doughnutChartData = data.datasets;
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection
this.doughnutChartData = [...this.doughnutChartData];
console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
// Set loading state to false
this.isLoading = false;
} else { } else {
console.warn('Drilldown received data does not have expected structure', data); console.warn('Drilldown received data does not have expected structure', data);
this.noDataAvailable = true; this.noDataAvailable = true;
this.doughnutChartLabels = []; this.doughnutChartLabels = [];
this.doughnutChartData = []; this.doughnutChartData = [
// Validate and sanitize data {
this.validateChartData(); data: [],
backgroundColor: [],
hoverBackgroundColor: []
}
];
// Set loading state to false
this.isLoading = false;
} }
}, },
(error) => { (error) => {
@@ -544,9 +907,14 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
this.noDataAvailable = true; this.noDataAvailable = true;
this.doughnutChartLabels = []; this.doughnutChartLabels = [];
this.doughnutChartData = []; this.doughnutChartData = [];
// Set loading state to false
this.isLoading = false;
// Keep current data in case of error // Keep current data in case of error
} }
); );
// Set loading state
this.isLoading = true;
} }
// Reset to original data (go back to base level) // Reset to original data (go back to base level)
@@ -563,7 +931,7 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
console.log('Restored original labels'); console.log('Restored original labels');
} }
if (this.originalDoughnutChartData.length > 0) { if (this.originalDoughnutChartData.length > 0) {
this.doughnutChartData = [...this.originalDoughnutChartData]; this.doughnutChartData = JSON.parse(JSON.stringify(this.originalDoughnutChartData));
console.log('Restored original data'); console.log('Restored original data');
} }
@@ -605,44 +973,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
} }
} }
/** // Get legend color for a specific index
* Get color for legend item getLegendColor(index: number): string {
* @param index Index of the legend item
*/
public getLegendColor(index: number): string {
return this.chartColors[index % this.chartColors.length]; return this.chartColors[index % this.chartColors.length];
} }
/**
* Ensure labels and data arrays have the same length
*/
private syncLabelAndDataArrays(): void {
// Handle empty arrays
if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) {
return;
}
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length);
// Pad the shorter array with default values
while (this.doughnutChartLabels.length < maxLength) {
this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
}
while (this.doughnutChartData.length < maxLength) {
this.doughnutChartData.push(0);
}
// Truncate the longer array if needed
if (this.doughnutChartLabels.length > maxLength) {
this.doughnutChartLabels = this.doughnutChartLabels.slice(0, maxLength);
}
if (this.doughnutChartData.length > maxLength) {
this.doughnutChartData = this.doughnutChartData.slice(0, maxLength);
}
}
// events // events
public chartClicked(e: any): void { public chartClicked(e: any): void {
console.log('Doughnut chart clicked:', e); console.log('Doughnut chart clicked:', e);
@@ -729,6 +1064,6 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
} }
public chartHovered(e: any): void { public chartHovered(e: any): void {
console.log(e); console.log('Doughnut chart hovered:', e);
} }
} }

View File

@@ -1,4 +1,285 @@
<div class="dynamic-chart-container"> <div class="dynamic-chart-container">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Dynamic Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</div>
</div>
<!-- Show current drilldown level -->
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
<div class="clr-col-12">
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
<div class="alert-items">
<div class="alert-item static">
<div class="alert-icon-wrapper">
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
</div>
<span class="alert-text">
Drilldown Level: {{currentDrilldownLevel}}
<span *ngIf="drilldownStack.length > 0">
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Existing content -->
<!-- Drilldown mode indicator --> <!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> <div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>

View File

@@ -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;
}
}
}

View File

@@ -2,6 +2,8 @@ import { Component, OnInit, ViewChild, Input, OnChanges, SimpleChanges } from '@
import { ChartConfiguration, ChartData, ChartDataset } from 'chart.js'; import { ChartConfiguration, ChartData, ChartDataset } from 'chart.js';
import { BaseChartDirective } from 'ng2-charts'; import { BaseChartDirective } from 'ng2-charts';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-dynamic-chart', selector: 'app-dynamic-chart',
@@ -37,9 +39,20 @@ export class DynamicChartComponent implements OnInit, OnChanges {
@ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined; @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined;
constructor(private dashboardService: Dashboard3Service) { } constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
// Initialize with default data // Initialize with default data
this.fetchChartData(); this.fetchChartData();
} }
@@ -47,6 +60,12 @@ export class DynamicChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('DynamicChartComponent input changes:', changes); console.log('DynamicChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -107,6 +126,14 @@ export class DynamicChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
fetchChartData(): void { fetchChartData(): void {
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
@@ -139,7 +166,49 @@ export class DynamicChartComponent implements OnInit, OnChanges {
filterParams = JSON.stringify(filterObj); filterParams = JSON.stringify(filterObj);
} }
} }
console.log('Base filter parameters:', filterParams);
// Add common filters to filter parameters
const commonFilters = this.filterService.getFilterValues();
console.log('Common filters from service:', commonFilters);
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with base filters
const mergedFilterObj = {};
// Add base filters first
if (filterParams) {
try {
const baseFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, baseFilterObj);
} catch (e) {
console.warn('Failed to parse base filter parameters:', e);
}
}
// Add common filters using the field name as the key, not the filter id
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
}
} else {
// Fallback to using filterId as field name if no field is defined
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Final filter parameters:', filterParams);
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/dynamic?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/dynamic?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -504,4 +573,322 @@ export class DynamicChartComponent implements OnInit, OnChanges {
} }
this.dynamicChartData = _dynamicChartData; this.dynamicChartData = _dynamicChartData;
} }
ngOnDestroy(): void {
// Unsubscribe from all subscriptions to prevent memory leaks
this.subscriptions.forEach(subscription => subscription.unsubscribe());
// Remove document click handler if it exists
this.removeDocumentClickHandler();
}
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
} }

View File

@@ -1,4 +1,285 @@
<div class="financial-chart-container"> <div class="financial-chart-container">
<!-- Filter Controls Section -->
<div class="filter-section" *ngIf="hasActiveFilters()">
<!-- Base Filters -->
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
<h4>Base Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of baseFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'base-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Drilldown Filters -->
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
<h4>Drilldown Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of drilldownFilters" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onDrilldownFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'drilldown-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Layer Filters -->
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
<h4>Layer Filters</h4>
<div class="filter-controls">
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div>
<!-- Dropdown Filter -->
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
<select [(ngModel)]="filter.value"
(ngModelChange)="onLayerFilterChange(filter)"
class="clr-select filter-select">
<option value="">Select {{ filter.field }}</option>
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
<span class="multiselect-label">{{ filter.field }}</span>
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
({{ getSelectedOptionsCount(filter) }} selected)
</span>
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
</div>
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
<div class="checkbox-group">
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(filter, option)"
(change)="onMultiSelectChange(filter, option, $event)"
[id]="'layer-' + filter.field + '-' + i"
class="clr-checkbox">
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filter.value.start"
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
placeholder="Start Date"
class="clr-input filter-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filter.value.end"
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
placeholder="End Date"
class="clr-input filter-date">
</div>
</div>
<!-- Toggle Filter -->
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
<input type="checkbox"
[(ngModel)]="filter.value"
(ngModelChange)="onToggleChange(filter, $event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filter.field }}</label>
</div>
</div>
</div>
</div>
<!-- Clear Filters Button -->
<div class="filter-actions">
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
</div>
</div>
<!-- Header row with chart title and drilldown navigation -->
<div class="clr-row header-row">
<div class="clr-col-6">
<h3 class="chart-title">{{charttitle || 'Financial Chart'}}</h3>
</div>
<div class="clr-col-6" style="text-align: right;">
<!-- Add drilldown navigation controls -->
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</div>
</div>
<!-- Show current drilldown level -->
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
<div class="clr-col-12">
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
<div class="alert-items">
<div class="alert-item static">
<div class="alert-icon-wrapper">
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
</div>
<span class="alert-text">
Drilldown Level: {{currentDrilldownLevel}}
<span *ngIf="drilldownStack.length > 0">
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Existing content -->
<!-- Drilldown mode indicator --> <!-- Drilldown mode indicator -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> <div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>

View File

@@ -1,108 +1,192 @@
.financial-chart-container { .filter-section {
display: flex;
flex-direction: column;
height: 400px;
min-height: 400px;
padding: 20px;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
transition: all 0.3s ease;
border: 1px solid #eaeaea;
}
.financial-chart-container:hover {
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
transform: translateY(-2px);
}
.chart-title {
font-size: 26px;
font-weight: 700;
color: #2c3e50;
margin-bottom: 20px; margin-bottom: 20px;
text-align: center;
padding-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; 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 { .chart-title {
font-size: 20px; margin: 0;
margin-bottom: 15px; font-size: 18px;
font-weight: 600;
color: #333;
}
} }
.chart-wrapper { // Responsive design
min-height: 200px; @media (max-width: 768px) {
.filter-controls {
flex-direction: column;
} }
.no-data-message { .filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px; font-size: 16px;
padding: 20px; }
} }
} }

View File

@@ -1,5 +1,8 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
import { AlertsService } from 'src/app/services/fnd/alerts.service';
@Component({ @Component({
selector: 'app-financial-chart', selector: 'app-financial-chart',
@@ -33,9 +36,21 @@ export class FinancialChartComponent implements OnInit, OnChanges {
// Multi-layer drilldown configuration inputs // Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
constructor(private dashboardService: Dashboard3Service) { } constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService,
private alertService: AlertsService
) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
// Initialize with default data // Initialize with default data
this.fetchChartData(); this.fetchChartData();
} }
@@ -43,6 +58,14 @@ export class FinancialChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('FinancialChartComponent input changes:', changes); console.log('FinancialChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
// Load filter options for dropdown/multiselect filters
this.loadFilterOptions();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -86,6 +109,14 @@ export class FinancialChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
fetchChartData(): void { fetchChartData(): void {
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
@@ -118,7 +149,49 @@ export class FinancialChartComponent implements OnInit, OnChanges {
filterParams = JSON.stringify(filterObj); filterParams = JSON.stringify(filterObj);
} }
} }
console.log('Base filter parameters:', filterParams);
// Add common filters to filter parameters
const commonFilters = this.filterService.getFilterValues();
console.log('Common filters from service:', commonFilters);
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with base filters
const mergedFilterObj = {};
// Add base filters first
if (filterParams) {
try {
const baseFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, baseFilterObj);
} catch (e) {
console.warn('Failed to parse base filter parameters:', e);
}
}
// Add common filters using the field name as the key, not the filter id
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
}
} else {
// Fallback to using filterId as field name if no field is defined
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Final filter parameters:', filterParams);
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/financial?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/financial?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -496,4 +569,520 @@ export class FinancialChartComponent implements OnInit, OnChanges {
public chartHovered(e: any): void { public chartHovered(e: any): void {
console.log(e); console.log(e);
} }
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
// Ensure filter has required properties
if (!filter.type) filter.type = 'text';
if (!filter.options) filter.options = '';
if (!filter.availableValues) filter.availableValues = '';
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 => {
// Ensure filter has required properties
if (!filter.type) filter.type = 'text';
if (!filter.options) filter.options = '';
if (!filter.availableValues) filter.availableValues = '';
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) => {
// Ensure filter has required properties
if (!filter.type) filter.type = 'text';
if (!filter.options) filter.options = '';
if (!filter.availableValues) filter.availableValues = '';
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();
}
// Load filter options for dropdown and multiselect filters
private loadFilterOptions(): void {
console.log('Loading filter options');
// Load options for base filters
if (this.baseFilters && this.table) {
this.baseFilters.forEach((filter, index) => {
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
this.loadFilterValuesForField(this.table, this.connection, filter.field, index, 'base');
}
});
}
// Load options for drilldown filters
if (this.drilldownFilters && this.drilldownApiUrl) {
this.drilldownFilters.forEach((filter, index) => {
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, filter.field, index, 'drilldown');
}
});
}
// Load options for layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach((layer, layerIndex) => {
if (layer.filters && layer.apiUrl) {
layer.filters.forEach((filter, filterIndex) => {
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
this.loadFilterValuesForField(layer.apiUrl, this.connection, filter.field, filterIndex, 'layer', layerIndex);
}
});
}
});
}
}
// Load filter values for a specific field
private loadFilterValuesForField(
apiUrl: string,
connectionId: number | undefined,
field: string,
filterIndex: number,
filterType: 'base' | 'drilldown' | 'layer',
layerIndex?: number
): void {
if (apiUrl && field) {
this.alertService.getValuesFromUrl(apiUrl, connectionId, field).subscribe(
(values: string[]) => {
console.log(`Loaded filter values for ${filterType} filter ${field}:`, values);
// Update the filter with available values
if (filterType === 'base') {
const filter = this.baseFilters[filterIndex];
if (filter) {
filter.availableValues = values.join(', ');
// For dropdown/multiselect types, also update the options
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
filter.options = filter.availableValues;
}
}
} else if (filterType === 'drilldown') {
const filter = this.drilldownFilters[filterIndex];
if (filter) {
filter.availableValues = values.join(', ');
// For dropdown/multiselect types, also update the options
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
filter.options = filter.availableValues;
}
}
} else if (filterType === 'layer' && layerIndex !== undefined) {
const layer = this.drilldownLayers[layerIndex];
if (layer && layer.filters) {
const filter = layer.filters[filterIndex];
if (filter) {
filter.availableValues = values.join(', ');
// For dropdown/multiselect types, also update the options
if (filter.type === 'dropdown' || filter.type === 'multiselect') {
filter.options = filter.availableValues;
}
}
}
}
},
(error) => {
console.error('Error loading available values for field:', field, error);
}
);
}
}
// Handle base filter field change
onBaseFilterFieldChange(index: number, field: string): void {
const filter = this.baseFilters[index];
if (filter) {
filter.field = field;
// If field changes, reset value and options
filter.value = '';
filter.options = '';
filter.availableValues = '';
// If we have a field and table URL, load available values
if (field && this.table && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
this.loadFilterValuesForField(this.table, this.connection, field, index, 'base');
}
}
}
// Handle base filter type change
onBaseFilterTypeChange(index: number, type: string): void {
const filter = this.baseFilters[index];
if (filter) {
filter.type = type;
// If type changes to dropdown/multiselect and we have a field, load available values
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.table) {
this.loadFilterValuesForField(this.table, this.connection, filter.field, index, 'base');
}
}
}
// Handle drilldown filter field change
onDrilldownFilterFieldChange(index: number, field: string): void {
const filter = this.drilldownFilters[index];
if (filter) {
filter.field = field;
// If field changes, reset value and options
filter.value = '';
filter.options = '';
filter.availableValues = '';
// If we have a field and drilldown API URL, load available values
if (field && this.drilldownApiUrl && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, field, index, 'drilldown');
}
}
}
// Handle drilldown filter type change
onDrilldownFilterTypeChange(index: number, type: string): void {
const filter = this.drilldownFilters[index];
if (filter) {
filter.type = type;
// If type changes to dropdown/multiselect and we have a field, load available values
if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.drilldownApiUrl) {
this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, filter.field, index, 'drilldown');
}
}
}
// Handle layer filter field change
onLayerFilterFieldChange(layerIndex: number, filterIndex: number, field: string): void {
const layer = this.drilldownLayers[layerIndex];
if (layer && layer.filters) {
const filter = layer.filters[filterIndex];
if (filter) {
filter.field = field;
// If field changes, reset value and options
filter.value = '';
filter.options = '';
filter.availableValues = '';
// If we have a field and layer API URL, load available values
if (field && layer.apiUrl && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
this.loadFilterValuesForField(layer.apiUrl, this.connection, field, filterIndex, 'layer', layerIndex);
}
}
}
}
// Handle layer filter type change
onLayerFilterTypeChange(layerIndex: number, filterIndex: number, type: string): void {
const layer = this.drilldownLayers[layerIndex];
if (layer && layer.filters) {
const filter = layer.filters[filterIndex];
if (filter) {
filter.type = type;
// If type changes to dropdown/multiselect and we have a field, load available values
if ((type === 'dropdown' || type === 'multiselect') && filter.field && layer.apiUrl) {
this.loadFilterValuesForField(layer.apiUrl, this.connection, filter.field, filterIndex, 'layer', layerIndex);
}
}
}
}
} }

View File

@@ -1,12 +1,289 @@
<div style="display: block;"> <div style="display: block;">
<div class="dg-wrapper"> <div class="dg-wrapper">
<!-- 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>
<div class="clr-row"> <div class="clr-row">
<div class="clr-col-8"> <!-- <div class="clr-col-8">
<h3>{{charttitle || 'Data Grid'}}</h3> <h3>{{charttitle || 'Data Grid'}}</h3>
</div> -->
<!-- Add drilldown navigation controls -->
<div class="clr-col-4" *ngIf="drilldownEnabled && drilldownStack.length > 0" style="text-align: right;">
<button class="btn btn-sm btn-link" (click)="navigateBack()">
<cds-icon shape="arrow" direction="left"></cds-icon>
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
</button>
</div> </div>
</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].clickedKey}} = {{drilldownStack[drilldownStack.length - 1].clickedValue}})
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<clr-datagrid [clrDgLoading]="loading"> <clr-datagrid [clrDgLoading]="loading">
<clr-dg-placeholder> <ng-template #loadingSpinner><clr-spinner>Loading ... </clr-spinner></ng-template> <clr-dg-placeholder>
<ng-template #loadingSpinner>
<clr-spinner>Loading ... </clr-spinner>
</ng-template>
<div *ngIf="error;else loadingSpinner">{{error}}</div> <div *ngIf="error;else loadingSpinner">{{error}}</div>
</clr-dg-placeholder> </clr-dg-placeholder>
@@ -19,7 +296,7 @@
<clr-dg-row *clrDgItems="let item of givendata" [clrDgItem]="item"> <clr-dg-row *clrDgItems="let item of givendata" [clrDgItem]="item">
<!-- Dynamic cells based on response keys --> <!-- Dynamic cells based on response keys -->
<clr-dg-cell *ngFor="let header of dynamicHeaders"> <clr-dg-cell *ngFor="let header of dynamicHeaders" (click)="onRowClick(item, header.key)">
{{item[header.key]}} {{item[header.key]}}
</clr-dg-cell> </clr-dg-cell>
</clr-dg-row> </clr-dg-row>

View File

@@ -1,12 +1,180 @@
@import '../../../../../../../styles1.scss'; .filter-section {
input.ng-invalid.ng-touched { margin-bottom: 20px;
border-color: red; padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
} }
.error_mess { .filter-group {
color: red; margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
} }
clr-datagrid{ }
height: 400px; /* Adjust the height as needed */
.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; 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;
}
}
.dg-wrapper {
padding: 15px;
}
clr-datagrid {
margin-top: 10px;
}
// Responsive design
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
} }

View File

@@ -1,13 +1,17 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, OnChanges, OnDestroy, SimpleChanges, ViewChild } from '@angular/core';
import { UsergrpmaintainceService } from 'src/app/services/admin/usergrpmaintaince.service'; import { UsergrpmaintainceService } from 'src/app/services/admin/usergrpmaintaince.service';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
// Add FilterService import
import { FilterService } from '../../common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-grid-view', selector: 'app-grid-view',
templateUrl: './grid-view.component.html', templateUrl: './grid-view.component.html',
styleUrls: ['./grid-view.component.scss'] styleUrls: ['./grid-view.component.scss']
}) })
export class GridViewComponent implements OnInit, OnChanges { export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
@Input() xAxis: string; @Input() xAxis: string;
@Input() yAxis: string | string[]; @Input() yAxis: string | string[];
@Input() table: string; @Input() table: string;
@@ -23,6 +27,16 @@ export class GridViewComponent implements OnInit, OnChanges {
@Input() datasource: string; @Input() datasource: string;
@Input() fieldName: string; @Input() fieldName: string;
@Input() connection: number; // Add connection input @Input() connection: number; // Add connection input
// Drilldown configuration inputs
@Input() drilldownEnabled: boolean = false;
@Input() drilldownApiUrl: string;
@Input() drilldownXAxis: string;
@Input() drilldownYAxis: string;
@Input() drilldownParameter: string; // Add drilldown parameter input
@Input() baseFilters: any[] = []; // Add base filters input
@Input() drilldownFilters: any[] = []; // Add drilldown filters input
// Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
loading = false; loading = false;
givendata: any[] = []; givendata: any[] = [];
@@ -38,13 +52,46 @@ export class GridViewComponent implements OnInit, OnChanges {
submitted = false; submitted = false;
dynamicHeaders: any[] = []; dynamicHeaders: any[] = [];
constructor( // Multi-layer drilldown state tracking
drilldownStack: any[] = []; // Stack to track drilldown navigation history
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
originalGridData: any[] = [];
// No data state
noDataAvailable: boolean = false;
// Flag to prevent infinite loops
private isFetchingData: boolean = false;
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
// Add a flag to track if filters have been initialized
private filtersInitialized: boolean = false;
// Add properties to track open multiselect dropdowns
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
// Add property to track document click handler
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
constructor(
private mainservice: UsergrpmaintainceService, private mainservice: UsergrpmaintainceService,
private dashboardService: Dashboard3Service, private dashboardService: Dashboard3Service,
// Add FilterService to constructor
private filterService: FilterService
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the grid data
console.log('GridView: Filter state changed:', filters);
this.fetchGridData();
})
);
this.fetchGridData(); this.fetchGridData();
} }
@@ -55,33 +102,193 @@ export class GridViewComponent implements OnInit, OnChanges {
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
const tableChanged = changes.table && !changes.table.firstChange; const tableChanged = changes.table && !changes.table.firstChange;
const connectionChanged = changes.connection && !changes.connection.firstChange; // Add connection change detection const connectionChanged = changes.connection && !changes.connection.firstChange;
const baseFiltersChanged = changes.baseFilters && !changes.baseFilters.firstChange;
// Drilldown configuration changes
const drilldownEnabledChanged = changes.drilldownEnabled && !changes.drilldownEnabled.firstChange;
const drilldownApiUrlChanged = changes.drilldownApiUrl && !changes.drilldownApiUrl.firstChange;
const drilldownXAxisChanged = changes.drilldownXAxis && !changes.drilldownXAxis.firstChange;
const drilldownYAxisChanged = changes.drilldownYAxis && !changes.drilldownYAxis.firstChange;
const drilldownLayersChanged = changes.drilldownLayers && !changes.drilldownLayers.firstChange;
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Respond to input changes // Respond to input changes
if (xAxisChanged || yAxisChanged || tableChanged || connectionChanged) { if (!this.isFetchingData && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged ||
console.log('X or Y axis or table or connection changed, fetching new data'); drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
// Only fetch data if xAxis, yAxis, table, or connection has changed (and it's not the first change) drilldownLayersChanged)) {
console.log('X or Y axis or table or connection or base filters or drilldown config changed, fetching new data');
// Only fetch data if xAxis, yAxis, table, connection, baseFilters or drilldown config has changed (and it's not the first change)
this.fetchGridData(); this.fetchGridData();
} }
} }
// 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
});
}
// Dynamic headers for the grid // Dynamic headers for the grid
fetchGridData(): void { fetchGridData(): void {
// Set flag to prevent recursive calls
this.isFetchingData = true;
// If we're in drilldown mode, fetch the appropriate drilldown data
if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
this.fetchDrilldownData();
// Reset flag after fetching
this.isFetchingData = false;
return;
}
// If we have the necessary data, fetch grid data from the service // If we have the necessary data, fetch grid data from the service
if (this.table && this.xAxis) { // if (this.table && this.xAxis) {
console.log('Fetching grid data for:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); if (this.table) {
console.log('=== GRID VIEW DEBUG INFO ===');
console.log('Table:', this.table);
console.log('X-Axis:', this.xAxis);
console.log('Y-Axis:', this.yAxis);
console.log('Connection:', this.connection);
// Convert yAxis to string if it's an array // Convert yAxis to string if it's an array
const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis; const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis;
// Get the parameter value from the drilldown stack for base level (should be empty)
let parameterValue = '';
// Log the URL that will be called
let url = `chart/getdashjson/grid?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Grid data URL:', url);
// Get filter parameters from base filters
const filterObj = {};
// Add base filters
if (this.baseFilters && this.baseFilters.length > 0) {
this.baseFilters.forEach(filter => {
if (filter.field && filter.value) {
filterObj[filter.field] = filter.value;
}
});
}
// Add common filters directly as key-value pairs
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Add common filters using the field name as the key
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 !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('GridView: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service, similar to other chart components // Fetch data from the dashboard service, similar to other chart components
this.dashboardService.getChartData(this.table, 'grid', this.xAxis, yAxisString, this.connection).subscribe( this.dashboardService.getChartData(this.table, 'grid', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
(data: any) => { (data: any) => {
console.log('=== GRID VIEW DATA RESPONSE ===');
console.log('Received grid data:', data); console.log('Received grid data:', data);
if (data === null) { if (data === null) {
console.warn('Grid API returned null data. Check if the API endpoint is working correctly.'); console.warn('Grid API returned null data. Check if the API endpoint is working correctly.');
this.error = "No data Available"; this.error = "No data Available";
this.noDataAvailable = true;
// Reset flag after fetching
this.isFetchingData = false;
return; return;
} }
@@ -90,27 +297,36 @@ export class GridViewComponent implements OnInit, OnChanges {
this.givendata = data.chartData; this.givendata = data.chartData;
this.extractDynamicHeaders(data.chartData); this.extractDynamicHeaders(data.chartData);
this.error = this.givendata.length === 0 ? "No data Available" : undefined; this.error = this.givendata.length === 0 ? "No data Available" : undefined;
this.noDataAvailable = this.givendata.length === 0;
console.log('Updated grid with data:', this.givendata); console.log('Updated grid with data:', this.givendata);
} else if (data && data.data) { } else if (data && data.data) {
// Handle the original expected format as fallback // Handle the original expected format as fallback
this.givendata = data.data; this.givendata = data.data;
this.extractDynamicHeaders(data.data); this.extractDynamicHeaders(data.data);
this.error = this.givendata.length === 0 ? "No data Available" : undefined; this.error = this.givendata.length === 0 ? "No data Available" : undefined;
this.noDataAvailable = this.givendata.length === 0;
console.log('Updated grid with legacy data format:', this.givendata); console.log('Updated grid with legacy data format:', this.givendata);
} else if (Array.isArray(data)) { } else if (Array.isArray(data)) {
// Handle case where data is directly an array // Handle case where data is directly an array
this.givendata = data; this.givendata = data;
this.extractDynamicHeaders(data); this.extractDynamicHeaders(data);
this.error = this.givendata.length === 0 ? "No data Available" : undefined; this.error = this.givendata.length === 0 ? "No data Available" : undefined;
this.noDataAvailable = this.givendata.length === 0;
console.log('Updated grid with array data:', this.givendata); console.log('Updated grid with array data:', this.givendata);
} else { } else {
console.warn('Grid received data does not have expected structure', data); console.warn('Grid received data does not have expected structure', data);
this.error = "No valid data received"; this.error = "No valid data received";
this.givendata = []; this.givendata = [];
this.noDataAvailable = true;
} }
// Reset flag after fetching
this.isFetchingData = false;
}, (error) => { }, (error) => {
console.log('Error fetching grid data:', error); console.log('Error fetching grid data:', error);
this.error = "Server Error"; this.error = "Server Error";
this.noDataAvailable = true;
// Reset flag after fetching
this.isFetchingData = false;
}); });
} else if (this.table) { } else if (this.table) {
console.log('Missing xAxis, falling back to default data fetching'); console.log('Missing xAxis, falling back to default data fetching');
@@ -126,13 +342,342 @@ export class GridViewComponent implements OnInit, OnChanges {
this.givendata = Array.isArray(data) ? data : []; this.givendata = Array.isArray(data) ? data : [];
this.extractDynamicHeaders(data); this.extractDynamicHeaders(data);
this.error = this.givendata && this.givendata.length === 0 ? "No data Available" : undefined; this.error = this.givendata && this.givendata.length === 0 ? "No data Available" : undefined;
this.noDataAvailable = this.givendata && this.givendata.length === 0;
// Reset flag after fetching
this.isFetchingData = false;
}, (error) => { }, (error) => {
console.log(error); console.log(error);
this.error = "Server Error"; this.error = "Server Error";
this.noDataAvailable = true;
// Reset flag after fetching
this.isFetchingData = false;
}); });
} else { } else {
console.log('Missing required data for grid:', { table: this.table }); console.log('Missing required data for grid:', { table: this.table });
this.error = "Table name is required"; this.error = "Table name is required";
this.noDataAvailable = true;
// Reset flag after fetching
this.isFetchingData = false;
}
}
// Fetch drilldown data based on current drilldown level
fetchDrilldownData(): void {
console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
console.log('Drilldown stack:', this.drilldownStack);
// Get the current drilldown configuration based on the current level
let drilldownConfig;
if (this.currentDrilldownLevel === 1) {
// Base drilldown level
drilldownConfig = {
apiUrl: this.drilldownApiUrl,
xAxis: this.drilldownXAxis,
yAxis: this.drilldownYAxis,
parameter: this.drilldownParameter
};
} else {
// Multi-layer drilldown level
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) {
drilldownConfig = this.drilldownLayers[layerIndex];
} else {
console.warn('Invalid drilldown layer index:', layerIndex);
this.error = "Invalid drilldown configuration";
this.givendata = [];
this.noDataAvailable = true;
return;
}
}
console.log('Drilldown config for level', this.currentDrilldownLevel, ':', drilldownConfig);
// Check if we have valid drilldown configuration
if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) {
console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
this.error = "Missing drilldown configuration";
this.givendata = [];
this.noDataAvailable = true;
return;
}
// Get the parameter value from the drilldown stack
let parameterValue = '';
if (this.drilldownStack.length > 0) {
const lastEntry = this.drilldownStack[this.drilldownStack.length - 1];
parameterValue = lastEntry.clickedValue || '';
console.log('Parameter value from last click:', parameterValue);
}
// Get the parameter field from drilldown config
const parameterField = drilldownConfig.parameter || '';
console.log('Parameter field:', parameterField);
console.log('Fetching drilldown data for level:', this.currentDrilldownLevel, {
apiUrl: drilldownConfig.apiUrl,
xAxis: drilldownConfig.xAxis,
yAxis: drilldownConfig.yAxis,
parameterField: parameterField,
parameterValue: parameterValue,
connection: this.connection
});
// Build the actual API URL with parameter replacement
let actualApiUrl = drilldownConfig.apiUrl;
console.log('Original API URL:', actualApiUrl);
console.log('Parameter value to use:', parameterValue);
console.log('Parameter field:', parameterField);
// Check if the URL contains angle brackets for parameter replacement
const hasAngleBrackets = /<[^>]+>/.test(actualApiUrl);
if (hasAngleBrackets && parameterValue) {
// Replace angle brackets placeholder with actual value
console.log('Replacing angle brackets with parameter value');
const encodedValue = encodeURIComponent(parameterValue);
actualApiUrl = actualApiUrl.replace(/<[^>]+>/g, encodedValue);
console.log('URL after angle bracket replacement:', actualApiUrl);
}
// Log the URL that will be called
let url = `chart/getdashjson/grid?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
if (parameterField && parameterValue) {
url += `&parameter=${encodeURIComponent(parameterField)}&parameterValue=${encodeURIComponent(parameterValue)}`;
}
console.log('Drilldown data URL:', url);
// Convert drilldown layer filters to filter parameters (if applicable)
const filterObj = {};
// Add drilldown layer filters
if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
drilldownConfig.filters.forEach((filter: any) => {
if (filter.field && filter.value) {
filterObj[filter.field] = filter.value;
}
});
}
// Add drilldownFilters
if (this.drilldownFilters && this.drilldownFilters.length > 0) {
this.drilldownFilters.forEach(filter => {
if (filter.field && filter.value) {
filterObj[filter.field] = filter.value;
}
});
}
// Add common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
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 !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let drilldownFilterParams = '';
if (Object.keys(filterObj).length > 0) {
drilldownFilterParams = JSON.stringify(filterObj);
}
console.log('Drilldown filter parameters:', drilldownFilterParams);
// For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters
this.dashboardService.getChartData(
drilldownConfig.apiUrl, 'grid',
drilldownConfig.xAxis, drilldownConfig.yAxis,
this.connection,
parameterField, parameterValue,
drilldownFilterParams
).subscribe(
(data: any) => {
console.log('Received drilldown data:', data);
if (data === null) {
console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
this.error = "No data Available";
this.givendata = [];
this.noDataAvailable = true;
return;
}
// Handle the actual data structure returned by the API
if (data && data.chartData) {
this.givendata = data.chartData;
this.extractDynamicHeaders(data.chartData);
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
this.noDataAvailable = this.givendata.length === 0;
console.log('Updated grid with drilldown data:', this.givendata);
} else if (data && data.data) {
// Handle the original expected format as fallback
this.givendata = data.data;
this.extractDynamicHeaders(data.data);
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
this.noDataAvailable = this.givendata.length === 0;
console.log('Updated grid with drilldown legacy data format:', this.givendata);
} else if (Array.isArray(data)) {
// Handle case where data is directly an array
this.givendata = data;
this.extractDynamicHeaders(data);
this.error = this.givendata.length === 0 ? "No data Available" : undefined;
this.noDataAvailable = this.givendata.length === 0;
console.log('Updated grid with drilldown array data:', this.givendata);
} else {
console.warn('Drilldown received data does not have expected structure', data);
this.error = "No valid data received";
this.givendata = [];
this.noDataAvailable = true;
}
},
(error) => {
console.error('Error fetching drilldown data:', error);
this.error = "Server Error";
this.givendata = [];
this.noDataAvailable = true;
}
);
}
// Reset to original data (go back to base level)
resetToOriginalData(): void {
console.log('Resetting to original data');
console.log('Current stack before reset:', this.drilldownStack);
console.log('Current level before reset:', this.currentDrilldownLevel);
this.currentDrilldownLevel = 0;
this.drilldownStack = [];
if (this.originalGridData.length > 0) {
// Create a deep copy to avoid reference issues
this.givendata = JSON.parse(JSON.stringify(this.originalGridData));
this.extractDynamicHeaders(this.givendata);
console.log('Restored original data');
}
console.log('After reset - data:', this.givendata);
// Re-fetch original data
this.fetchGridData();
}
// Navigate back to previous drilldown level
navigateBack(): void {
console.log('Navigating back, current stack:', this.drilldownStack);
console.log('Current level:', this.currentDrilldownLevel);
if (this.drilldownStack.length > 0) {
// Remove the last entry from the stack
const removedEntry = this.drilldownStack.pop();
console.log('Removed entry from stack:', removedEntry);
// Update the current drilldown level
this.currentDrilldownLevel = this.drilldownStack.length;
console.log('New level after pop:', this.currentDrilldownLevel);
console.log('Stack after pop:', this.drilldownStack);
if (this.drilldownStack.length > 0) {
// Fetch data for the previous level
console.log('Fetching data for previous level');
this.fetchDrilldownData();
} else {
// Back to base level
console.log('Back to base level, resetting to original data');
this.resetToOriginalData();
}
} else {
// Already at base level, reset to original data
console.log('Already at base level, resetting to original data');
this.resetToOriginalData();
}
}
// Method to handle grid row clicks for drilldown
onRowClick(item: any, key: string): void {
console.log('Grid row clicked:', { item, key });
// If drilldown is enabled
if (this.drilldownEnabled) {
// Get the value for the clicked key
const clickedValue = item[key];
console.log('Clicked on row value:', { key, value: clickedValue });
// If we're not at the base level, store original data
if (this.currentDrilldownLevel === 0) {
// Store original data before entering drilldown mode
// Create a deep copy to avoid reference issues
this.originalGridData = JSON.parse(JSON.stringify(this.givendata));
console.log('Stored original data for drilldown');
}
// Determine the next drilldown level
const nextDrilldownLevel = this.currentDrilldownLevel + 1;
console.log('Next drilldown level will be:', nextDrilldownLevel);
// Check if there's a drilldown configuration for this level
let hasDrilldownConfig = false;
let drilldownConfig;
if (nextDrilldownLevel === 1) {
// Base drilldown level
drilldownConfig = {
apiUrl: this.drilldownApiUrl,
xAxis: this.drilldownXAxis,
yAxis: this.drilldownYAxis,
parameter: this.drilldownParameter
};
hasDrilldownConfig = !!this.drilldownApiUrl && !!this.drilldownXAxis && !!this.drilldownYAxis;
} else {
// Multi-layer drilldown level
const layerIndex = nextDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length) {
drilldownConfig = this.drilldownLayers[layerIndex];
hasDrilldownConfig = drilldownConfig.enabled &&
!!drilldownConfig.apiUrl &&
!!drilldownConfig.xAxis &&
!!drilldownConfig.yAxis;
}
}
console.log('Drilldown config for next level:', drilldownConfig);
console.log('Has drilldown config:', hasDrilldownConfig);
// If there's a drilldown configuration for the next level, proceed
if (hasDrilldownConfig) {
// Add this click to the drilldown stack
const stackEntry = {
level: nextDrilldownLevel,
clickedKey: key,
clickedValue: clickedValue
};
this.drilldownStack.push(stackEntry);
console.log('Added to drilldown stack:', stackEntry);
console.log('Current drilldown stack:', this.drilldownStack);
// Update the current drilldown level
this.currentDrilldownLevel = nextDrilldownLevel;
console.log('Entering drilldown level:', this.currentDrilldownLevel);
// Fetch drilldown data for the new level
this.fetchDrilldownData();
} else {
console.log('No drilldown configuration for level:', nextDrilldownLevel);
}
} else {
console.log('Drilldown not enabled');
} }
} }
@@ -175,4 +720,261 @@ export class GridViewComponent implements OnInit, OnChanges {
.replace(/([A-Z])/g, ' $1') .replace(/([A-Z])/g, ' $1')
.replace(/^./, str => str.toUpperCase()); .replace(/^./, str => str.toUpperCase());
} }
// 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.fetchGridData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchGridData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchGridData();
}
// 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.fetchGridData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchGridData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchGridData();
}
// 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.fetchGridData();
}
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('GridViewComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
// Clear data to help with garbage collection
this.givendata = [];
this.dynamicHeaders = [];
this.drilldownStack = [];
this.originalGridData = [];
// Clear multiselect tracking
this.openMultiselects.clear();
// Remove document click handler
this.removeDocumentClickHandler();
console.log('GridViewComponent destroyed and cleaned up');
}
} }

View File

@@ -1,14 +1,283 @@
<div style="display: block"> <div style="display: block; height: 100%; width: 100%;">
<!-- Drilldown mode indicator --> <!-- Filter Controls Section -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> <div class="filter-section" *ngIf="hasActiveFilters()">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <!-- Base Filters -->
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
Back to Level {{currentDrilldownLevel - 1}} <h4>Base Filters</h4>
</button> <div class="filter-controls">
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div *ngFor="let filter of baseFilters" class="filter-item">
Back to Main View <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- 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 || 'Line 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> </button>
</div> </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 --> <!-- No data message -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;"> <div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
@@ -16,12 +285,11 @@
</div> </div>
<!-- Chart display --> <!-- Chart display -->
<div *ngIf="!noDataAvailable"> <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
<canvas baseChart <canvas baseChart
[datasets]="lineChartData" [datasets]="lineChartData"
[labels]="lineChartLabels" [labels]="lineChartLabels"
[options]="lineChartOptions" [options]="lineChartOptions"
[legend]="lineChartLegend" [legend]="lineChartLegend"
[type]="lineChartType" [type]="lineChartType"
(chartHover)="chartHovered($event)" (chartHover)="chartHovered($event)"

View File

@@ -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;
}
}
}

View File

@@ -8,7 +8,7 @@ import { Subscription } from 'rxjs';
templateUrl: './line-chart.component.html', templateUrl: './line-chart.component.html',
styleUrls: ['./line-chart.component.scss'] styleUrls: ['./line-chart.component.scss']
}) })
export class LineChartComponent implements OnInit, OnChanges { export class LineChartComponent implements OnInit, OnChanges, OnDestroy {
@Input() xAxis: string; @Input() xAxis: string;
@Input() yAxis: string | string[]; @Input() yAxis: string | string[];
@Input() table: string; @Input() table: string;
@@ -88,6 +88,11 @@ export class LineChartComponent implements OnInit, OnChanges {
// Subscriptions to unsubscribe on destroy // Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
constructor( constructor(
private dashboardService: Dashboard3Service, private dashboardService: Dashboard3Service,
private filterService: FilterService private filterService: FilterService
@@ -109,6 +114,12 @@ export class LineChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('LineChartComponent input changes:', changes); console.log('LineChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -140,6 +151,318 @@ export class LineChartComponent implements OnInit, OnChanges {
ngOnDestroy(): void { ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
}
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
} }
// Public method to refresh data when filters change // Public method to refresh data when filters change

View File

@@ -1,16 +1,284 @@
<div class="pie-chart-container"> <div class="pie-chart-container">
<!-- Drilldown mode indicator --> <!-- Filter Controls Section -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> <div class="filter-section" *ngIf="hasActiveFilters()">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <!-- Base Filters -->
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
Back to Level {{currentDrilldownLevel - 1}} <h4>Base Filters</h4>
</button> <div class="filter-controls">
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div *ngFor="let filter of baseFilters" class="filter-item">
Back to Main View <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
</button>
<!-- Text Filter -->
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
<input type="text"
[(ngModel)]="filter.value"
(ngModelChange)="onBaseFilterChange(filter)"
[placeholder]="filter.field"
class="clr-input filter-text-input">
</div> </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> <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-wrapper">
<!-- Show loading indicator --> <!-- Show loading indicator -->
<div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable"> <div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable">

View File

@@ -149,10 +149,192 @@
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
/* Responsive design */ // Filter section styles
.filter-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative;
}
.multiselect-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
}
.multiselect-value {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.dropdown-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
&:hover {
border-color: #999;
}
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
.checkbox-group {
padding: 8px;
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
.checkbox-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
}
}
.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-separator {
margin: 0 5px;
color: #777;
}
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
.toggle-label {
margin: 0;
font-size: 14px;
cursor: pointer;
}
}
.filter-actions {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
.btn {
font-size: 13px;
}
}
// New header row styling
.header-row {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.chart-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
text-align: left;
padding-bottom: 0;
border-bottom: none;
}
}
// Responsive design
@media (max-width: 768px) { @media (max-width: 768px) {
.pie-chart-container { .pie-chart-container {
padding: 15px; padding: 15px;
height: auto;
min-height: 300px;
} }
.chart-title { .chart-title {
@@ -179,4 +361,18 @@
font-size: 16px; font-size: 16px;
padding: 20px; padding: 20px;
} }
.filter-controls {
flex-direction: column;
}
.filter-item {
min-width: 100%;
}
.header-row {
.chart-title {
font-size: 16px;
}
}
} }

View File

@@ -8,7 +8,7 @@ import { Subscription } from 'rxjs';
templateUrl: './pie-chart.component.html', templateUrl: './pie-chart.component.html',
styleUrls: ['./pie-chart.component.scss'] styleUrls: ['./pie-chart.component.scss']
}) })
export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
@Input() xAxis: string; @Input() xAxis: string;
@Input() yAxis: string | string[]; @Input() yAxis: string | string[];
@Input() table: string; @Input() table: string;
@@ -101,6 +101,11 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
// Subscriptions to unsubscribe on destroy // Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
constructor( constructor(
private dashboardService: Dashboard3Service, private dashboardService: Dashboard3Service,
private filterService: FilterService private filterService: FilterService
@@ -133,6 +138,12 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('PieChartComponent input changes:', changes); console.log('PieChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -158,6 +169,318 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
ngOnDestroy(): void { ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
}
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
} }
// Public method to refresh data when filters change // Public method to refresh data when filters change
@@ -243,7 +566,7 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Pie chart data URL:', url); console.log('Chart data URL:', url);
// Fetch data from the dashboard service with parameter field and value // Fetch data from the dashboard service with parameter field and value
// For base level, we pass empty parameter and value, but now also pass filters // For base level, we pass empty parameter and value, but now also pass filters
@@ -251,12 +574,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
(data: any) => { (data: any) => {
console.log('Received pie chart data:', data); console.log('Received pie chart data:', data);
if (data === null) { if (data === null) {
console.warn('Pie chart API returned null data. Check if the API endpoint is working correctly.'); console.warn('API returned null data. Check if the API endpoint is working correctly.');
this.noDataAvailable = true; this.noDataAvailable = true;
this.pieChartLabels = []; this.pieChartLabels = [];
this.pieChartData = []; this.pieChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
// Reset flag after fetching // Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
return; return;
@@ -264,50 +585,26 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
// Handle the actual data structure returned by the API // Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) { if (data && data.chartLabels && data.chartData) {
// For pie charts, we need to extract the data differently // Backend has already filtered the data, just display it
// The first dataset's data array contains the values for the pie chart
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.pieChartLabels = data.chartLabels || []; this.pieChartLabels = data.chartLabels;
if (data.chartData && data.chartData.length > 0) { this.pieChartData = data.chartData;
this.pieChartData = data.chartData[0].data.map(value => {
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
} else {
this.pieChartData = [];
}
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection // Trigger change detection
this.pieChartData = [...this.pieChartData]; this.pieChartData = [...this.pieChartData];
console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData }); console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData });
} else if (data && data.labels && data.data) { } else if (data && data.labels && data.datasets) {
// Handle the original expected format as fallback // Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0; this.noDataAvailable = data.labels.length === 0;
this.pieChartLabels = data.labels || []; this.pieChartLabels = data.labels;
this.pieChartData = data.data.map(value => { this.pieChartData = data.datasets[0]?.data || [];
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection // Trigger change detection
this.pieChartData = [...this.pieChartData]; this.pieChartData = [...this.pieChartData];
console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData }); console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
} else { } else {
console.warn('Pie chart received data does not have expected structure', data); console.warn('Received data does not have expected structure', data);
// Reset to default data
this.noDataAvailable = true; this.noDataAvailable = true;
this.pieChartLabels = []; this.pieChartLabels = [];
this.pieChartData = []; this.pieChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
} }
// Reset flag after fetching // Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
@@ -317,21 +614,16 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
this.noDataAvailable = true; this.noDataAvailable = true;
this.pieChartLabels = []; this.pieChartLabels = [];
this.pieChartData = []; this.pieChartData = [];
// Validate and sanitize data to show default data
this.validateChartData();
// Reset flag after fetching // Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
// Keep default data in case of error
} }
); );
} else { } else {
console.log('Missing required data for pie chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
// Don't set noDataAvailable to true when there's no required data this.noDataAvailable = true;
// This allows static data to be displayed this.pieChartLabels = [];
this.noDataAvailable = false; this.pieChartData = [];
// Validate the chart data to ensure we have some data to display
this.validateChartData();
// Force a redraw to ensure the chart displays
this.pieChartData = [...this.pieChartData];
// Reset flag after fetching // Reset flag after fetching
this.isFetchingData = false; this.isFetchingData = false;
} }
@@ -477,39 +769,18 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
// Handle the actual data structure returned by the API // Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) { if (data && data.chartLabels && data.chartData) {
// For pie charts, we need to extract the data differently // Backend has already filtered the data, just display it
// The first dataset's data array contains the values for the pie chart
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.pieChartLabels = data.chartLabels || []; this.pieChartLabels = data.chartLabels;
if (data.chartData && data.chartData.length > 0) { this.pieChartData = data.chartData;
this.pieChartData = data.chartData[0].data.map(value => {
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
} else {
this.pieChartData = [];
}
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection // Trigger change detection
this.pieChartData = [...this.pieChartData]; this.pieChartData = [...this.pieChartData];
console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData }); console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData });
} else if (data && data.labels && data.data) { } else if (data && data.labels && data.datasets) {
// Handle the original expected format as fallback // Backend has already filtered the data, just display it
this.noDataAvailable = data.labels.length === 0; this.noDataAvailable = data.labels.length === 0;
this.pieChartLabels = data.labels || []; this.pieChartLabels = data.labels;
this.pieChartData = data.data.map(value => { this.pieChartData = data.datasets[0]?.data || [];
// Convert to number if it's not already
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Validate and sanitize data
this.validateChartData();
// Trigger change detection // Trigger change detection
this.pieChartData = [...this.pieChartData]; this.pieChartData = [...this.pieChartData];
console.log('Updated pie chart with drilldown legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData }); console.log('Updated pie chart with drilldown legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
@@ -518,8 +789,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
this.noDataAvailable = true; this.noDataAvailable = true;
this.pieChartLabels = []; this.pieChartLabels = [];
this.pieChartData = []; this.pieChartData = [];
// Validate and sanitize data
this.validateChartData();
} }
}, },
(error) => { (error) => {
@@ -588,82 +857,32 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
} }
} }
/** // Validate chart data to ensure labels and data arrays have the same length
* Get color for legend item private validateChartData(): void {
* @param index Index of the legend item if (this.pieChartLabels && this.pieChartData) {
*/ // For pie charts, we need to ensure labels and data arrays have the same length
public getLegendColor(index: number): string { const labelCount = this.pieChartLabels.length;
return this.chartColors[index % this.chartColors.length]; const dataCount = this.pieChartData.length;
}
/** if (labelCount !== dataCount) {
* Ensure labels and data arrays have the same length console.warn('Pie chart labels and data arrays have different lengths:', { labels: labelCount, data: dataCount });
*/ // Pad or truncate data array to match label count
private syncLabelAndDataArrays(): void { if (dataCount < labelCount) {
// Ensure we have matching arrays // Pad with zeros
if (this.pieChartLabels.length !== this.pieChartData.length) { while (this.pieChartData.length < labelCount) {
const maxLength = Math.max(this.pieChartLabels.length, this.pieChartData.length);
while (this.pieChartLabels.length < maxLength) {
this.pieChartLabels.push(`Label ${this.pieChartLabels.length + 1}`);
}
while (this.pieChartData.length < maxLength) {
this.pieChartData.push(0); this.pieChartData.push(0);
} }
} else if (dataCount > labelCount) {
// Truncate data array
this.pieChartData = this.pieChartData.slice(0, labelCount);
}
}
} }
} }
/** // Get legend color for a specific index
* Validate and sanitize chart data getLegendColor(index: number): string {
*/ return this.chartColors[index % this.chartColors.length];
private validateChartData(): void {
console.log('Validating chart data:', { labels: this.pieChartLabels, data: this.pieChartData });
// Ensure we have valid arrays
if (!Array.isArray(this.pieChartLabels)) {
this.pieChartLabels = [];
}
if (!Array.isArray(this.pieChartData)) {
this.pieChartData = [];
}
// Ensure we have some data to display
if (this.pieChartLabels.length === 0 && this.pieChartData.length === 0) {
// Add default data to ensure chart visibility
this.pieChartLabels = ['Category A', 'Category B', 'Category C'];
this.pieChartData = [30, 50, 20];
console.log('Added default data for chart display');
}
// Ensure labels and data arrays have the same length
this.syncLabelAndDataArrays();
// Ensure all data values are numbers
this.pieChartData = this.pieChartData.map(value => {
const numValue = Number(value);
return isNaN(numValue) ? 0 : numValue;
});
console.log('After validation:', { labels: this.pieChartLabels, data: this.pieChartData });
}
ngAfterViewChecked() {
// Debugging: Log component state after view checks
console.log('PieChartComponent state:', {
labels: this.pieChartLabels,
data: this.pieChartData,
hasData: this.pieChartLabels.length > 0 && this.pieChartData.length > 0
});
}
/**
* Check if chart data is valid and ready to display
*/
public isChartDataValid(): boolean {
return this.pieChartLabels && this.pieChartData &&
Array.isArray(this.pieChartLabels) && Array.isArray(this.pieChartData) &&
this.pieChartLabels.length > 0 && this.pieChartData.length > 0 &&
this.pieChartLabels.length === this.pieChartData.length;
} }
// events // events
@@ -752,6 +971,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
} }
public chartHovered(e: any): void { public chartHovered(e: any): void {
console.log(e); console.log('Pie chart hovered:', e);
}
ngAfterViewChecked(): void {
// This lifecycle hook can be used if needed for post-render operations
} }
} }

View File

@@ -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 <canvas baseChart
[datasets]="polarAreaChartData" [datasets]="polarAreaChartData"
[labels]="polarAreaChartLabels" [labels]="polarAreaChartLabels"
@@ -8,3 +288,4 @@
(chartClick)="chartClicked($event)"> (chartClick)="chartClicked($event)">
</canvas> </canvas>
</div> </div>
</div>

View File

@@ -1,18 +1,192 @@
// Polar Chart Component Styles .filter-section {
div[style*="display: block"] { margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.filter-group {
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-weight: 600;
}
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.filter-item {
flex: 1 1 300px;
min-width: 250px;
padding: 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.filter-label {
font-weight: 500;
margin-bottom: 8px;
color: #555;
font-size: 14px;
}
.filter-input {
width: 100%;
.filter-text-input,
.filter-select,
.filter-date {
width: 100%;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
height: 34px;
}
}
.multiselect-container {
position: relative; position: relative;
width: 100%;
height: 100%;
} }
canvas { .multiselect-display {
max-width: 100%; display: flex;
max-height: 100%; align-items: center;
justify-content: space-between;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
min-height: 34px;
.multiselect-label {
flex: 1;
font-size: 14px;
} }
// Ensure the chart container has proper sizing .multiselect-value {
:host { color: #666;
display: block; font-size: 12px;
width: 100%; margin-right: 8px;
height: 100%; }
.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;
}
}
} }

View File

@@ -1,5 +1,7 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-polar-chart', selector: 'app-polar-chart',
@@ -33,9 +35,20 @@ export class PolarChartComponent implements OnInit, OnChanges {
// Multi-layer drilldown configuration inputs // Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
constructor(private dashboardService: Dashboard3Service) { } constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
// Initialize with default data // Initialize with default data
this.fetchChartData(); this.fetchChartData();
} }
@@ -43,6 +56,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('PolarChartComponent input changes:', changes); console.log('PolarChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -85,6 +104,324 @@ export class PolarChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
fetchChartData(): void { fetchChartData(): void {
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
@@ -117,7 +454,49 @@ export class PolarChartComponent implements OnInit, OnChanges {
filterParams = JSON.stringify(filterObj); filterParams = JSON.stringify(filterObj);
} }
} }
console.log('Base filter parameters:', filterParams);
// Add common filters to filter parameters
const commonFilters = this.filterService.getFilterValues();
console.log('Common filters from service:', commonFilters);
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with base filters
const mergedFilterObj = {};
// Add base filters first
if (filterParams) {
try {
const baseFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, baseFilterObj);
} catch (e) {
console.warn('Failed to parse base filter parameters:', e);
}
}
// Add common filters using the field name as the key, not the filter id
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
}
} else {
// Fallback to using filterId as field name if no field is defined
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Final filter parameters:', filterParams);
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/polar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/polar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -287,6 +666,35 @@ export class PolarChartComponent implements OnInit, OnChanges {
} }
} }
// Add common filters to drilldown filter parameters
const commonFilters = this.filterService.getFilterValues();
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with drilldown filters
const mergedFilterObj = {};
// Add drilldown filters first
if (filterParams) {
try {
const drilldownFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, drilldownFilterObj);
} catch (e) {
console.warn('Failed to parse drilldown filter parameters:', e);
}
}
// Add common filters
Object.keys(commonFilters).forEach(key => {
const value = commonFilters[key];
if (value !== undefined && value !== null && value !== '') {
mergedFilterObj[key] = value;
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/polar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/polar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Drilldown data URL:', url); console.log('Drilldown data URL:', url);
@@ -307,7 +715,6 @@ export class PolarChartComponent implements OnInit, OnChanges {
// Handle the actual data structure returned by the API // Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) { if (data && data.chartLabels && data.chartData) {
// For polar charts, we need to extract the data differently // For polar charts, we need to extract the data differently
// The first dataset's data array contains the values for the polar chart
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.polarAreaChartLabels = data.chartLabels; this.polarAreaChartLabels = data.chartLabels;
if (data.chartData && data.chartData.length > 0) { if (data.chartData && data.chartData.length > 0) {
@@ -417,13 +824,13 @@ export class PolarChartComponent implements OnInit, OnChanges {
// Get the label of the clicked element // Get the label of the clicked element
const clickedLabel = this.polarAreaChartLabels[clickedIndex]; const clickedLabel = this.polarAreaChartLabels[clickedIndex];
console.log('Clicked on polar area:', { index: clickedIndex, label: clickedLabel }); console.log('Clicked on polar point:', { index: clickedIndex, label: clickedLabel });
// If we're not at the base level, store original data // If we're not at the base level, store original data
if (this.currentDrilldownLevel === 0) { if (this.currentDrilldownLevel === 0) {
// Store original data before entering drilldown mode // Store original data before entering drilldown mode
this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels]; this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels];
this.originalPolarAreaChartData = [...this.polarAreaChartData]; this.originalPolarAreaChartData = JSON.parse(JSON.stringify(this.polarAreaChartData));
console.log('Stored original data for drilldown'); console.log('Stored original data for drilldown');
} }
@@ -491,6 +898,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
} }
public chartHovered(e: any): void { public chartHovered(e: any): void {
console.log(e); console.log('Polar chart hovered:', e);
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
} }
} }

View File

@@ -1,14 +1,283 @@
<div style="display: block"> <div style="display: block; height: 100%; width: 100%;">
<!-- Drilldown mode indicator --> <!-- Filter Controls Section -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> <div class="filter-section" *ngIf="hasActiveFilters()">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <!-- Base Filters -->
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
Back to Level {{currentDrilldownLevel - 1}} <h4>Base Filters</h4>
</button> <div class="filter-controls">
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div *ngFor="let filter of baseFilters" class="filter-item">
Back to Main View <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- 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> </button>
</div> </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 --> <!-- No data message -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;"> <div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
@@ -16,7 +285,7 @@
</div> </div>
<!-- Chart display --> <!-- Chart display -->
<div *ngIf="!noDataAvailable"> <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
<canvas baseChart <canvas baseChart
[datasets]="radarChartData" [datasets]="radarChartData"
[labels]="radarChartLabels" [labels]="radarChartLabels"

View File

@@ -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;
}
}
}

View File

@@ -1,5 +1,7 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-radar-chart', selector: 'app-radar-chart',
@@ -62,15 +64,40 @@ export class RadarChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; 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 { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.fetchChartData(); this.fetchChartData();
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('RadarChartComponent input changes:', changes); console.log('RadarChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -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 { fetchChartData(): void {
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
@@ -126,7 +463,49 @@ export class RadarChartComponent implements OnInit, OnChanges {
filterParams = JSON.stringify(filterObj); filterParams = JSON.stringify(filterObj);
} }
} }
console.log('Base filter parameters:', filterParams);
// Add common filters to filter parameters
const commonFilters = this.filterService.getFilterValues();
console.log('Common filters from service:', commonFilters);
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with base filters
const mergedFilterObj = {};
// Add base filters first
if (filterParams) {
try {
const baseFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, baseFilterObj);
} catch (e) {
console.warn('Failed to parse base filter parameters:', e);
}
}
// Add common filters using the field name as the key, not the filter id
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
}
} else {
// Fallback to using filterId as field name if no field is defined
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Final filter parameters:', filterParams);
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/radar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/radar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -298,6 +677,35 @@ export class RadarChartComponent implements OnInit, OnChanges {
} }
} }
// Add common filters to drilldown filter parameters
const commonFilters = this.filterService.getFilterValues();
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with drilldown filters
const mergedFilterObj = {};
// Add drilldown filters first
if (filterParams) {
try {
const drilldownFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, drilldownFilterObj);
} catch (e) {
console.warn('Failed to parse drilldown filter parameters:', e);
}
}
// Add common filters
Object.keys(commonFilters).forEach(key => {
const value = commonFilters[key];
if (value !== undefined && value !== null && value !== '') {
mergedFilterObj[key] = value;
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/radar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/radar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
console.log('Drilldown data URL:', url); console.log('Drilldown data URL:', url);
@@ -321,7 +729,6 @@ export class RadarChartComponent implements OnInit, OnChanges {
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.radarChartLabels = data.chartLabels; this.radarChartLabels = data.chartLabels;
// For radar charts, we need to ensure the data is properly formatted // For radar charts, we need to ensure the data is properly formatted
// Each dataset should have a data array with numeric values
this.radarChartData = data.chartData.map(dataset => ({ this.radarChartData = data.chartData.map(dataset => ({
...dataset, ...dataset,
data: dataset.data ? dataset.data.map(value => { data: dataset.data ? dataset.data.map(value => {
@@ -358,6 +765,7 @@ export class RadarChartComponent implements OnInit, OnChanges {
this.noDataAvailable = true; this.noDataAvailable = true;
this.radarChartLabels = []; this.radarChartLabels = [];
this.radarChartData = []; this.radarChartData = [];
// Keep current data in case of error
} }
); );
} }
@@ -436,7 +844,7 @@ export class RadarChartComponent implements OnInit, OnChanges {
if (this.currentDrilldownLevel === 0) { if (this.currentDrilldownLevel === 0) {
// Store original data before entering drilldown mode // Store original data before entering drilldown mode
this.originalRadarChartLabels = [...this.radarChartLabels]; this.originalRadarChartLabels = [...this.radarChartLabels];
this.originalRadarChartData = [...this.radarChartData]; this.originalRadarChartData = JSON.parse(JSON.stringify(this.radarChartData));
console.log('Stored original data for drilldown'); console.log('Stored original data for drilldown');
} }
@@ -504,6 +912,12 @@ export class RadarChartComponent implements OnInit, OnChanges {
} }
public chartHovered(e: any): void { public chartHovered(e: any): void {
console.log(e); console.log('Radar chart hovered:', e);
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
} }
} }

View File

@@ -1,14 +1,283 @@
<div style="display: block"> <div style="display: block; height: 100%; width: 100%;">
<!-- Drilldown mode indicator --> <!-- Filter Controls Section -->
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;"> <div class="filter-section" *ngIf="hasActiveFilters()">
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span> <!-- Base Filters -->
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
Back to Level {{currentDrilldownLevel - 1}} <h4>Base Filters</h4>
</button> <div class="filter-controls">
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;"> <div *ngFor="let filter of baseFilters" class="filter-item">
Back to Main View <div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
<!-- 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> </button>
</div> </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 --> <!-- No data message -->
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;"> <div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
@@ -16,9 +285,10 @@
</div> </div>
<!-- Chart display --> <!-- Chart display -->
<div *ngIf="!noDataAvailable"> <div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 70px); min-height: 300px;">
<canvas baseChart <canvas baseChart
[datasets]="scatterChartData" [datasets]="scatterChartData"
[options]="scatterChartOptions"
[type]="scatterChartType" [type]="scatterChartType"
(chartHover)="chartHovered($event)" (chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)"> (chartClick)="chartClicked($event)">

View File

@@ -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;
}
}
}

View File

@@ -1,6 +1,8 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { ChartData,ChartDataset } from 'chart.js'; import { ChartData,ChartDataset,ChartOptions } from 'chart.js';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-scatter-chart', selector: 'app-scatter-chart',
@@ -34,9 +36,20 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Multi-layer drilldown configuration inputs // Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
constructor(private dashboardService: Dashboard3Service) { } constructor(
private dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
// Initialize with default data // Initialize with default data
this.fetchChartData(); this.fetchChartData();
} }
@@ -44,6 +57,12 @@ export class ScatterChartComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('ScatterChartComponent input changes:', changes); console.log('ScatterChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -94,6 +113,89 @@ export class ScatterChartComponent implements OnInit, OnChanges {
], ],
}, },
]; ];
public scatterChartOptions: any = {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'linear',
position: 'bottom',
title: {
display: true,
text: 'X Axis'
},
ticks: {
autoSkip: true,
maxTicksLimit: 10,
callback: function(value: any) {
if (typeof value === 'number') {
// Format large numbers for better readability
if (Math.abs(value) >= 1000000) {
return (value / 1000000).toFixed(1) + 'M';
} else if (Math.abs(value) >= 1000) {
return (value / 1000).toFixed(1) + 'K';
}
return value.toString();
}
return value;
}
},
grid: {
display: true,
color: 'rgba(0, 0, 0, 0.1)'
}
},
y: {
title: {
display: true,
text: 'Y Axis'
},
ticks: {
autoSkip: true,
maxTicksLimit: 10,
callback: function(value: any) {
if (typeof value === 'number') {
// Format large numbers for better readability
if (Math.abs(value) >= 1000000) {
return (value / 1000000).toFixed(1) + 'M';
} else if (Math.abs(value) >= 1000) {
return (value / 1000).toFixed(1) + 'K';
}
return value.toString();
}
return value;
}
},
grid: {
display: true,
color: 'rgba(0, 0, 0, 0.1)'
}
}
},
plugins: {
legend: {
display: true,
position: 'top',
},
tooltip: {
callbacks: {
label: function(context: any) {
return `(${context.parsed.x}, ${context.parsed.y})`;
}
}
}
},
layout: {
padding: {
left: 15,
right: 15,
top: 15,
bottom: 60 // Add padding at the bottom to ensure X-axis visibility
}
}
};
public scatterChartType: string = 'scatter'; public scatterChartType: string = 'scatter';
// Multi-layer drilldown state tracking // Multi-layer drilldown state tracking
@@ -107,6 +209,417 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Flag to prevent infinite loops // Flag to prevent infinite loops
private isFetchingData: boolean = false; private isFetchingData: boolean = false;
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
// Initialize filter values with proper default values based on type
private initializeFilterValues(): void {
console.log('Initializing filter values');
// Initialize base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
// Initialize layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.value === undefined || filter.value === null) {
switch (filter.type) {
case 'multiselect':
filter.value = [];
break;
case 'date-range':
filter.value = { start: null, end: null };
break;
case 'toggle':
filter.value = false;
break;
default:
filter.value = '';
}
}
});
}
});
}
console.log('Filter values initialized:', {
baseFilters: this.baseFilters,
drilldownFilters: this.drilldownFilters,
drilldownLayers: this.drilldownLayers
});
}
// Check if there are active filters
hasActiveFilters(): boolean {
return (this.baseFilters && this.baseFilters.length > 0) ||
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
this.hasActiveLayerFilters();
}
// Check if there are active layer filters for current drilldown level
hasActiveLayerFilters(): boolean {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
return layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters &&
this.drilldownLayers[layerIndex].filters.length > 0;
}
return false;
}
// Get active layer filters for current drilldown level
getActiveLayerFilters(): any[] {
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
if (layerIndex < this.drilldownLayers.length &&
this.drilldownLayers[layerIndex].filters) {
return this.drilldownLayers[layerIndex].filters;
}
}
return [];
}
// Get filter options for dropdown/multiselect filters
getFilterOptions(filter: any): string[] {
if (filter.options) {
if (Array.isArray(filter.options)) {
return filter.options;
} else if (typeof filter.options === 'string') {
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
}
}
return [];
}
// Check if an option is selected for multiselect filters
isOptionSelected(filter: any, option: string): boolean {
if (!filter.value) {
return false;
}
if (Array.isArray(filter.value)) {
return filter.value.includes(option);
}
return filter.value === option;
}
// Handle base filter changes
onBaseFilterChange(filter: any): void {
console.log('Base filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchChartData();
}
// Handle multiselect changes
onMultiSelectChange(filter: any, option: string, event: any): void {
const checked = event.target.checked;
// Initialize filter.value as array if it's not already
if (!Array.isArray(filter.value)) {
filter.value = [];
}
if (checked) {
// Add option to array if not already present
if (!filter.value.includes(option)) {
filter.value.push(option);
}
} else {
// Remove option from array
filter.value = filter.value.filter((item: string) => item !== option);
}
// Refresh data when filter changes
this.fetchChartData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchChartData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchChartData();
}
// Toggle multiselect dropdown visibility
toggleMultiselect(filter: any, context: string): void {
const filterId = `${context}-${filter.field}`;
if (this.isMultiselectOpen(filter, context)) {
this.openMultiselects.delete(filterId);
} else {
// Close all other multiselects first
this.openMultiselects.clear();
this.openMultiselects.set(filterId, context);
// Add document click handler to close dropdown when clicking outside
this.addDocumentClickHandler();
}
}
// Add document click handler to close dropdowns when clicking outside
private addDocumentClickHandler(): void {
if (!this.documentClickHandler) {
this.documentClickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is outside any multiselect dropdown
if (!target.closest('.multiselect-container')) {
this.openMultiselects.clear();
this.removeDocumentClickHandler();
}
};
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
setTimeout(() => {
document.addEventListener('click', this.documentClickHandler!);
}, 0);
}
}
// Remove document click handler
private removeDocumentClickHandler(): void {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
this.documentClickHandler = null;
}
}
// Check if multiselect dropdown is open
isMultiselectOpen(filter: any, context: string): boolean {
const filterId = `${context}-${filter.field}`;
return this.openMultiselects.has(filterId);
}
// Get count of selected options for a multiselect filter
getSelectedOptionsCount(filter: any): number {
if (!filter.value) {
return 0;
}
if (Array.isArray(filter.value)) {
return filter.value.length;
}
return 0;
}
// Clear all filters
clearAllFilters(): void {
// Clear base filters
if (this.baseFilters) {
this.baseFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear drilldown filters
if (this.drilldownFilters) {
this.drilldownFilters.forEach(filter => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
// Clear layer filters
if (this.drilldownLayers) {
this.drilldownLayers.forEach(layer => {
if (layer.filters) {
layer.filters.forEach((filter: any) => {
if (filter.type === 'multiselect') {
filter.value = [];
} else if (filter.type === 'date-range') {
filter.value = { start: null, end: null };
} else if (filter.type === 'toggle') {
filter.value = false;
} else {
filter.value = '';
}
});
}
});
}
// Close all multiselect dropdowns
this.openMultiselects.clear();
// Refresh data
this.fetchChartData();
}
// Transform data to scatter chart format
private transformToScatterData(labels: any[], data: any[]): ChartDataset[] {
// For scatter charts, we need to transform the data into scatter format
// Scatter charts expect data in the format: {x: number, y: number}
console.log('Transforming data to scatter format:', { labels, data });
// If we have the expected scatter data format, return it as is
if (data && data.length > 0 && data[0].data && data[0].data.length > 0 &&
typeof data[0].data[0] === 'object' && data[0].data[0].hasOwnProperty('x') &&
data[0].data[0].hasOwnProperty('y')) {
return data;
}
// Transform the data properly for scatter chart
// Assuming labels are x-values and data[0].data are y-values
if (labels && data && data.length > 0 && data[0].data) {
const yValues = data[0].data;
const label = data[0].label || 'Dataset 1';
// Create scatter points from labels (x) and data (y)
const scatterPoints = [];
const minLength = Math.min(labels.length, yValues.length);
for (let i = 0; i < minLength; i++) {
// Convert to numbers if they're strings
const x = typeof labels[i] === 'string' ? parseFloat(labels[i]) : labels[i];
const y = typeof yValues[i] === 'string' ? parseFloat(yValues[i]) : yValues[i];
// Only add valid points
if (!isNaN(x) && !isNaN(y)) {
scatterPoints.push({ x, y });
}
}
// Generate different colors for each point to avoid all points showing the same color
const backgroundColors = [];
const borderColors = [];
for (let i = 0; i < scatterPoints.length; i++) {
// Generate a color based on the point index for variety
const hue = (i * 137.508) % 360; // Use golden angle to spread colors
backgroundColors.push(`hsla(${hue}, 70%, 50%, 0.6)`);
borderColors.push(`hsla(${hue}, 70%, 40%, 1)`);
}
// Create a single dataset with all scatter points
const scatterDatasets: ChartDataset[] = [
{
data: scatterPoints,
label: label,
pointRadius: 8,
pointHoverRadius: 10,
backgroundColor: backgroundColors,
borderColor: borderColors,
borderWidth: 1,
pointHoverBackgroundColor: 'rgba(255, 99, 132, 1)',
}
];
console.log('Transformed scatter data:', scatterDatasets);
return scatterDatasets;
}
// Otherwise, create a default scatter dataset
const scatterDatasets: ChartDataset[] = [
{
data: [
{ x: 1, y: 1 },
{ x: 2, y: 3 },
{ x: 3, y: -2 },
{ x: 4, y: 4 },
{ x: 5, y: -3 },
],
label: 'Dataset 1',
pointRadius: 10,
backgroundColor: [
'red',
'green',
'blue',
'purple',
'yellow',
'brown',
'magenta',
'cyan',
'orange',
'pink'
],
}
];
return scatterDatasets;
}
fetchChartData(): void { fetchChartData(): void {
// Set flag to prevent recursive calls // Set flag to prevent recursive calls
this.isFetchingData = true; this.isFetchingData = true;
@@ -139,7 +652,49 @@ export class ScatterChartComponent implements OnInit, OnChanges {
filterParams = JSON.stringify(filterObj); filterParams = JSON.stringify(filterObj);
} }
} }
console.log('Base filter parameters:', filterParams);
// Add common filters to filter parameters
const commonFilters = this.filterService.getFilterValues();
console.log('Common filters from service:', commonFilters);
if (Object.keys(commonFilters).length > 0) {
// Merge common filters with base filters
const mergedFilterObj = {};
// Add base filters first
if (filterParams) {
try {
const baseFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, baseFilterObj);
} catch (e) {
console.warn('Failed to parse base filter parameters:', e);
}
}
// Add common filters using the field name as the key, not the filter id
Object.keys(commonFilters).forEach(filterId => {
const filterValue = commonFilters[filterId];
// Find the filter definition to get the field name
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
if (filterDef && filterDef.field) {
const fieldName = filterDef.field;
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[fieldName] = filterValue;
}
} else {
// Fallback to using filterId as field name if no field is defined
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
mergedFilterObj[filterId] = filterValue;
}
}
});
if (Object.keys(mergedFilterObj).length > 0) {
filterParams = JSON.stringify(mergedFilterObj);
}
}
console.log('Final filter parameters:', filterParams);
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/scatter?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/scatter?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -165,11 +720,19 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Scatter charts expect data in the format: {x: number, y: number} // Scatter charts expect data in the format: {x: number, y: number}
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData); this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData);
// Update chart options with axis titles
this.updateChartOptionsWithAxisTitles();
console.log('Updated scatter chart with data:', this.scatterChartData); console.log('Updated scatter chart with data:', this.scatterChartData);
} else if (data && data.labels && data.datasets) { } else if (data && data.labels && data.datasets) {
// Handle the original expected format as fallback // Handle the original expected format as fallback
this.noDataAvailable = data.labels.length === 0; this.noDataAvailable = data.labels.length === 0;
this.scatterChartData = data.datasets; this.scatterChartData = data.datasets;
// Update chart options with axis titles
this.updateChartOptionsWithAxisTitles();
console.log('Updated scatter chart with legacy data format:', this.scatterChartData); console.log('Updated scatter chart with legacy data format:', this.scatterChartData);
} else { } else {
console.warn('Scatter chart received data does not have expected structure', data); console.warn('Scatter chart received data does not have expected structure', data);
@@ -197,6 +760,20 @@ export class ScatterChartComponent implements OnInit, OnChanges {
} }
} }
// Update chart options with axis titles
private updateChartOptionsWithAxisTitles(): void {
// Update X axis title
if (this.scatterChartOptions.scales && this.scatterChartOptions.scales.x) {
this.scatterChartOptions.scales.x.title.text = this.xAxis || 'X Axis';
}
// Update Y axis title
if (this.scatterChartOptions.scales && this.scatterChartOptions.scales.y) {
const yAxisLabel = Array.isArray(this.yAxis) ? this.yAxis[0] : this.yAxis;
this.scatterChartOptions.scales.y.title.text = yAxisLabel || 'Y Axis';
}
}
// Fetch drilldown data based on current drilldown level // Fetch drilldown data based on current drilldown level
fetchDrilldownData(): void { fetchDrilldownData(): void {
console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel); console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
@@ -287,24 +864,34 @@ export class ScatterChartComponent implements OnInit, OnChanges {
} }
} }
// Convert drilldownFilters to filter parameters for drilldown level // Add common filters to drilldown filter parameters
let drilldownFilterParams = ''; const commonFilters = this.filterService.getFilterValues();
if (this.drilldownFilters && this.drilldownFilters.length > 0) { if (Object.keys(commonFilters).length > 0) {
const filterObj = {}; // Merge common filters with drilldown filters
this.drilldownFilters.forEach(filter => { const mergedFilterObj = {};
if (filter.field && filter.value) {
filterObj[filter.field] = filter.value; // Add drilldown filters first
if (filterParams) {
try {
const drilldownFilterObj = JSON.parse(filterParams);
Object.assign(mergedFilterObj, drilldownFilterObj);
} catch (e) {
console.warn('Failed to parse drilldown filter parameters:', e);
}
}
// Add common filters
Object.keys(commonFilters).forEach(key => {
const value = commonFilters[key];
if (value !== undefined && value !== null && value !== '') {
mergedFilterObj[key] = value;
} }
}); });
if (Object.keys(filterObj).length > 0) {
drilldownFilterParams = JSON.stringify(filterObj);
}
}
console.log('Drilldown filter parameters:', drilldownFilterParams);
// Use drilldown filters if available, otherwise use layer filters if (Object.keys(mergedFilterObj).length > 0) {
const finalFilterParams = drilldownFilterParams || filterParams; filterParams = JSON.stringify(mergedFilterObj);
console.log('Final filter parameters:', finalFilterParams); }
}
// Log the URL that will be called // Log the URL that will be called
const url = `chart/getdashjson/scatter?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; const url = `chart/getdashjson/scatter?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
@@ -312,7 +899,7 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Fetch data from the dashboard service with parameter field and value // Fetch data from the dashboard service with parameter field and value
// Backend handles filtering, we just pass the parameter field and value // Backend handles filtering, we just pass the parameter field and value
this.dashboardService.getChartData(actualApiUrl, 'scatter', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, finalFilterParams).subscribe( this.dashboardService.getChartData(actualApiUrl, 'scatter', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, filterParams).subscribe(
(data: any) => { (data: any) => {
console.log('Received drilldown data:', data); console.log('Received drilldown data:', data);
if (data === null) { if (data === null) {
@@ -325,7 +912,6 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Handle the actual data structure returned by the API // Handle the actual data structure returned by the API
if (data && data.chartLabels && data.chartData) { if (data && data.chartLabels && data.chartData) {
// For scatter charts, we need to transform the data into scatter format // For scatter charts, we need to transform the data into scatter format
// Scatter charts expect data in the format: {x: number, y: number}
this.noDataAvailable = data.chartLabels.length === 0; this.noDataAvailable = data.chartLabels.length === 0;
this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData); this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData);
console.log('Updated scatter chart with drilldown data:', this.scatterChartData); console.log('Updated scatter chart with drilldown data:', this.scatterChartData);
@@ -349,33 +935,6 @@ export class ScatterChartComponent implements OnInit, OnChanges {
); );
} }
// Transform chart data to scatter chart format
private transformToScatterData(labels: string[], datasets: any[]): ChartDataset[] {
// For scatter charts, we need to transform the data into scatter format
// Scatter charts expect data in the format: {x: number, y: number}
// This is a simple transformation - in a real implementation, you might want to
// create a more sophisticated mapping based on your data structure
return datasets.map((dataset, index) => {
// Create scatter data points
const scatterData = labels.map((label, i) => {
// Use x-axis data as x coordinate, y-axis data as y coordinate
const xValue = dataset.data[i] || 0;
const yValue = i < datasets.length ? (datasets[i].data[index] || 0) : 0;
return { x: xValue, y: yValue };
});
return {
data: scatterData,
label: dataset.label || `Dataset ${index + 1}`,
backgroundColor: dataset.backgroundColor || `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.6)`,
borderColor: dataset.borderColor || 'rgba(0, 0, 0, 1)',
pointRadius: dataset.pointRadius || 5
};
});
}
// Reset to original data (go back to base level) // Reset to original data (go back to base level)
resetToOriginalData(): void { resetToOriginalData(): void {
console.log('Resetting to original data'); console.log('Resetting to original data');
@@ -436,16 +995,18 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Get the index of the clicked element // Get the index of the clicked element
const clickedIndex = e.active[0].index; const clickedIndex = e.active[0].index;
// Get the label of the clicked element // Get the dataset index
// For scatter charts, we might not have labels in the same way as other charts const datasetIndex = e.active[0].datasetIndex;
const clickedLabel = `Point ${clickedIndex}`;
console.log('Clicked on scatter point:', { index: clickedIndex, label: clickedLabel }); // Get the data point
const dataPoint = this.scatterChartData[datasetIndex].data[clickedIndex];
console.log('Clicked on scatter point:', { datasetIndex, clickedIndex, dataPoint });
// If we're not at the base level, store original data // If we're not at the base level, store original data
if (this.currentDrilldownLevel === 0) { if (this.currentDrilldownLevel === 0) {
// Store original data before entering drilldown mode // Store original data before entering drilldown mode
this.originalScatterChartData = [...this.scatterChartData]; this.originalScatterChartData = JSON.parse(JSON.stringify(this.scatterChartData));
console.log('Stored original data for drilldown'); console.log('Stored original data for drilldown');
} }
@@ -487,9 +1048,10 @@ export class ScatterChartComponent implements OnInit, OnChanges {
// Add this click to the drilldown stack // Add this click to the drilldown stack
const stackEntry = { const stackEntry = {
level: nextDrilldownLevel, level: nextDrilldownLevel,
datasetIndex: datasetIndex,
clickedIndex: clickedIndex, clickedIndex: clickedIndex,
clickedLabel: clickedLabel, dataPoint: dataPoint,
clickedValue: clickedLabel // Using label as value for now clickedValue: dataPoint // Using data point as value for now
}; };
this.drilldownStack.push(stackEntry); this.drilldownStack.push(stackEntry);
@@ -513,6 +1075,12 @@ export class ScatterChartComponent implements OnInit, OnChanges {
} }
public chartHovered(e: any): void { public chartHovered(e: any): void {
console.log(e); console.log('Scatter chart hovered:', e);
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
// Clean up document click handler
this.removeDocumentClickHandler();
} }
} }

View File

@@ -1,4 +1,287 @@
<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 -->
<div class="todo-table-container">
<table class="table todo-table">
<thead> <thead>
<th class="c-col">#</th> <th class="c-col">#</th>
<th>Item</th> <th>Item</th>
@@ -8,20 +291,17 @@
<td class="c-col">{{i + 1}}</td> <td class="c-col">{{i + 1}}</td>
<td>{{todo}}</td> <td>{{todo}}</td>
<td style="text-align:right"> <td style="text-align:right">
<a routerLink="." (click)="removeTodo(i)"> <a (click)="removeTodo(i)" class="remove-button">
<clr-icon shape="times"></clr-icon> <clr-icon shape="times"></clr-icon>
</a> </a>
</td> </td>
</tr> </tr>
<tr>
<td></td>
<td>
<input [(ngModel)]="todo" placeholder="Add Todo" class="clr-input">
</td>
<td style="text-align:right">
<a routerLink="." color='primary' (click)="addTodo(todo)">
<clr-icon shape="plus"></clr-icon>
</a>
</td>
</tr>
</table> </table>
</div>
<div class="add-todo-section">
<input [(ngModel)]="todo" (keyup.enter)="addTodo(todo)" placeholder="Add Todo" class="clr-input todo-input">
<button (click)="addTodo(todo)" class="btn btn-primary add-button">
<clr-icon shape="plus"></clr-icon> Add
</button>
</div>
</div>

View File

@@ -0,0 +1,289 @@
// To Do Chart specific styles
.to-do-chart-container {
padding: 20px;
height: 100%;
display: flex;
flex-direction: column;
}
.todo-table-container {
flex: 1;
overflow-y: auto;
max-height: 400px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 20px; // Add margin at the bottom for spacing
.todo-table {
width: 100%;
border-collapse: collapse;
margin: 0;
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
font-weight: bold;
position: sticky;
top: 0;
z-index: 10;
}
tr:hover {
background-color: #f5f5f5;
}
.c-col {
width: 50px;
}
// Add padding at the bottom of the table body
tbody {
tr:last-child td {
padding-bottom: 20px; // Extra padding for the last row
}
}
}
}
.add-todo-section {
display: flex;
gap: 10px;
margin-top: 15px;
padding: 20px; // Increased padding all around
border-top: 1px solid #eee;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
.todo-input {
flex: 1;
padding: 12px; // Increased padding for better touch targets
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.add-button {
white-space: nowrap;
padding: 12px 20px; // Increased padding for better touch targets
}
}
.remove-button {
background: none;
border: none;
cursor: pointer;
padding: 8px; // Increased padding for better touch targets
border-radius: 3px;
color: #dc3545;
&:hover {
background-color: #e0e0e0;
}
}
.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;
}
}
.add-todo-section {
flex-direction: column;
}
.to-do-chart-container {
padding: 15px; // Adjust padding for mobile
}
}

View File

@@ -1,4 +1,7 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { FilterService } from '../../common-filter/filter.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-to-do-chart', selector: 'app-to-do-chart',
@@ -21,15 +24,49 @@ export class ToDoChartComponent implements OnInit, OnChanges {
@Input() datasource: string; @Input() datasource: string;
@Input() fieldName: string; @Input() fieldName: string;
@Input() connection: number; // Add connection input @Input() connection: number; // Add connection input
// Drilldown configuration inputs
@Input() drilldownEnabled: boolean = false;
@Input() drilldownApiUrl: string;
@Input() drilldownXAxis: string;
@Input() drilldownYAxis: string;
@Input() drilldownParameter: string; // Add drilldown parameter input
@Input() baseFilters: any[] = []; // Add base filters input
@Input() drilldownFilters: any[] = []; // Add drilldown filters input
// Multi-layer drilldown configuration inputs
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
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 dashboardService: Dashboard3Service,
private filterService: FilterService
) { }
ngOnInit(): void { ngOnInit(): void {
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchToDoData();
})
);
// Initialize with default data
this.fetchToDoData();
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
console.log('ToDoChartComponent input changes:', changes); console.log('ToDoChartComponent input changes:', changes);
// Initialize filter values if they haven't been initialized yet
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
this.initializeFilterValues();
this.filtersInitialized = true;
}
// Check if any of the key properties have changed // Check if any of the key properties have changed
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
@@ -46,26 +83,119 @@ export class ToDoChartComponent implements OnInit, OnChanges {
data: any; data: any;
todo: string; todo: string;
todoList = ['todo 1']; todoList: string[] = [];
// Add properties for filter functionality
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
private filtersInitialized: boolean = false;
// Subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
fetchToDoData(): void { fetchToDoData(): void {
// If we have the necessary data, fetch to-do data from the service // If we have the necessary data, fetch to-do data from the service
if (this.table) { if (this.table && this.xAxis) {
console.log('Fetching to-do data for:', { table: this.table }); console.log('Fetching to-do data for:', { table: this.table, xAxis: this.xAxis, connection: this.connection });
// For to-do chart, we might want to fetch data differently // Convert baseFilters to filter parameters
// This is a placeholder implementation - you may need to adjust based on your API let filterParams = '';
console.log('To-do chart would fetch data from table:', this.table); if (this.baseFilters && this.baseFilters.length > 0) {
const filterObj = {};
this.baseFilters.forEach(filter => {
if (filter.field && filter.value) {
filterObj[filter.field] = filter.value;
}
});
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
}
// In a real implementation, you would connect to your service here // Add common filters to filter parameters
// For now, we'll just keep the default to-do list 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 { } else {
console.log('Missing required data for to-do chart:', { table: this.table }); // 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);
// Fetch data from the dashboard service
this.dashboardService.getChartData(this.table, 'todo', this.xAxis, '', this.connection, '', '', filterParams).subscribe(
(data: any) => {
console.log('Received to-do chart data:', data);
if (data === null) {
console.warn('To-do chart API returned null data. Check if the API endpoint is working correctly.');
this.todoList = [];
return;
}
// Handle the actual data structure returned by the API
if (data && data.chartLabels) {
// Use chartLabels as the todo items
this.todoList = data.chartLabels;
} else if (data && data.labels) {
// Fallback to labels if chartLabels is not available
this.todoList = data.labels;
} else {
console.warn('To-do chart received data does not have expected structure', data);
this.todoList = [];
}
},
(error) => {
console.error('Error fetching to-do chart data:', error);
this.todoList = [];
}
);
} else {
console.log('Missing required data for to-do chart:', { table: this.table, xAxis: this.xAxis });
// Initialize with default data if no table is specified
if (this.todoList.length === 0) {
this.todoList = ['Sample Task 1', 'Sample Task 2', 'Sample Task 3'];
}
} }
} }
public addTodo(todo: string) { public addTodo(todo: string) {
this.todoList.push(todo); if (todo && todo.trim() !== '') {
this.todoList.push(todo.trim());
this.todo = ''; // Clear the input field
}
} }
public removeTodo(todoIx: number) { public removeTodo(todoIx: number) {
@@ -73,4 +203,353 @@ export class ToDoChartComponent implements OnInit, OnChanges {
this.todoList.splice(todoIx, 1); this.todoList.splice(todoIx, 1);
} }
} }
// Navigate back to previous drilldown level
navigateBack(): void {
console.log('Navigating back, current stack:', this.drilldownStack);
console.log('Current level:', this.currentDrilldownLevel);
if (this.drilldownStack.length > 0) {
// Remove the last entry from the stack
const removedEntry = this.drilldownStack.pop();
console.log('Removed entry from stack:', removedEntry);
// Update the current drilldown level
this.currentDrilldownLevel = this.drilldownStack.length;
console.log('New level after pop:', this.currentDrilldownLevel);
console.log('Stack after pop:', this.drilldownStack);
if (this.drilldownStack.length > 0) {
// Fetch data for the previous level
console.log('Fetching data for previous level');
this.fetchToDoData();
} else {
// Back to base level
console.log('Back to base level, resetting to original data');
this.todoList = [...this.originalTodoList];
}
} else {
// Already at base level, reset to original data
console.log('Already at base level, resetting to original data');
this.todoList = [...this.originalTodoList];
}
}
// 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.fetchToDoData();
}
// Handle drilldown filter changes
onDrilldownFilterChange(filter: any): void {
console.log('Drilldown filter changed:', filter);
// Refresh data when filter changes
this.fetchToDoData();
}
// Handle layer filter changes
onLayerFilterChange(filter: any): void {
console.log('Layer filter changed:', filter);
// Refresh data when filter changes
this.fetchToDoData();
}
// 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.fetchToDoData();
}
// Handle date range changes
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
filter.value = dateRange;
// Refresh data when filter changes
this.fetchToDoData();
}
// Handle toggle changes
onToggleChange(filter: any, checked: boolean): void {
filter.value = checked;
// Refresh data when filter changes
this.fetchToDoData();
}
// 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.fetchToDoData();
}
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();
}
} }

View File

@@ -11,6 +11,9 @@
<h3>{{ 'all_dashboard' | translate }}</h3> <h3>{{ 'all_dashboard' | translate }}</h3>
</div> </div>
<div class="clr-col-4" style="text-align: right;"> <div class="clr-col-4" style="text-align: right;">
<button class="btn btn-success" [routerLink]="['/cns-portal/shield-dashboard']">
<clr-icon shape="shield"></clr-icon>Shield Dashboard
</button>
<button id="add" class="btn btn-primary" (click)="gotoadd()"> <button id="add" class="btn btn-primary" (click)="gotoadd()">
<clr-icon shape="plus"></clr-icon>{{ 'dashboard_builder' | translate }} <clr-icon shape="plus"></clr-icon>{{ 'dashboard_builder' | translate }}
</button> </button>
@@ -112,6 +115,3 @@
</div> </div>
</div> </div>
</clr-modal> </clr-modal>

View File

@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-bar-runner', selector: 'app-bar-runner',
@@ -24,8 +28,19 @@ export class BarRunnerComponent implements OnInit {
JsonData; JsonData;
barData; barData;
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, ConnectionId: number; // Add ConnectionId property
private router : Router,) { }
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(
private Dashtestservive:DashrunnerService,
private route: ActivatedRoute,
private dashboardService: Dashboard3Service,
private router : Router,
// Add FilterService to constructor
private filterService: FilterService
) { }
barChartLabels: any[] = []; barChartLabels: any[] = [];
barChartType: string = 'bar'; barChartType: string = 'bar';
@@ -47,6 +62,13 @@ export class BarRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -74,22 +96,62 @@ export class BarRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.barChartLegend = ChartObject[i].chartlegend; this.barChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Bar Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.barChartData = this.JsonData.barChartData;
this.barChartLabels = this.JsonData.barChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
}); });
} }
// Fetch chart data with filter support
fetchChartData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
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 !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('BarRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Bar Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.JsonData = Ldata;
this.barChartData = this.JsonData.barChartData;
this.barChartLabels = this.JsonData.barChartLabels;
},(error) => {
console.log(error);
});
}
}
generatePDFFile(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -98,4 +160,16 @@ export class BarRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('BarRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('BarRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -5,6 +5,10 @@ import { ChartConfiguration, ChartDataset, ChartOptions } from 'chart.js';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
import { DashrunnerService } from '../dashrunner.service'; import { DashrunnerService } from '../dashrunner.service';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-bubble-runner', selector: 'app-bubble-runner',
@@ -25,9 +29,15 @@ export class BubbleRunnerComponent implements OnInit {
JsonData; JsonData;
lineChartNoLabels: [] = []; lineChartNoLabels: [] = [];
ChartLegend = false; ChartLegend = false;
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router,) { } private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
public bubbleChartOptions: ChartConfiguration['options'] = { public bubbleChartOptions: ChartConfiguration['options'] = {
// scales: { // scales: {
@@ -87,6 +97,13 @@ export class BubbleRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -112,22 +129,62 @@ export class BubbleRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.ChartLegend = ChartObject[i].chartlegend; this.ChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Bubble Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.bubbleChartData = this.JsonData.bubbleChartData;
// this.radarChartLabels = this.JsonData.radarChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
}); });
} }
// Fetch chart data with filter support
fetchChartData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
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 !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('BubbleRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Bubble Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.JsonData = Ldata;
this.bubbleChartData = this.JsonData.bubbleChartData;
// this.radarChartLabels = this.JsonData.radarChartLabels;
},(error) => {
console.log(error);
});
}
}
generatePDFFile(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -136,5 +193,18 @@ export class BubbleRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('BubbleRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('BubbleRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -0,0 +1,73 @@
<div class="compact-filter">
<div class="filter-header" (click)="toggleFilter()">
<span class="filter-label" *ngIf="filterLabel">{{ filterLabel }}</span>
<span class="filter-key" *ngIf="!filterLabel && filterKey">{{ filterKey }}</span>
<span class="filter-type">({{ filterType }})</span>
<clr-icon shape="caret down" class="expand-icon" *ngIf="!isExpanded"></clr-icon>
<clr-icon shape="caret up" class="expand-icon" *ngIf="isExpanded"></clr-icon>
</div>
<div class="filter-content" *ngIf="isExpanded">
<!-- Text Filter -->
<div class="filter-control" *ngIf="filterType === 'text'">
<input type="text"
[(ngModel)]="filterValue"
(ngModelChange)="onFilterValueChange($event)"
[placeholder]="filterLabel || filterKey"
class="clr-input compact-input">
</div>
<!-- Dropdown Filter -->
<div class="filter-control" *ngIf="filterType === 'dropdown'">
<select [(ngModel)]="filterValue"
(ngModelChange)="onFilterValueChange($event)"
class="clr-select compact-select">
<option value="">{{ filterLabel || filterKey }}</option>
<option *ngFor="let option of filterOptions; let i = index" [value]="option">{{ option }}</option>
</select>
</div>
<!-- Multi-Select Filter -->
<div class="filter-control" *ngIf="filterType === 'multiselect'">
<div class="multiselect-container">
<div class="checkbox-group">
<div *ngFor="let option of filterOptions; let i = index" class="checkbox-item">
<input type="checkbox"
[checked]="isOptionSelected(option)"
(change)="onMultiSelectChange(option, $event)"
[id]="'checkbox-' + filterKey + '-' + i"
class="clr-checkbox">
<label [for]="'checkbox-' + filterKey + '-' + i" class="checkbox-label">{{ option }}</label>
</div>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div class="filter-control date-range" *ngIf="filterType === 'date-range'">
<div class="date-input-group">
<input type="date"
[(ngModel)]="filterValue.start"
(ngModelChange)="onDateRangeChange({ start: $event, end: filterValue.end })"
placeholder="Start Date"
class="clr-input compact-date">
<span class="date-separator">to</span>
<input type="date"
[(ngModel)]="filterValue.end"
(ngModelChange)="onDateRangeChange({ start: filterValue.start, end: $event })"
placeholder="End Date"
class="clr-input compact-date">
</div>
</div>
<!-- Toggle Filter -->
<div class="filter-control toggle" *ngIf="filterType === 'toggle'">
<input type="checkbox"
[(ngModel)]="filterValue"
(ngModelChange)="onToggleChange($event)"
clrToggle
class="clr-toggle">
<label class="toggle-label">{{ filterLabel || filterKey }}</label>
</div>
</div>
</div>

View File

@@ -0,0 +1,149 @@
.compact-filter {
display: block;
min-width: 200px;
max-width: 300px;
margin: 8px;
padding: 0;
background: #ffffff;
border: 1px solid #d7d7d7;
border-radius: 4px;
font-size: 14px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
cursor: pointer;
background: #f8f8f8;
border-bottom: 1px solid #eaeaea;
border-radius: 4px 4px 0 0;
&:hover {
background: #f0f0f0;
}
.filter-label, .filter-key {
font-weight: 500;
color: #333333;
flex-grow: 1;
}
.filter-type {
font-size: 12px;
color: #666666;
margin: 0 8px;
background: #eaeaea;
padding: 2px 8px;
border-radius: 12px;
}
.expand-icon {
width: 16px;
height: 16px;
color: #666666;
}
}
.filter-content {
padding: 15px;
.filter-control {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
&.date-range {
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
.date-separator {
font-size: 14px;
color: #666666;
}
}
}
&.toggle {
display: flex;
align-items: center;
gap: 8px;
}
}
}
.compact-input,
.compact-select,
.compact-date {
width: 100%;
padding: 8px 12px;
font-size: 14px;
border: 1px solid #d7d7d7;
border-radius: 4px;
background: #ffffff;
&:focus {
outline: none;
border-color: #0072ce;
box-shadow: 0 0 0 1px #0072ce;
}
}
.compact-select {
height: 36px;
}
.multiselect-container {
max-height: 200px;
overflow-y: auto;
border: 1px solid #d7d7d7;
border-radius: 4px;
padding: 10px;
background: #ffffff;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
.clr-checkbox {
width: 16px;
height: 16px;
cursor: pointer;
}
.checkbox-label {
font-size: 14px;
margin: 0;
cursor: pointer;
color: #333333;
}
}
.toggle-label {
margin: 0;
font-size: 14px;
color: #333333;
}
.clr-toggle {
margin: 0;
}
}
// Host styling
:host {
display: block;
}

View File

@@ -0,0 +1,245 @@
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
import { FilterService, Filter } from '../../../dashboardnew/common-filter/filter.service';
@Component({
selector: 'app-compact-filter-runner',
templateUrl: './compact-filter-runner.component.html',
styleUrls: ['./compact-filter-runner.component.scss']
})
export class CompactFilterRunnerComponent implements OnInit, OnChanges {
@Input() filterKey: string = '';
@Input() filterType: string = 'text';
@Input() filterOptions: string[] = [];
@Input() filterLabel: string = '';
@Input() apiUrl: string = '';
@Input() connection: number | undefined;
@Output() filterChange = new EventEmitter<any>();
selectedFilter: Filter | null = null;
filterValue: any = '';
availableFilters: Filter[] = [];
availableKeys: string[] = [];
availableValues: string[] = [];
isExpanded: boolean = false; // Add expansion state
constructor(
private filterService: FilterService
) {
console.log('=== COMPACT FILTER RUNNER CONSTRUCTOR CALLED ===');
}
ngOnInit(): void {
console.log('=== COMPACT FILTER RUNNER DEBUG INFO ===');
console.log('Component initialized with inputs:');
console.log('- filterKey:', this.filterKey);
console.log('- filterType:', this.filterType);
console.log('- filterOptions:', this.filterOptions);
console.log('- filterLabel:', this.filterLabel);
console.log('- apiUrl:', this.apiUrl);
console.log('- connection:', this.connection);
console.log('========================================');
// Register this filter with the filter service
this.registerFilter();
// Subscribe to filter definitions to get available filters
this.filterService.filters$.subscribe(filters => {
this.availableFilters = filters;
console.log('Available filters updated:', filters);
this.updateSelectedFilter();
});
// Subscribe to filter state changes
this.filterService.filterState$.subscribe(state => {
console.log('Filter state updated:', state);
if (this.selectedFilter && state.hasOwnProperty(this.selectedFilter.id)) {
this.filterValue = state[this.selectedFilter.id];
console.log('Filter value updated for', this.selectedFilter.id, ':', this.filterValue);
}
});
}
ngOnChanges(changes: SimpleChanges): void {
console.log('=== COMPACT FILTER RUNNER CHANGES DEBUG ===');
console.log('Component inputs changed:', changes);
// If filterKey or filterType changes, re-register the filter
if (changes.filterKey || changes.filterType || changes.filterOptions) {
console.log('Re-registering filter due to input changes');
this.registerFilter();
}
console.log('==========================================');
}
// Toggle filter expansion
toggleFilter(): void {
this.isExpanded = !this.isExpanded;
}
// Register this filter with the filter service
registerFilter(): void {
console.log('Registering filter with key:', this.filterKey, 'type:', this.filterType);
if (this.filterKey) {
// Get current filter values from the service
const currentFilterValues = this.filterService.getFilterValues();
console.log('Current filter values from service:', currentFilterValues);
// Create a filter definition for this compact filter
const filterDef: Filter = {
id: `${this.filterKey}`,
field: this.filterKey,
label: this.filterLabel || this.filterKey,
type: this.filterType as any,
options: this.filterOptions,
value: this.filterValue // Use the current filter value
};
console.log('Created filter definition:', filterDef);
// Get current filters
const currentFilters = this.filterService.getFilters();
console.log('Current filters from service:', currentFilters);
// Check if this filter is already registered
const existingFilterIndex = currentFilters.findIndex(f => f.id === filterDef.id);
console.log('Existing filter index:', existingFilterIndex);
if (existingFilterIndex >= 0) {
// Preserve the existing filter configuration
const existingFilter = currentFilters[existingFilterIndex];
console.log('Found existing filter:', existingFilter);
// Preserve the existing filter value if it exists in the service
if (currentFilterValues.hasOwnProperty(existingFilter.id)) {
filterDef.value = currentFilterValues[existingFilter.id];
this.filterValue = filterDef.value; // Update local value
console.log('Using value from service:', filterDef.value);
} else if (existingFilter.value !== undefined) {
// Fallback to existing filter's value if no service value
filterDef.value = existingFilter.value;
this.filterValue = filterDef.value;
console.log('Using value from existing filter:', filterDef.value);
}
// Preserve other configuration properties
filterDef.label = existingFilter.label;
filterDef.options = existingFilter.options || this.filterOptions;
// Update existing filter
currentFilters[existingFilterIndex] = filterDef;
console.log('Updated existing filter:', filterDef);
} else {
// For new filters, check if there's already a value in the service
if (currentFilterValues.hasOwnProperty(filterDef.id)) {
filterDef.value = currentFilterValues[filterDef.id];
this.filterValue = filterDef.value; // Update local value
console.log('Using value from service for new filter:', filterDef.value);
}
// Add new filter
currentFilters.push(filterDef);
console.log('Added new filter:', filterDef);
}
// Update the filter service with the new filter list
this.filterService.setFilters(currentFilters);
// Update the selected filter reference
this.selectedFilter = filterDef;
console.log('Selected filter set to:', this.selectedFilter);
} else {
console.log('No filterKey provided, skipping filter registration');
}
}
updateSelectedFilter(): void {
console.log('Updating selected filter. Filter key:', this.filterKey, 'Available filters:', this.availableFilters);
if (this.filterKey && this.availableFilters.length > 0) {
this.selectedFilter = this.availableFilters.find(f => f.field === this.filterKey) || null;
console.log('Found selected filter:', this.selectedFilter);
if (this.selectedFilter) {
// Get current value for this filter from the service
const currentState = this.filterService.getFilterValues();
console.log('Current state from service:', currentState);
const filterValue = currentState[this.selectedFilter.id];
if (filterValue !== undefined) {
this.filterValue = filterValue;
} else if (this.selectedFilter.value !== undefined) {
// Use the filter's default value if no service value
this.filterValue = this.selectedFilter.value;
} else {
// Use the current filter value as fallback
this.filterValue = this.filterValue || '';
}
console.log('Updated selected filter value:', this.filterValue);
}
}
}
onFilterValueChange(value: any): void {
console.log('Filter value changed:', value);
if (this.selectedFilter) {
this.filterValue = value;
this.filterService.updateFilterValue(this.selectedFilter.id, value);
this.filterChange.emit({ filterId: this.selectedFilter.id, value: value });
// Update the filter definition in the service to reflect the new value
const currentFilters = this.filterService.getFilters();
const filterIndex = currentFilters.findIndex(f => f.id === this.selectedFilter.id);
if (filterIndex >= 0) {
currentFilters[filterIndex].value = value;
this.filterService.setFilters(currentFilters);
}
}
}
onToggleChange(checked: boolean): void {
this.onFilterValueChange(checked);
}
onDateRangeChange(dateRange: { start: string | null, end: string | null }): void {
this.onFilterValueChange(dateRange);
}
// Handle multi-select changes
onMultiSelectChange(option: string, event: any): void {
const checked = event.target.checked;
// Initialize filterValue as array if it's not already
if (!Array.isArray(this.filterValue)) {
this.filterValue = [];
}
if (checked) {
// Add option to array if not already present
if (!this.filterValue.includes(option)) {
this.filterValue.push(option);
}
} else {
// Remove option from array
this.filterValue = this.filterValue.filter((item: string) => item !== option);
}
// Emit the change
this.onFilterValueChange(this.filterValue);
}
// Add method to check if an option is selected for checkboxes (needed for proper UI rendering)
isOptionSelected(option: string): boolean {
console.log('Checking if option is selected:', option, 'Current filter value:', this.filterValue);
if (!this.filterValue) {
return false;
}
// Ensure filterValue is an array for multiselect
if (!Array.isArray(this.filterValue)) {
this.filterValue = [];
return false;
}
return this.filterValue.includes(option);
}
}

View File

@@ -160,7 +160,27 @@ getlinechart(): any[] {
return this._http.get(url); return this._http.get(url);
} }
// New method to support filters
public getChartDataWithFilters(tableName: string, jobType: string, xAxis:any, yAxes:any, sureId: number | undefined, parameterField: string, parameterValue: string, filterParams: string): Observable<any> {
let url = `${baseUrl}/chart/getdashjson/${jobType}?tableName=${tableName}&xAxis=${xAxis}&yAxes=${yAxes}`;
// Add sureId if provided
if (sureId) {
url += `&sureId=${sureId}`;
}
// Add parameter field and value if provided
if (parameterField && parameterValue) {
url += `&parameter=${encodeURIComponent(parameterField)}&parameterValue=${encodeURIComponent(parameterValue)}`;
}
// Add filter parameters if provided
if (filterParams) {
url += `&filters=${encodeURIComponent(filterParams)}`;
}
return this._http.get(url);
}
////////////////////////////////////////////// //////////////////////////////////////////////

View File

@@ -26,7 +26,14 @@
<!-- <span><button class="btn btn-primary" (click)="Export(item.name)">Export</button></span> --> <!-- <span><button class="btn btn-primary" (click)="Export(item.name)">Export</button></span> -->
<!-- <span><app-line-runner (buttonClicked)="generatePDFFile()"></app-line-runner></span> --> <!-- <span><app-line-runner (buttonClicked)="generatePDFFile()"></app-line-runner></span> -->
<!-- <h4 style="margin-top: 10px; margin-left: 10px;">{{ item.charttitle }}</h4> --> <!-- <h4 style="margin-top: 10px; margin-left: 10px;">{{ item.charttitle }}</h4> -->
<ndc-dynamic class="no-drag" [ndcDynamicComponent]="item.component" (moduleInfo)="display($event)"></ndc-dynamic>
<ndc-dynamic class="no-drag"
[ndcDynamicComponent]="item.component"
[ndcDynamicInputs]="getComponentInputs(item)"
(moduleInfo)="display($event)">
</ndc-dynamic>
</gridster-item> </gridster-item>
</gridster> </gridster>
</div> </div>

View File

@@ -17,6 +17,10 @@ import { BubbleRunnerComponent } from './bubble-runner/bubble-runner.component';
import { ScatterRunnerComponent } from './scatter-runner/scatter-runner.component'; import { ScatterRunnerComponent } from './scatter-runner/scatter-runner.component';
import { PolarRunnerComponent } from './polar-runner/polar-runner.component'; import { PolarRunnerComponent } from './polar-runner/polar-runner.component';
import { RadarRunnerComponent } from './radar-runner/radar-runner.component'; import { RadarRunnerComponent } from './radar-runner/radar-runner.component';
// Add FilterService import
import { FilterService } from '../../dashboardnew/common-filter/filter.service';
// Add CompactFilterRunnerComponent import
import { CompactFilterRunnerComponent } from './compact-filter-runner/compact-filter-runner.component';
@Component({ @Component({
selector: 'app-dashrunnerline', selector: 'app-dashrunnerline',
@@ -44,10 +48,13 @@ export class DashrunnerlineComponent implements OnInit {
{ name: "Radar Chart", componentInstance: RadarRunnerComponent }, { name: "Radar Chart", componentInstance: RadarRunnerComponent },
{ name: "Grid View", componentInstance: GridRunnerComponent }, { name: "Grid View", componentInstance: GridRunnerComponent },
{ name: "To Do Chart", componentInstance: TodoRunnerComponent }, { name: "To Do Chart", componentInstance: TodoRunnerComponent },
{ name: "Compact Filter", componentInstance: CompactFilterRunnerComponent }, // Add Compact Filter Runner
]; ];
constructor(private Dashtestservive:DashrunnerService, private dashboardService: Dashboard3Service,private route: ActivatedRoute, constructor(private Dashtestservive:DashrunnerService, private dashboardService: Dashboard3Service,private route: ActivatedRoute,
private router : Router,) { } private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
ngOnInit(): void { ngOnInit(): void {
@@ -288,4 +295,54 @@ dashboard_name = "Dashtest";
console.log('Button clicked in SomeComponent'); console.log('Button clicked in SomeComponent');
// Add your custom logic here when the button is clicked in SomeComponent // Add your custom logic here when the button is clicked in SomeComponent
} }
// Method to provide inputs for dynamic components based on their type
getComponentInputs(item: any): any {
const inputs: any = {};
// Common inputs for all components
if (item.table !== undefined) inputs.table = item.table;
if (item.xAxis !== undefined) inputs.xAxis = item.xAxis;
if (item.yAxis !== undefined) inputs.yAxis = item.yAxis;
if (item.connection !== undefined) inputs.connection = item.connection;
if (item.charttitle !== undefined) inputs.charttitle = item.charttitle;
if (item.chartlegend !== undefined) inputs.chartlegend = item.chartlegend;
if (item.showlabel !== undefined) inputs.showlabel = item.showlabel;
// Compact Filter specific inputs
if (item.name === 'Compact Filter') {
console.log('=== COMPACT FILTER INPUTS DEBUG ===');
console.log('Item data for compact filter:', item);
if (item.filterKey !== undefined) inputs.filterKey = item.filterKey;
if (item.filterType !== undefined) inputs.filterType = item.filterType;
if (item.filterLabel !== undefined) inputs.filterLabel = item.filterLabel;
if (item.filterOptions !== undefined) inputs.filterOptions = item.filterOptions;
if (item.table !== undefined) inputs.apiUrl = item.table; // Use table as API URL for compact filter
if (item.connection !== undefined) inputs.connection = item.connection ? parseInt(item.connection, 10) : undefined;
console.log('Final inputs for compact filter:', inputs);
console.log('==============================');
}
// Grid View specific inputs
if (item.name === 'Grid View') {
if (item.baseFilters !== undefined) inputs.baseFilters = item.baseFilters;
}
// Chart specific inputs
if (item.name.includes('Chart') && item.name !== 'Compact Filter') {
if (item.baseFilters !== undefined) inputs.baseFilters = item.baseFilters;
if (item.drilldownEnabled !== undefined) inputs.drilldownEnabled = item.drilldownEnabled;
if (item.drilldownApiUrl !== undefined) inputs.drilldownApiUrl = item.drilldownApiUrl;
if (item.drilldownXAxis !== undefined) inputs.drilldownXAxis = item.drilldownXAxis;
if (item.drilldownYAxis !== undefined) inputs.drilldownYAxis = item.drilldownYAxis;
if (item.drilldownParameter !== undefined) inputs.drilldownParameter = item.drilldownParameter;
if (item.drilldownFilters !== undefined) inputs.drilldownFilters = item.drilldownFilters;
if (item.drilldownLayers !== undefined) inputs.drilldownLayers = item.drilldownLayers;
}
console.log('Component inputs for', item.name, ':', inputs);
return inputs;
}
} }

View File

@@ -5,6 +5,10 @@ import { ChartDataset, ChartType, } from 'chart.js';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-doughnut-runner', selector: 'app-doughnut-runner',
@@ -33,10 +37,16 @@ export class DoughnutRunnerComponent implements OnInit {
"chartLabels": ["Project", "Repository", "Wireframe"] "chartLabels": ["Project", "Repository", "Wireframe"]
} }
doughnutChartType: ChartType = 'doughnut'; doughnutChartType: ChartType = 'doughnut';
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router,) { } private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
ngOnInit(): void { ngOnInit(): void {
this.doughnutChartData = this.doughnutData.chartData; this.doughnutChartData = this.doughnutData.chartData;
this.doughnutChartLabels = this.doughnutData.chartLabels; this.doughnutChartLabels = this.doughnutData.chartLabels;
@@ -44,6 +54,14 @@ export class DoughnutRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
this.workflowLine = data.dashbord1_Line[0].model; this.workflowLine = data.dashbord1_Line[0].model;
@@ -70,22 +88,62 @@ export class DoughnutRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.doughnutChartLegend = ChartObject[i].chartlegend; this.doughnutChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Doughnut Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.doughnutChartData = this.JsonData.chartData;
this.doughnutChartLabels = this.JsonData.chartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
}); });
} }
// Fetch chart data with filter support
fetchChartData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
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 !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('DoughnutRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Doughnut Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.JsonData = Ldata;
this.doughnutChartData = this.JsonData.chartData;
this.doughnutChartLabels = this.JsonData.chartLabels;
},(error) => {
console.log(error);
});
}
}
generatePDFFile(){ generatePDFFile(){
// this.buttonClicked.emit(); // this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -94,6 +152,19 @@ export class DoughnutRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('DoughnutRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('DoughnutRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -39,16 +39,35 @@
</div> --> </div> -->
<div><button class="btn btn-primary" (click)="generatePDFFile()">Export</button></div> <div><button class="btn btn-primary" (click)="generatePDFFile()">Export</button></div>
<div style="max-height: 400px; overflow: auto; padding: 10px;"> <div style="max-height: 400px; overflow: auto; padding: 10px;">
<table class="table"> <!-- Debug information -->
<div *ngIf="false" style="background-color: #f0f0f0; padding: 10px; margin-bottom: 10px;">
<h4>Debug Information</h4>
<p><strong>TableName:</strong> {{ TableName }}</p>
<p><strong>XAxis:</strong> {{ XAxis }}</p>
<p><strong>YAxis:</strong> {{ YAxis }}</p>
<p><strong>Rows:</strong> {{ rows?.length }} items</p>
<p><strong>Headers:</strong> {{ getHeaders() | json }}</p>
<div *ngIf="error"><strong>Error:</strong> {{ error }}</div>
</div>
<div *ngIf="error" class="error_mess">
{{ error }}
</div>
<table class="table" *ngIf="rows && rows.length > 0; else noData">
<thead> <thead>
<tr> <tr>
<th *ngFor="let co of getHeaders();let i=index">{{co}}</th> <th *ngFor="let co of getHeaders();let i=index">{{co}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let item of rows?.slice()?.reverse()"> <tr *ngFor="let item of rows">
<td *ngFor="let key of getHeaders()">{{item[key]}}</td> <td *ngFor="let key of getHeaders()">{{item[key]}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<ng-template #noData>
<p *ngIf="!error">No data available</p>
</ng-template>
</div> </div>

View File

@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-grid-runner', selector: 'app-grid-runner',
@@ -26,57 +30,150 @@ export class GridRunnerComponent implements OnInit {
public DashtestboardArray: DashboardContentModel[] = []; public DashtestboardArray: DashboardContentModel[] = [];
workflowLine; workflowLine;
TableName; TableName;
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor( constructor(
private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, private Dashtestservive:DashrunnerService,
private router : Router private route: ActivatedRoute,
private dashboardService: Dashboard3Service,
private router : Router,
// Add FilterService to constructor
private filterService: FilterService
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log('GridRunner: Component initialized with editId:', this.editId);
// this.getbyId(); // this.getbyId();
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
console.log('GridRunner: Filter state changed:', filters);
// When filters change, refresh the grid data
this.fetchGridData();
})
);
this.dashboardService.getById(this.editId).subscribe((data) => { this.dashboardService.getById(this.editId).subscribe((data) => {
console.log(data); console.log('GridRunner: Received dashboard data:', data);
this.workflowLine = data.dashbord1_Line[0].model; this.workflowLine = data.dashbord1_Line[0].model;
const dash = JSON.parse(this.workflowLine); const dash = JSON.parse(this.workflowLine);
// this.DashtestboardArray = dash.dashboard; // this.DashtestboardArray = dash.dashboard;
// console.log(this.DashtestboardArray); // console.log(this.DashtestboardArray);
const ChartObject = dash.dashboard.filter(obj => obj.name === "Grid View"); const ChartObject = dash.dashboard.filter(obj => obj.name === "Grid View");
console.log(ChartObject); console.log('GridRunner: ChartObject for Grid View:', ChartObject);
for (let i = 0; i < ChartObject.length; i++) { for (let i = 0; i < ChartObject.length; i++) {
const ids = this.Dashtestservive.getgridview(); const ids = this.Dashtestservive.getgridview();
console.log('GridRunner: Current gridview ids:', ids);
console.log('GridRunner: Checking chartid:', ChartObject[i].chartid);
// console.log(ids); // console.log(ids);
if (ids.includes(ChartObject[i].chartid)) { if (ids.includes(ChartObject[i].chartid)) {
// If the chartid is already in the ids array, continue to the next iteration // If the chartid is already in the ids array, continue to the next iteration
console.log('GridRunner: Skipping chartid as it already exists:', ChartObject[i].chartid);
continue; continue;
} }
console.log('GridRunner: Adding new chartid:', ChartObject[i].chartid);
this.Dashtestservive.setgridview(ChartObject[i].chartid); this.Dashtestservive.setgridview(ChartObject[i].chartid);
const id = ids[i];
console.log(id);
if (ChartObject[i].chartid === id) {
this.TableName = ChartObject[i].table; this.TableName = ChartObject[i].table;
this.XAxis = ChartObject[i].xAxis; this.XAxis = ChartObject[i].xAxis;
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
console.log(this.TableName); // Add connection ID if available
this.Dashtestservive.getChartData(this.TableName,"Grid View",this.XAxis,this.YAxis).subscribe((Ldata) => { this.ConnectionId = ChartObject[i].connection;
console.log(Ldata); console.log('GridRunner: TableName:', this.TableName);
this.rows = Ldata; console.log('GridRunner: XAxis:', this.XAxis);
this.rowdata = this.rows console.log('GridRunner: YAxis:', this.YAxis);
console.log('GridRunner: ConnectionId:', this.ConnectionId);
},(error) => { // Fetch data with filters
console.log(error); this.fetchGridData();
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }, (error) => {
console.log('GridRunner: Error fetching dashboard data:', error);
}); });
} }
// Fetch grid data with filter support
fetchGridData(): void {
console.log('fetching grid data ...')
if (this.TableName) {
console.log('GridRunner: Fetching data for TableName:', this.TableName, 'XAxis:', this.XAxis, 'YAxis:', this.YAxis);
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
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 !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('GridRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "grid", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log('GridRunner: Received data from API:', Ldata);
// Handle the actual data structure returned by the API
if (Ldata && Ldata.chartData) {
this.rows = Ldata.chartData;
this.rowdata = this.rows;
} else if (Ldata && Ldata.data) {
// Handle the original expected format as fallback
this.rows = Ldata.data;
this.rowdata = this.rows;
} else if (Array.isArray(Ldata)) {
// Handle case where data is directly an array
this.rows = Ldata;
this.rowdata = this.rows;
} else {
console.warn('GridRunner: Received data does not have expected structure', Ldata);
this.rows = [];
this.rowdata = [];
}
// Log the structure of the received data
if (this.rows) {
console.log('GridRunner: Rows length:', this.rows.length);
if (this.rows.length > 0) {
console.log('GridRunner: First row structure:', this.rows[0]);
}
} else {
console.log('GridRunner: No data received');
}
}, (error) => {
console.log('GridRunner: Error fetching data:', error);
this.error = error;
});
} else {
console.log('GridRunner: Missing TableName or XAxis');
}
}
//dynamic table //dynamic table
@@ -87,16 +184,16 @@ getTableData(id){
getHeaders() { getHeaders() {
let headers: string[] = []; let headers: string[] = [];
if (this.rows) { if (this.rows) {
console.log('GridRunner: Getting headers from rows:', this.rows);
this.rows.forEach((value) => { this.rows.forEach((value) => {
Object.keys(value).forEach((key) => { Object.keys(value).forEach((key) => {
if (!headers.find((header) => header == key)) { if (!headers.find((header) => header == key)) {
headers.push(key) headers.push(key)
} }
}) })
}) })
} }
console.log('GridRunner: Computed headers:', headers);
return headers; return headers;
} }
@@ -107,5 +204,17 @@ generatePDFFile(){
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
}
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('GridRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('GridRunnerComponent destroyed and cleaned up');
}
}

View File

@@ -7,6 +7,10 @@ import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
import { jsPDF } from 'jspdf'; import { jsPDF } from 'jspdf';
import domtoimage from 'dom-to-image'; import domtoimage from 'dom-to-image';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-line-runner', selector: 'app-line-runner',
templateUrl: './line-runner.component.html', templateUrl: './line-runner.component.html',
@@ -54,8 +58,14 @@ export class LineRunnerComponent implements OnInit {
lineChartLegend = false; lineChartLegend = false;
lineChartPlugins = []; lineChartPlugins = [];
lineChartType = 'line'; lineChartType = 'line';
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router,) { } private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
ngOnInit(): void { ngOnInit(): void {
@@ -65,6 +75,13 @@ export class LineRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -92,16 +109,10 @@ export class LineRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.lineChartLegend = ChartObject[i].chartlegend; this.lineChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Line Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.lineChartData = this.JsonData.chartData;
this.lineChartLabels = this.JsonData.chartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
@@ -128,6 +139,52 @@ export class LineRunnerComponent implements OnInit {
// } // }
} }
// Fetch chart data with filter support
fetchChartData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
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 !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('LineRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Line Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.JsonData = Ldata;
this.lineChartData = this.JsonData.chartData;
this.lineChartLabels = this.JsonData.chartLabels;
},(error) => {
console.log(error);
});
}
}
generatePDFFile(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -166,4 +223,17 @@ export class LineRunnerComponent implements OnInit {
// } // }
// } // }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('LineRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('LineRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
@@ -23,9 +27,15 @@ export class PieRunnerComponent implements OnInit {
showlabel; showlabel;
JsonData; JsonData;
lineChartNoLabels: any[] = []; lineChartNoLabels: any[] = [];
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router,) { } private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
public pieChartLabels: string[] = ['SciFi', 'Drama', 'Comedy']; public pieChartLabels: string[] = ['SciFi', 'Drama', 'Comedy'];
public pieChartData: number[] = [30, 50, 20]; public pieChartData: number[] = [30, 50, 20];
@@ -39,6 +49,13 @@ export class PieRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -66,22 +83,62 @@ export class PieRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.ChartLegend = ChartObject[i].chartlegend; this.ChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Pie Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.pieChartData = this.JsonData.pieChartData;
this.pieChartLabels = this.JsonData.pieChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
}); });
} }
// Fetch chart data with filter support
fetchChartData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
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 !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('PieRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Pie Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.JsonData = Ldata;
this.pieChartData = this.JsonData.pieChartData;
this.pieChartLabels = this.JsonData.pieChartLabels;
},(error) => {
console.log(error);
});
}
}
generatePDFFile(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -89,4 +146,17 @@ export class PieRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('PieRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('PieRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -4,6 +4,10 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
// import { Label } from 'ng2-charts'; // import { Label } from 'ng2-charts';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-polar-runner', selector: 'app-polar-runner',
@@ -23,9 +27,15 @@ export class PolarRunnerComponent implements OnInit {
showlabel; showlabel;
JsonData; JsonData;
lineChartNoLabels: any[] = []; lineChartNoLabels: any[] = [];
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router,) { } private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
public polarAreaChartLabels: string[] = [ 'Download Sales', 'In-Store Sales', 'Mail Sales', 'Telesales', 'Corporate Sales' ]; public polarAreaChartLabels: string[] = [ 'Download Sales', 'In-Store Sales', 'Mail Sales', 'Telesales', 'Corporate Sales' ];
public polarAreaChartData: any = [ public polarAreaChartData: any = [
@@ -41,6 +51,13 @@ export class PolarRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -66,22 +83,62 @@ export class PolarRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.ChartLegend = ChartObject[i].chartlegend; this.ChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"PolarArea Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.polarAreaChartData = this.JsonData.polarAreaChartData;
this.polarAreaChartLabels = this.JsonData.polarAreaChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
}); });
} }
// Fetch chart data with filter support
fetchChartData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
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 !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('PolarRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "PolarArea Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.JsonData = Ldata;
this.polarAreaChartData = this.JsonData.polarAreaChartData;
this.polarAreaChartLabels = this.JsonData.polarAreaChartLabels;
},(error) => {
console.log(error);
});
}
}
generatePDFFile(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -90,4 +147,17 @@ export class PolarRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('PolarRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('PolarRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -4,6 +4,10 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
// import { Label } from 'ng2-charts'; // import { Label } from 'ng2-charts';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-radar-runner', selector: 'app-radar-runner',
@@ -24,9 +28,15 @@ export class RadarRunnerComponent implements OnInit {
JsonData; JsonData;
lineChartNoLabels: any[] = []; lineChartNoLabels: any[] = [];
ChartLegend = false; ChartLegend = false;
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router,) { } private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
public radarChartLabels: string[] = [ public radarChartLabels: string[] = [
"Eating", "Eating",
@@ -50,6 +60,13 @@ export class RadarRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -75,22 +92,62 @@ export class RadarRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.ChartLegend = ChartObject[i].chartlegend; this.ChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Radar Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.radarChartData = this.JsonData.radarChartData;
this.radarChartLabels = this.JsonData.radarChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
}); });
} }
// Fetch chart data with filter support
fetchChartData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
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 !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('RadarRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Radar Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.JsonData = Ldata;
this.radarChartData = this.JsonData.radarChartData;
this.radarChartLabels = this.JsonData.radarChartLabels;
},(error) => {
console.log(error);
});
}
}
generatePDFFile(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -99,4 +156,17 @@ export class RadarRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('RadarRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('RadarRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -5,6 +5,10 @@ import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
// import { Label } from 'ng2-charts'; // import { Label } from 'ng2-charts';
import { ChartDataset } from 'chart.js'; import { ChartDataset } from 'chart.js';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-scatter-runner', selector: 'app-scatter-runner',
@@ -25,9 +29,15 @@ export class ScatterRunnerComponent implements OnInit {
JsonData; JsonData;
lineChartNoLabels: any[] = []; lineChartNoLabels: any[] = [];
ChartLegend = false; ChartLegend = false;
ConnectionId: number; // Add ConnectionId property
// Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service, constructor(private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router,) { } private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
public scatterChartLabels: string[] = [ 'Eating', 'Drinking', 'Sleeping', 'Designing', 'Coding', 'Cycling', 'Running' ]; public scatterChartLabels: string[] = [ 'Eating', 'Drinking', 'Sleeping', 'Designing', 'Coding', 'Cycling', 'Running' ];
@@ -69,6 +79,13 @@ export class ScatterRunnerComponent implements OnInit {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the chart data
this.fetchChartData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
@@ -94,22 +111,62 @@ export class ScatterRunnerComponent implements OnInit {
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.showlabel = ChartObject[i].showlabel; this.showlabel = ChartObject[i].showlabel;
this.ChartLegend = ChartObject[i].chartlegend; this.ChartLegend = ChartObject[i].chartlegend;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Scatter Chart",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchChartData();
this.JsonData = Ldata;
this.scatterChartData = this.JsonData.scatterChartData;
this.scatterChartLabels = this.JsonData.scatterChartLabels;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
}); });
} }
// Fetch chart data with filter support
fetchChartData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Convert YAxis to string if it's an array
const yAxisString = Array.isArray(this.YAxis) ? this.YAxis.join(',') : this.YAxis;
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
const filterDefinitions = this.filterService.getFilters();
// Build filter object using field names as keys
const filterObj = {};
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 !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('ScatterRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Scatter Chart", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.JsonData = Ldata;
this.scatterChartData = this.JsonData.scatterChartData;
this.scatterChartLabels = this.JsonData.scatterChartLabels;
},(error) => {
console.log(error);
});
}
}
generatePDFFile(){ generatePDFFile(){
this.buttonClicked.emit(); this.buttonClicked.emit();
const content = this.contentContainerRef.nativeElement; const content = this.contentContainerRef.nativeElement;
@@ -118,4 +175,17 @@ export class ScatterRunnerComponent implements OnInit {
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('ScatterRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('ScatterRunnerComponent destroyed and cleaned up');
}
} }

View File

@@ -3,6 +3,10 @@ import { DashrunnerService } from '../dashrunner.service';
import { DashboardContentModel } from 'src/app/models/builder/dashboard'; import { DashboardContentModel } from 'src/app/models/builder/dashboard';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
// Add FilterService import
import { FilterService } from '../../../dashboardnew/common-filter/filter.service';
// Add Subscription import
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-todo-runner', selector: 'app-todo-runner',
@@ -12,8 +16,9 @@ import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
export class TodoRunnerComponent implements OnInit { export class TodoRunnerComponent implements OnInit {
@ViewChild('contentContainer') contentContainerRef!: ElementRef; @ViewChild('contentContainer') contentContainerRef!: ElementRef;
@Output() buttonClicked = new EventEmitter<void>(); @Output() buttonClicked = new EventEmitter<void>();
constructor( private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router) { } // Add subscriptions to unsubscribe on destroy
private subscriptions: Subscription[] = [];
loading = false; loading = false;
givendata; givendata;
@@ -25,6 +30,7 @@ export class TodoRunnerComponent implements OnInit {
public DashtestboardArray: DashboardContentModel[] = []; public DashtestboardArray: DashboardContentModel[] = [];
workflowLine; workflowLine;
TableName; TableName;
ConnectionId: number; // Add ConnectionId property
list; list;
data: any; data: any;
@@ -34,11 +40,25 @@ export class TodoRunnerComponent implements OnInit {
listName: "title123", listName: "title123",
List:['todo 1','todo 2'], List:['todo 1','todo 2'],
} }
constructor( private Dashtestservive:DashrunnerService,private route: ActivatedRoute,private dashboardService: Dashboard3Service,
private router : Router,
// Add FilterService to constructor
private filterService: FilterService) { }
ngOnInit(): void { ngOnInit(): void {
this.editId = this.route.snapshot.params.id; this.editId = this.route.snapshot.params.id;
console.log(this.editId); console.log(this.editId);
// this.getbyId(); // this.getbyId();
// Subscribe to filter changes
this.subscriptions.push(
this.filterService.filterState$.subscribe(filters => {
// When filters change, refresh the todo data
this.fetchTodoData();
})
);
this.dashboardService.getById(this.editId).subscribe((data)=>{ this.dashboardService.getById(this.editId).subscribe((data)=>{
console.log(data); console.log(data);
this.workflowLine = data.dashbord1_Line[0].model; this.workflowLine = data.dashbord1_Line[0].model;
@@ -63,15 +83,10 @@ export class TodoRunnerComponent implements OnInit {
this.TableName = ChartObject[i].table; this.TableName = ChartObject[i].table;
this.XAxis = ChartObject[i].xAxis; this.XAxis = ChartObject[i].xAxis;
this.YAxis = ChartObject[i].yAxis; this.YAxis = ChartObject[i].yAxis;
this.ConnectionId = ChartObject[i].connection; // Add connection ID
console.log(this.TableName); console.log(this.TableName);
this.Dashtestservive.getChartData(this.TableName,"Todo List",this.XAxis,this.YAxis).subscribe((Ldata) => { // Fetch data with filters
console.log(Ldata); this.fetchTodoData();
this.todoList.listName = Ldata.listName;
this.todoList.List = Ldata.List;
},(error) => {
console.log(error);
});
break; // No need to continue the loop once the correct placeholder is found break; // No need to continue the loop once the correct placeholder is found
} }
} }
@@ -100,4 +115,58 @@ generatePDFFile(){
this.Dashtestservive.generatePDF(content, filename); this.Dashtestservive.generatePDF(content, filename);
} }
// Fetch todo data with filter support
fetchTodoData(): void {
if (this.TableName && this.XAxis && this.YAxis) {
// Get filter parameters from common filters
const commonFilters = this.filterService.getFilterValues();
// Build filter object using field names as keys
const filterObj = {};
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 !== '') {
filterObj[fieldName] = filterValue;
}
}
});
// Convert to JSON string for API call
let filterParams = '';
if (Object.keys(filterObj).length > 0) {
filterParams = JSON.stringify(filterObj);
}
console.log('TodoRunner: Final filter object to send to API:', filterObj);
// Fetch data from the dashboard service with filters
this.Dashtestservive.getChartDataWithFilters(this.TableName, "Todo List", this.XAxis, this.YAxis, this.ConnectionId, '', '', filterParams).subscribe((Ldata) => {
console.log(Ldata);
this.todoList.listName = Ldata.listName;
this.todoList.List = Ldata.List;
},(error) => {
console.log(error);
});
}
}
ngOnDestroy() {
// Unsubscribe from all subscriptions to prevent memory leaks
console.log('TodoRunnerComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
this.subscriptions.forEach(subscription => {
if (subscription && !subscription.closed) {
subscription.unsubscribe();
}
});
this.subscriptions = [];
console.log('TodoRunnerComponent destroyed and cleaned up');
}
} }