Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b775a8c63 | ||
|
|
cf4fc1be93 | ||
|
|
4c135c4901 | ||
|
|
0b738ca7ca | ||
|
|
1b17bb706d | ||
|
|
f740076d60 | ||
|
|
bedcc0822d | ||
|
|
87810acc9e | ||
|
|
96b90e5dbd | ||
|
|
ced99e0940 | ||
|
|
4f82ae8698 | ||
|
|
e6779e8f34 | ||
|
|
82425d5377 | ||
|
|
2995328ec1 | ||
|
|
afc2c1f8a1 | ||
|
|
418b02acd7 | ||
|
|
cdd752469c |
@@ -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 -->
|
||||||
|
|||||||
@@ -331,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -47,6 +47,12 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
commonFilterModalOpen: boolean = false; // Add common filter modal state
|
commonFilterModalOpen: boolean = false; // Add common filter modal state
|
||||||
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[] = [
|
||||||
{
|
{
|
||||||
@@ -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,11 +846,16 @@ 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 || '';
|
||||||
xyz.filterOptions = this.gadgetsEditdata.filterOptions || [];
|
// 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.table = this.gadgetsEditdata.table || '';
|
xyz.table = this.gadgetsEditdata.table || '';
|
||||||
xyz.connection = this.gadgetsEditdata.connection || undefined;
|
xyz.connection = this.gadgetsEditdata.connection || undefined;
|
||||||
}
|
}
|
||||||
@@ -821,7 +895,7 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
getChartInputs(item: any): any {
|
getChartInputs(item: any): any {
|
||||||
// For CompactFilterComponent, pass only filter configuration properties
|
// For CompactFilterComponent, pass only filter configuration properties
|
||||||
if (item.component && item.component.name === 'CompactFilterComponent') {
|
if (item.name === 'Compact Filter') {
|
||||||
const filterInputs = {
|
const filterInputs = {
|
||||||
filterKey: item['filterKey'] || '',
|
filterKey: item['filterKey'] || '',
|
||||||
filterType: item['filterType'] || 'text',
|
filterType: item['filterType'] || 'text',
|
||||||
@@ -934,8 +1008,8 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
drilldownXAxis: item['drilldownXAxis'],
|
drilldownXAxis: item['drilldownXAxis'],
|
||||||
drilldownYAxis: item['drilldownYAxis'],
|
drilldownYAxis: item['drilldownYAxis'],
|
||||||
drilldownParameter: item['drilldownParameter'], // Add drilldown parameter
|
drilldownParameter: item['drilldownParameter'], // Add drilldown parameter
|
||||||
baseFilters: item['baseFilters'] || [], // Add base filters
|
baseFilters: item['baseFilters'] || [], // Add base filters with type information
|
||||||
drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters
|
drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters with type information
|
||||||
// Multi-layer drilldown configurations
|
// Multi-layer drilldown configurations
|
||||||
drilldownLayers: item['drilldownLayers'] || []
|
drilldownLayers: item['drilldownLayers'] || []
|
||||||
};
|
};
|
||||||
@@ -950,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);
|
||||||
|
|
||||||
@@ -995,11 +1070,16 @@ 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 || '';
|
||||||
updatedItem.filterOptions = this.gadgetsEditdata.filterOptions || [];
|
// 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.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
|
||||||
|
|
||||||
@@ -1244,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);
|
||||||
@@ -1435,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,334 @@
|
|||||||
<div style="display: block; height: 100%; width: 100%;">
|
<div class="chart-container">
|
||||||
<!-- No filter controls needed with the new simplified approach -->
|
<!-- Filter Controls Section -->
|
||||||
<!-- Filters are now configured at the drilldown level -->
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
|
<!-- Base Filters -->
|
||||||
<!-- Drilldown mode indicator -->
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<h4>Base Filters</h4>
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<div class="filter-controls">
|
||||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
Back to Level {{currentDrilldownLevel - 1}}
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
</button>
|
|
||||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<!-- Text Filter -->
|
||||||
Back to Main View
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
</button>
|
<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>
|
||||||
|
|
||||||
<!-- No data message -->
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
<div *ngIf="noDataAvailable" style="text-align: center; padding: 20px; color: #666; font-style: italic;">
|
<div class="chart-header">
|
||||||
No data available
|
<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>
|
||||||
|
|
||||||
<!-- Chart display -->
|
<div class="chart-wrapper">
|
||||||
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
|
<div class="chart-content" [class.loading]="isLoading">
|
||||||
<canvas baseChart
|
|
||||||
[datasets]="barChartData"
|
<div *ngIf="noDataAvailable" class="no-data-message">
|
||||||
[labels]="barChartLabels"
|
No data available
|
||||||
[type]="barChartType"
|
</div>
|
||||||
[options]="barChartOptions"
|
|
||||||
(chartHover)="chartHovered($event)"
|
|
||||||
(chartClick)="chartClicked($event)">
|
<div *ngIf="!noDataAvailable" class="chart-display">
|
||||||
</canvas>
|
<canvas baseChart
|
||||||
|
[datasets]="barChartData"
|
||||||
|
[labels]="barChartLabels"
|
||||||
|
[type]="barChartType"
|
||||||
|
[options]="barChartOptions"
|
||||||
|
(chartHover)="chartHovered($event)"
|
||||||
|
(chartClick)="chartClicked($event)">
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="loading-overlay" *ngIf="isLoading">
|
||||||
|
<div class="shimmer-bar"></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>
|
||||||
@@ -1,31 +1,278 @@
|
|||||||
// Bar Chart Component Styles
|
// Chart container structure
|
||||||
:host {
|
.chart-container {
|
||||||
display: block;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
|
|
||||||
|
// Filter section styling
|
||||||
|
.filter-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
.bar-chart-container {
|
.filter-group {
|
||||||
position: relative;
|
margin-bottom: 15px;
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
h4 {
|
||||||
}
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
canvas {
|
.filter-controls {
|
||||||
display: block;
|
display: flex;
|
||||||
max-width: 100%;
|
flex-wrap: wrap;
|
||||||
max-height: 100%;
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Responsive design for chart container
|
.filter-item {
|
||||||
@media (max-width: 768px) {
|
flex: 1 1 300px;
|
||||||
.bar-chart-container {
|
min-width: 250px;
|
||||||
height: 300px;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart header styling
|
||||||
|
.chart-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart wrapper and content
|
||||||
|
.chart-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.chart-content {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 300px; // Ensure minimum height for chart
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
|
||||||
|
.chart-display {
|
||||||
|
filter: blur(2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-height: 100%;
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
|
||||||
|
.shimmer-bar {
|
||||||
|
width: 80%;
|
||||||
|
height: 20px;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@keyframes shimmer {
|
||||||
.bar-chart-container {
|
0% {
|
||||||
height: 250px;
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chart-container {
|
||||||
|
.filter-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
.header-row {
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-content {
|
||||||
|
min-height: 250px; // Adjust for mobile
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,15 +180,329 @@ export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,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 ===');
|
||||||
@@ -258,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
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -271,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,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) => {
|
||||||
@@ -452,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)
|
||||||
@@ -661,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,282 @@
|
|||||||
<div style="display:block">
|
<div style="display:block; height: 100%; width: 100%;">
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Filter Controls Section -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<!-- Base Filters -->
|
||||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
Back to Level {{currentDrilldownLevel - 1}}
|
<h4>Base Filters</h4>
|
||||||
</button>
|
<div class="filter-controls">
|
||||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
Back to Main View
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
</button>
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || 'Bubble Chart'}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No data message -->
|
<!-- No data message -->
|
||||||
@@ -16,7 +285,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chart display -->
|
<!-- Chart display -->
|
||||||
<div *ngIf="!noDataAvailable">
|
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
|
||||||
<canvas baseChart
|
<canvas baseChart
|
||||||
[datasets]="bubbleChartData"
|
[datasets]="bubbleChartData"
|
||||||
[type]="bubbleChartType"
|
[type]="bubbleChartType"
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
.filter-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
flex: 1 1 300px;
|
||||||
|
min-width: 250px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.filter-text-input,
|
||||||
|
.filter-select,
|
||||||
|
.filter-date {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 34px;
|
||||||
|
|
||||||
|
.multiselect-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-value {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range {
|
||||||
|
.date-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New header row styling
|
||||||
|
.header-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filter-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
import { ChartConfiguration, ChartDataset, ChartOptions } from 'chart.js';
|
import { ChartConfiguration, ChartDataset, ChartOptions } from 'chart.js';
|
||||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||||
|
import { FilterService } from '../../common-filter/filter.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-bubble-chart',
|
selector: 'app-bubble-chart',
|
||||||
@@ -95,16 +97,41 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
// Flag to prevent infinite loops
|
// Flag to prevent infinite loops
|
||||||
private isFetchingData: boolean = false;
|
private isFetchingData: boolean = false;
|
||||||
|
|
||||||
|
// Subscriptions to unsubscribe on destroy
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
constructor(private dashboardService: Dashboard3Service) { }
|
// Add properties for filter functionality
|
||||||
|
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||||
|
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||||
|
private filtersInitialized: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dashboardService: Dashboard3Service,
|
||||||
|
private filterService: FilterService
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Subscribe to filter changes
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.filterService.filterState$.subscribe(filters => {
|
||||||
|
// When filters change, refresh the chart data
|
||||||
|
this.fetchChartData();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log('BubbleChartComponent input changes:', changes);
|
console.log('BubbleChartComponent input changes:', changes);
|
||||||
|
|
||||||
|
// Initialize filter values if they haven't been initialized yet
|
||||||
|
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||||
|
this.initializeFilterValues();
|
||||||
|
this.filtersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any of the key properties have changed
|
// Check if any of the key properties have changed
|
||||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||||
@@ -127,6 +154,349 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
|||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize filter values with proper default values based on type
|
||||||
|
private initializeFilterValues(): void {
|
||||||
|
console.log('Initializing filter values');
|
||||||
|
|
||||||
|
// Initialize base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Filter values initialized:', {
|
||||||
|
baseFilters: this.baseFilters,
|
||||||
|
drilldownFilters: this.drilldownFilters,
|
||||||
|
drilldownLayers: this.drilldownLayers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active filters
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||||
|
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||||
|
this.hasActiveLayerFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active layer filters for current drilldown level
|
||||||
|
hasActiveLayerFilters(): boolean {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
return layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters &&
|
||||||
|
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active layer filters for current drilldown level
|
||||||
|
getActiveLayerFilters(): any[] {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
if (layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters) {
|
||||||
|
return this.drilldownLayers[layerIndex].filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filter options for dropdown/multiselect filters
|
||||||
|
getFilterOptions(filter: any): string[] {
|
||||||
|
if (filter.options) {
|
||||||
|
if (Array.isArray(filter.options)) {
|
||||||
|
return filter.options;
|
||||||
|
} else if (typeof filter.options === 'string') {
|
||||||
|
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an option is selected for multiselect filters
|
||||||
|
isOptionSelected(filter: any, option: string): boolean {
|
||||||
|
if (!filter.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.includes(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.value === option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter changes
|
||||||
|
onBaseFilterChange(filter: any): void {
|
||||||
|
console.log('Base filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter changes
|
||||||
|
onDrilldownFilterChange(filter: any): void {
|
||||||
|
console.log('Drilldown filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter changes
|
||||||
|
onLayerFilterChange(filter: any): void {
|
||||||
|
console.log('Layer filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiselect changes
|
||||||
|
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
// Initialize filter.value as array if it's not already
|
||||||
|
if (!Array.isArray(filter.value)) {
|
||||||
|
filter.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add option to array if not already present
|
||||||
|
if (!filter.value.includes(option)) {
|
||||||
|
filter.value.push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove option from array
|
||||||
|
filter.value = filter.value.filter((item: string) => item !== option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||||
|
filter.value = dateRange;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
onToggleChange(filter: any, checked: boolean): void {
|
||||||
|
filter.value = checked;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselect(filter: any, context: string): void {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
if (this.isMultiselectOpen(filter, context)) {
|
||||||
|
this.openMultiselects.delete(filterId);
|
||||||
|
} else {
|
||||||
|
// Close all other multiselects first
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.openMultiselects.set(filterId, context);
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
this.addDocumentClickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add document click handler to close dropdowns when clicking outside
|
||||||
|
private addDocumentClickHandler(): void {
|
||||||
|
if (!this.documentClickHandler) {
|
||||||
|
this.documentClickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside any multiselect dropdown
|
||||||
|
if (!target.closest('.multiselect-container')) {
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler!);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
private removeDocumentClickHandler(): void {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
this.documentClickHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if multiselect dropdown is open
|
||||||
|
isMultiselectOpen(filter: any, context: string): boolean {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
return this.openMultiselects.has(filterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of selected options for a multiselect filter
|
||||||
|
getSelectedOptionsCount(filter: any): number {
|
||||||
|
if (!filter.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
clearAllFilters(): void {
|
||||||
|
// Clear base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all multiselect dropdowns
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform data to bubble chart format
|
||||||
|
private transformToBubbleData(labels: any[], data: any[]): ChartDataset[] {
|
||||||
|
// For bubble charts, we need to transform the data into bubble format
|
||||||
|
// Bubble charts expect data in the format: {x: number, y: number, r: number}
|
||||||
|
console.log('Transforming data to bubble format:', { labels, data });
|
||||||
|
|
||||||
|
// If we have the expected bubble data format, return it as is
|
||||||
|
if (data && data.length > 0 && data[0].data && data[0].data.length > 0 &&
|
||||||
|
typeof data[0].data[0] === 'object' && data[0].data[0].hasOwnProperty('x') &&
|
||||||
|
data[0].data[0].hasOwnProperty('y') && data[0].data[0].hasOwnProperty('r')) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, create a default bubble dataset
|
||||||
|
const bubbleDatasets: ChartDataset[] = [
|
||||||
|
{
|
||||||
|
data: [
|
||||||
|
{ x: 10, y: 10, r: 10 },
|
||||||
|
{ x: 15, y: 5, r: 15 },
|
||||||
|
{ x: 26, y: 12, r: 23 },
|
||||||
|
{ x: 7, y: 8, r: 8 },
|
||||||
|
],
|
||||||
|
label: 'Dataset 1',
|
||||||
|
backgroundColor: 'rgba(255, 0, 0, 0.6)',
|
||||||
|
borderColor: 'blue',
|
||||||
|
hoverBackgroundColor: 'purple',
|
||||||
|
hoverBorderColor: 'red',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return bubbleDatasets;
|
||||||
|
}
|
||||||
|
|
||||||
fetchChartData(): void {
|
fetchChartData(): void {
|
||||||
// Set flag to prevent recursive calls
|
// Set flag to prevent recursive calls
|
||||||
@@ -160,7 +530,49 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
|||||||
filterParams = JSON.stringify(filterObj);
|
filterParams = JSON.stringify(filterObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Base filter parameters:', filterParams);
|
|
||||||
|
// Add common filters to filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
console.log('Common filters from service:', commonFilters);
|
||||||
|
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with base filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add base filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const baseFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, baseFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse base filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters using the field name as the key, not the filter id
|
||||||
|
Object.keys(commonFilters).forEach(filterId => {
|
||||||
|
const filterValue = commonFilters[filterId];
|
||||||
|
// Find the filter definition to get the field name
|
||||||
|
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||||
|
if (filterDef && filterDef.field) {
|
||||||
|
const fieldName = filterDef.field;
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[fieldName] = filterValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to using filterId as field name if no field is defined
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[filterId] = filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Final filter parameters:', filterParams);
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/bubble?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/bubble?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
@@ -206,6 +618,7 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
|||||||
this.bubbleChartData = [];
|
this.bubbleChartData = [];
|
||||||
// Reset flag after fetching
|
// Reset flag after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
|
// Keep default data in case of error
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -307,6 +720,35 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add common filters to drilldown filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with drilldown filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add drilldown filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const drilldownFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters
|
||||||
|
Object.keys(commonFilters).forEach(key => {
|
||||||
|
const value = commonFilters[key];
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
mergedFilterObj[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/bubble?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/bubble?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
console.log('Drilldown data URL:', url);
|
console.log('Drilldown data URL:', url);
|
||||||
@@ -326,7 +768,6 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
|||||||
// Handle the actual data structure returned by the API
|
// Handle the actual data structure returned by the API
|
||||||
if (data && data.chartLabels && data.chartData) {
|
if (data && data.chartLabels && data.chartData) {
|
||||||
// For bubble charts, we need to transform the data into bubble format
|
// For bubble charts, we need to transform the data into bubble format
|
||||||
// Bubble charts expect data in the format: {x: number, y: number, r: number}
|
|
||||||
this.noDataAvailable = data.chartLabels.length === 0;
|
this.noDataAvailable = data.chartLabels.length === 0;
|
||||||
this.bubbleChartData = this.transformToBubbleData(data.chartLabels, data.chartData);
|
this.bubbleChartData = this.transformToBubbleData(data.chartLabels, data.chartData);
|
||||||
console.log('Updated bubble chart with drilldown data:', this.bubbleChartData);
|
console.log('Updated bubble chart with drilldown data:', this.bubbleChartData);
|
||||||
@@ -345,39 +786,11 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
|||||||
console.error('Error fetching drilldown data:', error);
|
console.error('Error fetching drilldown data:', error);
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.bubbleChartData = [];
|
this.bubbleChartData = [];
|
||||||
|
// Keep current data in case of error
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform chart data to bubble chart format
|
|
||||||
private transformToBubbleData(labels: string[], datasets: any[]): ChartDataset[] {
|
|
||||||
// For bubble charts, we need to transform the data into bubble format
|
|
||||||
// Bubble charts expect data in the format: {x: number, y: number, r: number}
|
|
||||||
|
|
||||||
// This is a simple transformation - in a real implementation, you might want to
|
|
||||||
// create a more sophisticated mapping based on your data structure
|
|
||||||
return datasets.map((dataset, index) => {
|
|
||||||
// Create bubble data points
|
|
||||||
const bubbleData = labels.map((label, i) => {
|
|
||||||
// Use x-axis data as x coordinate, y-axis data as y coordinate, and a fixed radius
|
|
||||||
const xValue = dataset.data[i] || 0;
|
|
||||||
const yValue = i < datasets.length ? (datasets[i].data[index] || 0) : 0;
|
|
||||||
const radius = 10; // Fixed radius for now
|
|
||||||
|
|
||||||
return { x: xValue, y: yValue, r: radius };
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: bubbleData,
|
|
||||||
label: dataset.label || `Dataset ${index + 1}`,
|
|
||||||
backgroundColor: dataset.backgroundColor || `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.6)`,
|
|
||||||
borderColor: dataset.borderColor || 'rgba(0, 0, 0, 1)',
|
|
||||||
hoverBackgroundColor: dataset.hoverBackgroundColor || 'rgba(255, 255, 255, 0.8)',
|
|
||||||
hoverBorderColor: dataset.hoverBorderColor || 'rgba(0, 0, 0, 1)'
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset to original data (go back to base level)
|
// Reset to original data (go back to base level)
|
||||||
resetToOriginalData(): void {
|
resetToOriginalData(): void {
|
||||||
console.log('Resetting to original data');
|
console.log('Resetting to original data');
|
||||||
@@ -438,16 +851,18 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
|||||||
// Get the index of the clicked element
|
// Get the index of the clicked element
|
||||||
const clickedIndex = e.active[0].index;
|
const clickedIndex = e.active[0].index;
|
||||||
|
|
||||||
// Get the label of the clicked element
|
// Get the dataset index
|
||||||
// For bubble charts, we might not have labels in the same way as other charts
|
const datasetIndex = e.active[0].datasetIndex;
|
||||||
const clickedLabel = `Bubble ${clickedIndex}`;
|
|
||||||
|
|
||||||
console.log('Clicked on bubble:', { index: clickedIndex, label: clickedLabel });
|
// Get the data point
|
||||||
|
const dataPoint = this.bubbleChartData[datasetIndex].data[clickedIndex];
|
||||||
|
|
||||||
|
console.log('Clicked on bubble:', { datasetIndex, clickedIndex, dataPoint });
|
||||||
|
|
||||||
// If we're not at the base level, store original data
|
// If we're not at the base level, store original data
|
||||||
if (this.currentDrilldownLevel === 0) {
|
if (this.currentDrilldownLevel === 0) {
|
||||||
// Store original data before entering drilldown mode
|
// Store original data before entering drilldown mode
|
||||||
this.originalBubbleChartData = [...this.bubbleChartData];
|
this.originalBubbleChartData = JSON.parse(JSON.stringify(this.bubbleChartData));
|
||||||
console.log('Stored original data for drilldown');
|
console.log('Stored original data for drilldown');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,9 +904,10 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
|||||||
// Add this click to the drilldown stack
|
// Add this click to the drilldown stack
|
||||||
const stackEntry = {
|
const stackEntry = {
|
||||||
level: nextDrilldownLevel,
|
level: nextDrilldownLevel,
|
||||||
|
datasetIndex: datasetIndex,
|
||||||
clickedIndex: clickedIndex,
|
clickedIndex: clickedIndex,
|
||||||
clickedLabel: clickedLabel,
|
dataPoint: dataPoint,
|
||||||
clickedValue: clickedLabel // Using label as value for now
|
clickedValue: dataPoint // Using data point as value for now
|
||||||
};
|
};
|
||||||
|
|
||||||
this.drilldownStack.push(stackEntry);
|
this.drilldownStack.push(stackEntry);
|
||||||
@@ -515,6 +931,6 @@ export class BubbleChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public chartHovered(e: any): void {
|
public chartHovered(e: any): void {
|
||||||
console.log(e);
|
console.log('Bubble chart hovered:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,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>
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Text Filter -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" class="drilldown-indicator">
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
<span class="drilldown-text">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<input type="text"
|
||||||
<button class="btn btn-secondary btn-sm" (click)="navigateBack()">
|
[(ngModel)]="filter.value"
|
||||||
Back to Level {{currentDrilldownLevel - 1}}
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
</button>
|
[placeholder]="filter.field"
|
||||||
<button class="btn btn-danger btn-sm" (click)="resetToOriginalData()">
|
class="clr-input filter-text-input">
|
||||||
Back to Main View
|
</div>
|
||||||
</button>
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
<div class="chart-header">
|
<div class="chart-header">
|
||||||
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</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>
|
||||||
@@ -1,239 +1,292 @@
|
|||||||
.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;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.doughnut-chart-container:hover {
|
|
||||||
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact-filters-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 5px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 5px;
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 6px;
|
|
||||||
min-height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drilldown-indicator {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
padding: 10px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.drilldown-text {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 6px 12px;
|
|
||||||
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 {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
|
|
||||||
.chart-title {
|
// Filter section styling
|
||||||
font-size: 22px;
|
.filter-section {
|
||||||
font-weight: 600;
|
margin-bottom: 20px;
|
||||||
color: #0a192f;
|
padding: 15px;
|
||||||
margin: 0;
|
border: 1px solid #ddd;
|
||||||
text-align: center;
|
border-radius: 4px;
|
||||||
padding-bottom: 10px;
|
background-color: #f9f9f9;
|
||||||
border-bottom: 2px solid #3498db;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.chart-wrapper {
|
.filter-group {
|
||||||
position: relative;
|
margin-bottom: 15px;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-content {
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
&.loading {
|
|
||||||
opacity: 0.7;
|
|
||||||
|
|
||||||
canvas {
|
h4 {
|
||||||
filter: blur(2px);
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-data-message {
|
.filter-controls {
|
||||||
text-align: center;
|
|
||||||
padding: 30px;
|
|
||||||
color: #666;
|
|
||||||
font-size: 18px;
|
|
||||||
font-style: italic;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
gap: 15px;
|
||||||
justify-content: center;
|
}
|
||||||
height: 100%;
|
|
||||||
|
.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%;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-data-message p {
|
.multiselect-container {
|
||||||
margin: 0;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-overlay {
|
.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;
|
position: absolute;
|
||||||
top: 0;
|
top: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
z-index: 1000;
|
||||||
display: flex;
|
background: white;
|
||||||
align-items: center;
|
border: 1px solid #ccc;
|
||||||
justify-content: center;
|
border-top: none;
|
||||||
background: rgba(255, 255, 255, 0.8);
|
border-radius: 0 0 4px 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
.shimmer-donut {
|
.checkbox-group {
|
||||||
width: 120px;
|
padding: 8px;
|
||||||
height: 120px;
|
|
||||||
border-radius: 50%;
|
.checkbox-item {
|
||||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
display: flex;
|
||||||
background-size: 200% 100%;
|
align-items: center;
|
||||||
animation: shimmer 1.5s infinite;
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.chart-legend {
|
.date-range {
|
||||||
display: flex;
|
.date-input-group {
|
||||||
flex-wrap: wrap;
|
display: flex;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 8px;
|
||||||
margin-top: 20px;
|
}
|
||||||
padding: 20px;
|
|
||||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
.date-separator {
|
||||||
border-radius: 8px;
|
margin: 0 5px;
|
||||||
border: 1px solid #dee2e6;
|
color: #777;
|
||||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-item {
|
.toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 20px;
|
gap: 8px;
|
||||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
|
||||||
border-radius: 25px;
|
.toggle-label {
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
margin: 0;
|
||||||
transition: all 0.3s ease;
|
font-size: 14px;
|
||||||
border: 1px solid #eaeaea;
|
cursor: pointer;
|
||||||
cursor: pointer;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-item:hover {
|
.filter-actions {
|
||||||
transform: translateY(-3px);
|
margin-top: 15px;
|
||||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
padding-top: 15px;
|
||||||
border-color: #3498db;
|
border-top: 1px solid #eee;
|
||||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
|
||||||
}
|
.btn {
|
||||||
|
font-size: 13px;
|
||||||
.legend-color {
|
}
|
||||||
width: 20px;
|
}
|
||||||
height: 20px;
|
|
||||||
border-radius: 50%;
|
// Chart header styling
|
||||||
margin-right: 12px;
|
.chart-header {
|
||||||
display: inline-block;
|
margin-bottom: 20px;
|
||||||
border: 2px solid white;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
.header-row {
|
||||||
}
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
.legend-label {
|
border-bottom: 1px solid #eee;
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
.chart-title {
|
||||||
color: #2c3e50;
|
margin: 0;
|
||||||
margin-right: 15px;
|
font-size: 18px;
|
||||||
white-space: nowrap;
|
font-weight: 600;
|
||||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
color: #0a192f;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.legend-value {
|
}
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
// Chart wrapper and content - simplified to match shield dashboard
|
||||||
color: #3498db;
|
.chart-wrapper {
|
||||||
background: linear-gradient(135deg, #e9ecef 0%, #dde1e5 100%);
|
flex: 1;
|
||||||
padding: 6px 12px;
|
position: relative;
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
.chart-content {
|
||||||
min-width: 40px;
|
position: relative;
|
||||||
text-align: center;
|
height: 100%;
|
||||||
|
min-height: 300px;
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
filter: blur(2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data-message {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: calc(100% - 40px); // Leave space for legend
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
|
||||||
|
.shimmer-donut {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart legend - simplified
|
||||||
|
.chart-legend {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 15px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
min-width: 120px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 8px;
|
||||||
|
display: inline-block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-right: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-value {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #3498db;
|
||||||
|
min-width: 30px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
||||||
}
|
flex-direction: column;
|
||||||
|
}
|
||||||
.chart-header .chart-title {
|
|
||||||
font-size: 18px;
|
.filter-item {
|
||||||
}
|
min-width: 100%;
|
||||||
|
}
|
||||||
.drilldown-indicator {
|
|
||||||
flex-direction: column;
|
.chart-header {
|
||||||
gap: 5px;
|
.header-row {
|
||||||
}
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
.drilldown-text {
|
}
|
||||||
font-size: 14px;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-wrapper {
|
.chart-content {
|
||||||
min-height: 200px;
|
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 {
|
.chart-content {
|
||||||
font-size: 16px;
|
min-height: 250px;
|
||||||
padding: 20px;
|
|
||||||
}
|
canvas {
|
||||||
|
max-height: calc(100% - 60px); // More space for legend on mobile
|
||||||
.compact-filters-container {
|
}
|
||||||
flex-wrap: wrap;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,44 +972,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck
|
|||||||
this.resetToOriginalData();
|
this.resetToOriginalData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Get legend color for a specific index
|
||||||
* Get color for legend item
|
getLegendColor(index: number): string {
|
||||||
* @param index Index of the legend item
|
|
||||||
*/
|
|
||||||
public getLegendColor(index: number): string {
|
|
||||||
return this.chartColors[index % this.chartColors.length];
|
return this.chartColors[index % this.chartColors.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure labels and data arrays have the same length
|
|
||||||
*/
|
|
||||||
private syncLabelAndDataArrays(): void {
|
|
||||||
// Handle empty arrays
|
|
||||||
if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length);
|
|
||||||
|
|
||||||
// Pad the shorter array with default values
|
|
||||||
while (this.doughnutChartLabels.length < maxLength) {
|
|
||||||
this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (this.doughnutChartData.length < maxLength) {
|
|
||||||
this.doughnutChartData.push(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Truncate the longer array if needed
|
|
||||||
if (this.doughnutChartLabels.length > maxLength) {
|
|
||||||
this.doughnutChartLabels = this.doughnutChartLabels.slice(0, maxLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.doughnutChartData.length > maxLength) {
|
|
||||||
this.doughnutChartData = this.doughnutChartData.slice(0, maxLength);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// events
|
// events
|
||||||
public chartClicked(e: any): void {
|
public chartClicked(e: any): void {
|
||||||
@@ -729,6 +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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,285 @@
|
|||||||
<div class="dynamic-chart-container">
|
<div class="dynamic-chart-container">
|
||||||
|
<!-- Filter Controls Section -->
|
||||||
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
|
<!-- Base Filters -->
|
||||||
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
|
<h4>Base Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || 'Dynamic Chart'}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Existing content -->
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Drilldown mode indicator -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
.filter-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
flex: 1 1 300px;
|
||||||
|
min-width: 250px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.filter-text-input,
|
||||||
|
.filter-select,
|
||||||
|
.filter-date {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 34px;
|
||||||
|
|
||||||
|
.multiselect-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-value {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range {
|
||||||
|
.date-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New header row styling
|
||||||
|
.header-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filter-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import { Component, OnInit, ViewChild, Input, OnChanges, SimpleChanges } from '@
|
|||||||
import { ChartConfiguration, ChartData, ChartDataset } from 'chart.js';
|
import { ChartConfiguration, ChartData, ChartDataset } from 'chart.js';
|
||||||
import { BaseChartDirective } from 'ng2-charts';
|
import { BaseChartDirective } from 'ng2-charts';
|
||||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||||
|
import { FilterService } from '../../common-filter/filter.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dynamic-chart',
|
selector: 'app-dynamic-chart',
|
||||||
@@ -37,9 +39,20 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
@ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined;
|
@ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined;
|
||||||
|
|
||||||
constructor(private dashboardService: Dashboard3Service) { }
|
constructor(
|
||||||
|
private dashboardService: Dashboard3Service,
|
||||||
|
private filterService: FilterService
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Subscribe to filter changes
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.filterService.filterState$.subscribe(filters => {
|
||||||
|
// When filters change, refresh the chart data
|
||||||
|
this.fetchChartData();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize with default data
|
// Initialize with default data
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
@@ -47,6 +60,12 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
|||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log('DynamicChartComponent input changes:', changes);
|
console.log('DynamicChartComponent input changes:', changes);
|
||||||
|
|
||||||
|
// Initialize filter values if they haven't been initialized yet
|
||||||
|
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||||
|
this.initializeFilterValues();
|
||||||
|
this.filtersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any of the key properties have changed
|
// Check if any of the key properties have changed
|
||||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||||
@@ -106,6 +125,14 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
// Flag to prevent infinite loops
|
// Flag to prevent infinite loops
|
||||||
private isFetchingData: boolean = false;
|
private isFetchingData: boolean = false;
|
||||||
|
|
||||||
|
// Add properties for filter functionality
|
||||||
|
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||||
|
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||||
|
private filtersInitialized: boolean = false;
|
||||||
|
|
||||||
|
// Subscriptions to unsubscribe on destroy
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
fetchChartData(): void {
|
fetchChartData(): void {
|
||||||
// Set flag to prevent recursive calls
|
// Set flag to prevent recursive calls
|
||||||
@@ -139,7 +166,49 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
|||||||
filterParams = JSON.stringify(filterObj);
|
filterParams = JSON.stringify(filterObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Base filter parameters:', filterParams);
|
|
||||||
|
// Add common filters to filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
console.log('Common filters from service:', commonFilters);
|
||||||
|
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with base filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add base filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const baseFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, baseFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse base filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters using the field name as the key, not the filter id
|
||||||
|
Object.keys(commonFilters).forEach(filterId => {
|
||||||
|
const filterValue = commonFilters[filterId];
|
||||||
|
// Find the filter definition to get the field name
|
||||||
|
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||||
|
if (filterDef && filterDef.field) {
|
||||||
|
const fieldName = filterDef.field;
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[fieldName] = filterValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to using filterId as field name if no field is defined
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[filterId] = filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Final filter parameters:', filterParams);
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/dynamic?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/dynamic?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
@@ -504,4 +573,322 @@ export class DynamicChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
this.dynamicChartData = _dynamicChartData;
|
this.dynamicChartData = _dynamicChartData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||||
|
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||||
|
|
||||||
|
// Remove document click handler if it exists
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize filter values with proper default values based on type
|
||||||
|
private initializeFilterValues(): void {
|
||||||
|
console.log('Initializing filter values');
|
||||||
|
|
||||||
|
// Initialize base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Filter values initialized:', {
|
||||||
|
baseFilters: this.baseFilters,
|
||||||
|
drilldownFilters: this.drilldownFilters,
|
||||||
|
drilldownLayers: this.drilldownLayers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active filters
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||||
|
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||||
|
this.hasActiveLayerFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active layer filters for current drilldown level
|
||||||
|
hasActiveLayerFilters(): boolean {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
return layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters &&
|
||||||
|
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active layer filters for current drilldown level
|
||||||
|
getActiveLayerFilters(): any[] {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
if (layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters) {
|
||||||
|
return this.drilldownLayers[layerIndex].filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filter options for dropdown/multiselect filters
|
||||||
|
getFilterOptions(filter: any): string[] {
|
||||||
|
if (filter.options) {
|
||||||
|
if (Array.isArray(filter.options)) {
|
||||||
|
return filter.options;
|
||||||
|
} else if (typeof filter.options === 'string') {
|
||||||
|
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an option is selected for multiselect filters
|
||||||
|
isOptionSelected(filter: any, option: string): boolean {
|
||||||
|
if (!filter.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.includes(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.value === option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter changes
|
||||||
|
onBaseFilterChange(filter: any): void {
|
||||||
|
console.log('Base filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter changes
|
||||||
|
onDrilldownFilterChange(filter: any): void {
|
||||||
|
console.log('Drilldown filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter changes
|
||||||
|
onLayerFilterChange(filter: any): void {
|
||||||
|
console.log('Layer filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiselect changes
|
||||||
|
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
// Initialize filter.value as array if it's not already
|
||||||
|
if (!Array.isArray(filter.value)) {
|
||||||
|
filter.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add option to array if not already present
|
||||||
|
if (!filter.value.includes(option)) {
|
||||||
|
filter.value.push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove option from array
|
||||||
|
filter.value = filter.value.filter((item: string) => item !== option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||||
|
filter.value = dateRange;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
onToggleChange(filter: any, checked: boolean): void {
|
||||||
|
filter.value = checked;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselect(filter: any, context: string): void {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
if (this.isMultiselectOpen(filter, context)) {
|
||||||
|
this.openMultiselects.delete(filterId);
|
||||||
|
} else {
|
||||||
|
// Close all other multiselects first
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.openMultiselects.set(filterId, context);
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
this.addDocumentClickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add document click handler to close dropdowns when clicking outside
|
||||||
|
private addDocumentClickHandler(): void {
|
||||||
|
if (!this.documentClickHandler) {
|
||||||
|
this.documentClickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside any multiselect dropdown
|
||||||
|
if (!target.closest('.multiselect-container')) {
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler!);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
private removeDocumentClickHandler(): void {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
this.documentClickHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if multiselect dropdown is open
|
||||||
|
isMultiselectOpen(filter: any, context: string): boolean {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
return this.openMultiselects.has(filterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of selected options for a multiselect filter
|
||||||
|
getSelectedOptionsCount(filter: any): number {
|
||||||
|
if (!filter.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
clearAllFilters(): void {
|
||||||
|
// Clear base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all multiselect dropdowns
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,285 @@
|
|||||||
<div class="financial-chart-container">
|
<div class="financial-chart-container">
|
||||||
|
<!-- Filter Controls Section -->
|
||||||
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
|
<!-- Base Filters -->
|
||||||
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
|
<h4>Base Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || 'Financial Chart'}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Existing content -->
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Drilldown mode indicator -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
||||||
|
|||||||
@@ -1,108 +1,192 @@
|
|||||||
.financial-chart-container {
|
.filter-section {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 400px;
|
|
||||||
min-height: 400px;
|
|
||||||
padding: 20px;
|
|
||||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
border: 1px solid #eaeaea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.financial-chart-container:hover {
|
|
||||||
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-title {
|
|
||||||
font-size: 26px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #2c3e50;
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
text-align: center;
|
padding: 15px;
|
||||||
padding-bottom: 15px;
|
border: 1px solid #ddd;
|
||||||
border-bottom: 2px solid #3498db;
|
border-radius: 4px;
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-wrapper {
|
.filter-group {
|
||||||
position: relative;
|
margin-bottom: 15px;
|
||||||
flex: 1;
|
|
||||||
min-height: 250px;
|
h4 {
|
||||||
margin: 15px 0;
|
margin-top: 0;
|
||||||
background: #f8f9fa;
|
margin-bottom: 10px;
|
||||||
border: 1px solid #e9ecef;
|
color: #333;
|
||||||
border-radius: 8px;
|
font-weight: 600;
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-wrapper canvas {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-wrapper canvas:hover {
|
|
||||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15));
|
|
||||||
transform: scale(1.02);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-indicator, .no-data-message {
|
|
||||||
text-align: center;
|
|
||||||
padding: 30px;
|
|
||||||
color: #666;
|
|
||||||
font-size: 18px;
|
|
||||||
font-style: italic;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-indicator p, .no-data-message p {
|
|
||||||
margin: 10px 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
border: 4px solid #f3f3f3;
|
|
||||||
border-top: 4px solid #3498db;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.financial-chart-container {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
flex: 1 1 300px;
|
||||||
|
min-width: 250px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.filter-text-input,
|
||||||
|
.filter-select,
|
||||||
|
.filter-date {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 34px;
|
||||||
|
|
||||||
|
.multiselect-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-value {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range {
|
||||||
|
.date-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New header row styling
|
||||||
|
.header-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
.chart-title {
|
.chart-title {
|
||||||
font-size: 20px;
|
margin: 0;
|
||||||
margin-bottom: 15px;
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filter-controls {
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-wrapper {
|
.filter-item {
|
||||||
min-height: 200px;
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-data-message {
|
.header-row {
|
||||||
font-size: 16px;
|
.chart-title {
|
||||||
padding: 20px;
|
font-size: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,254 @@
|
|||||||
<div style="display: block;">
|
<div style="display: block;">
|
||||||
<div class="dg-wrapper">
|
<div class="dg-wrapper">
|
||||||
<div class="clr-row">
|
<!-- Filter Controls Section -->
|
||||||
<div class="clr-col-8">
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
<h3>{{charttitle || 'Data Grid'}}</h3>
|
<!-- 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>
|
</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-col-8">
|
||||||
|
<h3>{{charttitle || 'Data Grid'}}</h3>
|
||||||
|
</div> -->
|
||||||
<!-- Add drilldown navigation controls -->
|
<!-- Add drilldown navigation controls -->
|
||||||
<div class="clr-col-4" *ngIf="drilldownEnabled && drilldownStack.length > 0" style="text-align: right;">
|
<div class="clr-col-4" *ngIf="drilldownEnabled && drilldownStack.length > 0" style="text-align: right;">
|
||||||
<button class="btn btn-sm btn-link" (click)="navigateBack()">
|
<button class="btn btn-sm btn-link" (click)="navigateBack()">
|
||||||
|
|||||||
@@ -1,28 +1,180 @@
|
|||||||
// Add styles for drilldown navigation
|
.filter-section {
|
||||||
.alert-info {
|
margin-bottom: 20px;
|
||||||
background-color: #dcedf7;
|
padding: 15px;
|
||||||
border-color: #a3d4f5;
|
border: 1px solid #ddd;
|
||||||
color: #21333b;
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-info .alert-icon {
|
.filter-group {
|
||||||
color: #0072a3;
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-link {
|
.filter-controls {
|
||||||
color: #0072a3;
|
display: flex;
|
||||||
text-decoration: none;
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-link:hover {
|
.filter-item {
|
||||||
color: #00567a;
|
flex: 1 1 300px;
|
||||||
text-decoration: underline;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dg-wrapper {
|
.dg-wrapper {
|
||||||
padding: 12px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clr-row {
|
clr-datagrid {
|
||||||
margin-bottom: 12px;
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filter-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +66,15 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
// Add subscriptions to unsubscribe on destroy
|
// Add subscriptions to unsubscribe on destroy
|
||||||
private subscriptions: Subscription[] = [];
|
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(
|
constructor(
|
||||||
private mainservice: UsergrpmaintainceService,
|
private mainservice: UsergrpmaintainceService,
|
||||||
private dashboardService: Dashboard3Service,
|
private dashboardService: Dashboard3Service,
|
||||||
@@ -102,6 +111,12 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
const drilldownYAxisChanged = changes.drilldownYAxis && !changes.drilldownYAxis.firstChange;
|
const drilldownYAxisChanged = changes.drilldownYAxis && !changes.drilldownYAxis.firstChange;
|
||||||
const drilldownLayersChanged = changes.drilldownLayers && !changes.drilldownLayers.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 (!this.isFetchingData && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged ||
|
if (!this.isFetchingData && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged ||
|
||||||
drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
|
drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
|
||||||
@@ -112,6 +127,84 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
@@ -628,6 +721,238 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
.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() {
|
ngOnDestroy() {
|
||||||
// Unsubscribe from all subscriptions to prevent memory leaks
|
// Unsubscribe from all subscriptions to prevent memory leaks
|
||||||
console.log('GridViewComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
console.log('GridViewComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
|
||||||
@@ -644,6 +969,12 @@ export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.drilldownStack = [];
|
this.drilldownStack = [];
|
||||||
this.originalGridData = [];
|
this.originalGridData = [];
|
||||||
|
|
||||||
|
// Clear multiselect tracking
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
|
||||||
console.log('GridViewComponent destroyed and cleaned up');
|
console.log('GridViewComponent destroyed and cleaned up');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,282 @@
|
|||||||
<div style="display: block">
|
<div style="display: block; height: 100%; width: 100%;">
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Filter Controls Section -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<!-- Base Filters -->
|
||||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
Back to Level {{currentDrilldownLevel - 1}}
|
<h4>Base Filters</h4>
|
||||||
</button>
|
<div class="filter-controls">
|
||||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
Back to Main View
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
</button>
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || '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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No data message -->
|
<!-- No data message -->
|
||||||
@@ -16,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)"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,16 +1,284 @@
|
|||||||
<div class="pie-chart-container">
|
<div class="pie-chart-container">
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Filter Controls Section -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<!-- Base Filters -->
|
||||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
Back to Level {{currentDrilldownLevel - 1}}
|
<h4>Base Filters</h4>
|
||||||
</button>
|
<div class="filter-controls">
|
||||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
Back to Main View
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
</button>
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="chart-title" *ngIf="charttitle">{{ charttitle }}</h3>
|
|
||||||
<div class="chart-wrapper">
|
<div class="chart-wrapper">
|
||||||
<!-- Show loading indicator -->
|
<!-- Show loading indicator -->
|
||||||
<div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable">
|
<div class="loading-indicator" *ngIf="pieChartLabels.length === 0 && pieChartData.length === 0 && !noDataAvailable">
|
||||||
|
|||||||
@@ -149,10 +149,192 @@
|
|||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive design */
|
// Filter section styles
|
||||||
|
.filter-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
flex: 1 1 300px;
|
||||||
|
min-width: 250px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.filter-text-input,
|
||||||
|
.filter-select,
|
||||||
|
.filter-date {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 34px;
|
||||||
|
|
||||||
|
.multiselect-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-value {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range {
|
||||||
|
.date-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New header row styling
|
||||||
|
.header-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
text-align: left;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.pie-chart-container {
|
.pie-chart-container {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
height: auto;
|
||||||
|
min-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-title {
|
.chart-title {
|
||||||
@@ -179,4 +361,18 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ import { Subscription } from 'rxjs';
|
|||||||
templateUrl: './pie-chart.component.html',
|
templateUrl: './pie-chart.component.html',
|
||||||
styleUrls: ['./pie-chart.component.scss']
|
styleUrls: ['./pie-chart.component.scss']
|
||||||
})
|
})
|
||||||
export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
|
||||||
@Input() xAxis: string;
|
@Input() xAxis: string;
|
||||||
@Input() yAxis: string | string[];
|
@Input() yAxis: string | string[];
|
||||||
@Input() table: string;
|
@Input() table: string;
|
||||||
@@ -101,6 +101,11 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
// Subscriptions to unsubscribe on destroy
|
// Subscriptions to unsubscribe on destroy
|
||||||
private subscriptions: Subscription[] = [];
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
// Add properties for filter functionality
|
||||||
|
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||||
|
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||||
|
private filtersInitialized: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private dashboardService: Dashboard3Service,
|
private dashboardService: Dashboard3Service,
|
||||||
private filterService: FilterService
|
private filterService: FilterService
|
||||||
@@ -133,6 +138,12 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log('PieChartComponent input changes:', changes);
|
console.log('PieChartComponent input changes:', changes);
|
||||||
|
|
||||||
|
// Initialize filter values if they haven't been initialized yet
|
||||||
|
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||||
|
this.initializeFilterValues();
|
||||||
|
this.filtersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any of the key properties have changed
|
// Check if any of the key properties have changed
|
||||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||||
@@ -158,6 +169,318 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||||
|
// Clean up document click handler
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize filter values with proper default values based on type
|
||||||
|
private initializeFilterValues(): void {
|
||||||
|
console.log('Initializing filter values');
|
||||||
|
|
||||||
|
// Initialize base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Filter values initialized:', {
|
||||||
|
baseFilters: this.baseFilters,
|
||||||
|
drilldownFilters: this.drilldownFilters,
|
||||||
|
drilldownLayers: this.drilldownLayers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active filters
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||||
|
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||||
|
this.hasActiveLayerFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active layer filters for current drilldown level
|
||||||
|
hasActiveLayerFilters(): boolean {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
return layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters &&
|
||||||
|
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active layer filters for current drilldown level
|
||||||
|
getActiveLayerFilters(): any[] {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
if (layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters) {
|
||||||
|
return this.drilldownLayers[layerIndex].filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filter options for dropdown/multiselect filters
|
||||||
|
getFilterOptions(filter: any): string[] {
|
||||||
|
if (filter.options) {
|
||||||
|
if (Array.isArray(filter.options)) {
|
||||||
|
return filter.options;
|
||||||
|
} else if (typeof filter.options === 'string') {
|
||||||
|
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an option is selected for multiselect filters
|
||||||
|
isOptionSelected(filter: any, option: string): boolean {
|
||||||
|
if (!filter.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.includes(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.value === option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter changes
|
||||||
|
onBaseFilterChange(filter: any): void {
|
||||||
|
console.log('Base filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter changes
|
||||||
|
onDrilldownFilterChange(filter: any): void {
|
||||||
|
console.log('Drilldown filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter changes
|
||||||
|
onLayerFilterChange(filter: any): void {
|
||||||
|
console.log('Layer filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiselect changes
|
||||||
|
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
// Initialize filter.value as array if it's not already
|
||||||
|
if (!Array.isArray(filter.value)) {
|
||||||
|
filter.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add option to array if not already present
|
||||||
|
if (!filter.value.includes(option)) {
|
||||||
|
filter.value.push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove option from array
|
||||||
|
filter.value = filter.value.filter((item: string) => item !== option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||||
|
filter.value = dateRange;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
onToggleChange(filter: any, checked: boolean): void {
|
||||||
|
filter.value = checked;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselect(filter: any, context: string): void {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
if (this.isMultiselectOpen(filter, context)) {
|
||||||
|
this.openMultiselects.delete(filterId);
|
||||||
|
} else {
|
||||||
|
// Close all other multiselects first
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.openMultiselects.set(filterId, context);
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
this.addDocumentClickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add document click handler to close dropdowns when clicking outside
|
||||||
|
private addDocumentClickHandler(): void {
|
||||||
|
if (!this.documentClickHandler) {
|
||||||
|
this.documentClickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside any multiselect dropdown
|
||||||
|
if (!target.closest('.multiselect-container')) {
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler!);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
private removeDocumentClickHandler(): void {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
this.documentClickHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if multiselect dropdown is open
|
||||||
|
isMultiselectOpen(filter: any, context: string): boolean {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
return this.openMultiselects.has(filterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of selected options for a multiselect filter
|
||||||
|
getSelectedOptionsCount(filter: any): number {
|
||||||
|
if (!filter.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
clearAllFilters(): void {
|
||||||
|
// Clear base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all multiselect dropdowns
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public method to refresh data when filters change
|
// Public method to refresh data when filters change
|
||||||
@@ -243,7 +566,7 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
console.log('Pie chart data URL:', url);
|
console.log('Chart data URL:', url);
|
||||||
|
|
||||||
// Fetch data from the dashboard service with parameter field and value
|
// Fetch data from the dashboard service with parameter field and value
|
||||||
// For base level, we pass empty parameter and value, but now also pass filters
|
// For base level, we pass empty parameter and value, but now also pass filters
|
||||||
@@ -251,12 +574,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
(data: any) => {
|
(data: any) => {
|
||||||
console.log('Received pie chart data:', data);
|
console.log('Received pie chart data:', data);
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
console.warn('Pie chart API returned null data. Check if the API endpoint is working correctly.');
|
console.warn('API returned null data. Check if the API endpoint is working correctly.');
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.pieChartLabels = [];
|
this.pieChartLabels = [];
|
||||||
this.pieChartData = [];
|
this.pieChartData = [];
|
||||||
// Validate and sanitize data to show default data
|
|
||||||
this.validateChartData();
|
|
||||||
// Reset flag after fetching
|
// Reset flag after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
return;
|
return;
|
||||||
@@ -264,50 +585,26 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
|
|
||||||
// Handle the actual data structure returned by the API
|
// Handle the actual data structure returned by the API
|
||||||
if (data && data.chartLabels && data.chartData) {
|
if (data && data.chartLabels && data.chartData) {
|
||||||
// For pie charts, we need to extract the data differently
|
// Backend has already filtered the data, just display it
|
||||||
// The first dataset's data array contains the values for the pie chart
|
|
||||||
this.noDataAvailable = data.chartLabels.length === 0;
|
this.noDataAvailable = data.chartLabels.length === 0;
|
||||||
this.pieChartLabels = data.chartLabels || [];
|
this.pieChartLabels = data.chartLabels;
|
||||||
if (data.chartData && data.chartData.length > 0) {
|
this.pieChartData = data.chartData;
|
||||||
this.pieChartData = data.chartData[0].data.map(value => {
|
|
||||||
// Convert to number if it's not already
|
|
||||||
const numValue = Number(value);
|
|
||||||
return isNaN(numValue) ? 0 : numValue;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.pieChartData = [];
|
|
||||||
}
|
|
||||||
// Ensure labels and data arrays have the same length
|
|
||||||
this.syncLabelAndDataArrays();
|
|
||||||
// Validate and sanitize data
|
|
||||||
this.validateChartData();
|
|
||||||
// Trigger change detection
|
// Trigger change detection
|
||||||
this.pieChartData = [...this.pieChartData];
|
this.pieChartData = [...this.pieChartData];
|
||||||
console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||||
} else if (data && data.labels && data.data) {
|
} else if (data && data.labels && data.datasets) {
|
||||||
// Handle the original expected format as fallback
|
// Backend has already filtered the data, just display it
|
||||||
this.noDataAvailable = data.labels.length === 0;
|
this.noDataAvailable = data.labels.length === 0;
|
||||||
this.pieChartLabels = data.labels || [];
|
this.pieChartLabels = data.labels;
|
||||||
this.pieChartData = data.data.map(value => {
|
this.pieChartData = data.datasets[0]?.data || [];
|
||||||
// Convert to number if it's not already
|
|
||||||
const numValue = Number(value);
|
|
||||||
return isNaN(numValue) ? 0 : numValue;
|
|
||||||
});
|
|
||||||
// Ensure labels and data arrays have the same length
|
|
||||||
this.syncLabelAndDataArrays();
|
|
||||||
// Validate and sanitize data
|
|
||||||
this.validateChartData();
|
|
||||||
// Trigger change detection
|
// Trigger change detection
|
||||||
this.pieChartData = [...this.pieChartData];
|
this.pieChartData = [...this.pieChartData];
|
||||||
console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
|
console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||||
} else {
|
} else {
|
||||||
console.warn('Pie chart received data does not have expected structure', data);
|
console.warn('Received data does not have expected structure', data);
|
||||||
// Reset to default data
|
|
||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.pieChartLabels = [];
|
this.pieChartLabels = [];
|
||||||
this.pieChartData = [];
|
this.pieChartData = [];
|
||||||
// Validate and sanitize data to show default data
|
|
||||||
this.validateChartData();
|
|
||||||
}
|
}
|
||||||
// Reset flag after fetching
|
// Reset flag after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
@@ -317,21 +614,16 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.pieChartLabels = [];
|
this.pieChartLabels = [];
|
||||||
this.pieChartData = [];
|
this.pieChartData = [];
|
||||||
// Validate and sanitize data to show default data
|
|
||||||
this.validateChartData();
|
|
||||||
// Reset flag after fetching
|
// Reset flag after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
|
// Keep default data in case of error
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log('Missing required data for pie chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
|
||||||
// Don't set noDataAvailable to true when there's no required data
|
this.noDataAvailable = true;
|
||||||
// This allows static data to be displayed
|
this.pieChartLabels = [];
|
||||||
this.noDataAvailable = false;
|
this.pieChartData = [];
|
||||||
// Validate the chart data to ensure we have some data to display
|
|
||||||
this.validateChartData();
|
|
||||||
// Force a redraw to ensure the chart displays
|
|
||||||
this.pieChartData = [...this.pieChartData];
|
|
||||||
// Reset flag after fetching
|
// Reset flag after fetching
|
||||||
this.isFetchingData = false;
|
this.isFetchingData = false;
|
||||||
}
|
}
|
||||||
@@ -477,39 +769,18 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
|
|
||||||
// Handle the actual data structure returned by the API
|
// Handle the actual data structure returned by the API
|
||||||
if (data && data.chartLabels && data.chartData) {
|
if (data && data.chartLabels && data.chartData) {
|
||||||
// For pie charts, we need to extract the data differently
|
// Backend has already filtered the data, just display it
|
||||||
// The first dataset's data array contains the values for the pie chart
|
|
||||||
this.noDataAvailable = data.chartLabels.length === 0;
|
this.noDataAvailable = data.chartLabels.length === 0;
|
||||||
this.pieChartLabels = data.chartLabels || [];
|
this.pieChartLabels = data.chartLabels;
|
||||||
if (data.chartData && data.chartData.length > 0) {
|
this.pieChartData = data.chartData;
|
||||||
this.pieChartData = data.chartData[0].data.map(value => {
|
|
||||||
// Convert to number if it's not already
|
|
||||||
const numValue = Number(value);
|
|
||||||
return isNaN(numValue) ? 0 : numValue;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.pieChartData = [];
|
|
||||||
}
|
|
||||||
// Ensure labels and data arrays have the same length
|
|
||||||
this.syncLabelAndDataArrays();
|
|
||||||
// Validate and sanitize data
|
|
||||||
this.validateChartData();
|
|
||||||
// Trigger change detection
|
// Trigger change detection
|
||||||
this.pieChartData = [...this.pieChartData];
|
this.pieChartData = [...this.pieChartData];
|
||||||
console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||||
} else if (data && data.labels && data.data) {
|
} else if (data && data.labels && data.datasets) {
|
||||||
// Handle the original expected format as fallback
|
// Backend has already filtered the data, just display it
|
||||||
this.noDataAvailable = data.labels.length === 0;
|
this.noDataAvailable = data.labels.length === 0;
|
||||||
this.pieChartLabels = data.labels || [];
|
this.pieChartLabels = data.labels;
|
||||||
this.pieChartData = data.data.map(value => {
|
this.pieChartData = data.datasets[0]?.data || [];
|
||||||
// Convert to number if it's not already
|
|
||||||
const numValue = Number(value);
|
|
||||||
return isNaN(numValue) ? 0 : numValue;
|
|
||||||
});
|
|
||||||
// Ensure labels and data arrays have the same length
|
|
||||||
this.syncLabelAndDataArrays();
|
|
||||||
// Validate and sanitize data
|
|
||||||
this.validateChartData();
|
|
||||||
// Trigger change detection
|
// Trigger change detection
|
||||||
this.pieChartData = [...this.pieChartData];
|
this.pieChartData = [...this.pieChartData];
|
||||||
console.log('Updated pie chart with drilldown legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
|
console.log('Updated pie chart with drilldown legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
|
||||||
@@ -518,8 +789,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.pieChartLabels = [];
|
this.pieChartLabels = [];
|
||||||
this.pieChartData = [];
|
this.pieChartData = [];
|
||||||
// Validate and sanitize data
|
|
||||||
this.validateChartData();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@@ -588,84 +857,34 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Validate chart data to ensure labels and data arrays have the same length
|
||||||
* Get color for legend item
|
private validateChartData(): void {
|
||||||
* @param index Index of the legend item
|
if (this.pieChartLabels && this.pieChartData) {
|
||||||
*/
|
// For pie charts, we need to ensure labels and data arrays have the same length
|
||||||
public getLegendColor(index: number): string {
|
const labelCount = this.pieChartLabels.length;
|
||||||
|
const dataCount = this.pieChartData.length;
|
||||||
|
|
||||||
|
if (labelCount !== dataCount) {
|
||||||
|
console.warn('Pie chart labels and data arrays have different lengths:', { labels: labelCount, data: dataCount });
|
||||||
|
// Pad or truncate data array to match label count
|
||||||
|
if (dataCount < labelCount) {
|
||||||
|
// Pad with zeros
|
||||||
|
while (this.pieChartData.length < labelCount) {
|
||||||
|
this.pieChartData.push(0);
|
||||||
|
}
|
||||||
|
} else if (dataCount > labelCount) {
|
||||||
|
// Truncate data array
|
||||||
|
this.pieChartData = this.pieChartData.slice(0, labelCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get legend color for a specific index
|
||||||
|
getLegendColor(index: number): string {
|
||||||
return this.chartColors[index % this.chartColors.length];
|
return this.chartColors[index % this.chartColors.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure labels and data arrays have the same length
|
|
||||||
*/
|
|
||||||
private syncLabelAndDataArrays(): void {
|
|
||||||
// Ensure we have matching arrays
|
|
||||||
if (this.pieChartLabels.length !== this.pieChartData.length) {
|
|
||||||
const maxLength = Math.max(this.pieChartLabels.length, this.pieChartData.length);
|
|
||||||
while (this.pieChartLabels.length < maxLength) {
|
|
||||||
this.pieChartLabels.push(`Label ${this.pieChartLabels.length + 1}`);
|
|
||||||
}
|
|
||||||
while (this.pieChartData.length < maxLength) {
|
|
||||||
this.pieChartData.push(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate and sanitize chart data
|
|
||||||
*/
|
|
||||||
private validateChartData(): void {
|
|
||||||
console.log('Validating chart data:', { labels: this.pieChartLabels, data: this.pieChartData });
|
|
||||||
|
|
||||||
// Ensure we have valid arrays
|
|
||||||
if (!Array.isArray(this.pieChartLabels)) {
|
|
||||||
this.pieChartLabels = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(this.pieChartData)) {
|
|
||||||
this.pieChartData = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we have some data to display
|
|
||||||
if (this.pieChartLabels.length === 0 && this.pieChartData.length === 0) {
|
|
||||||
// Add default data to ensure chart visibility
|
|
||||||
this.pieChartLabels = ['Category A', 'Category B', 'Category C'];
|
|
||||||
this.pieChartData = [30, 50, 20];
|
|
||||||
console.log('Added default data for chart display');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure labels and data arrays have the same length
|
|
||||||
this.syncLabelAndDataArrays();
|
|
||||||
|
|
||||||
// Ensure all data values are numbers
|
|
||||||
this.pieChartData = this.pieChartData.map(value => {
|
|
||||||
const numValue = Number(value);
|
|
||||||
return isNaN(numValue) ? 0 : numValue;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('After validation:', { labels: this.pieChartLabels, data: this.pieChartData });
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewChecked() {
|
|
||||||
// Debugging: Log component state after view checks
|
|
||||||
console.log('PieChartComponent state:', {
|
|
||||||
labels: this.pieChartLabels,
|
|
||||||
data: this.pieChartData,
|
|
||||||
hasData: this.pieChartLabels.length > 0 && this.pieChartData.length > 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if chart data is valid and ready to display
|
|
||||||
*/
|
|
||||||
public isChartDataValid(): boolean {
|
|
||||||
return this.pieChartLabels && this.pieChartData &&
|
|
||||||
Array.isArray(this.pieChartLabels) && Array.isArray(this.pieChartData) &&
|
|
||||||
this.pieChartLabels.length > 0 && this.pieChartData.length > 0 &&
|
|
||||||
this.pieChartLabels.length === this.pieChartData.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// events
|
// events
|
||||||
public chartClicked(e: any): void {
|
public chartClicked(e: any): void {
|
||||||
console.log('Pie chart clicked:', e);
|
console.log('Pie chart clicked:', e);
|
||||||
@@ -752,6 +971,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public chartHovered(e: any): void {
|
public chartHovered(e: any): void {
|
||||||
console.log(e);
|
console.log('Pie chart hovered:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewChecked(): void {
|
||||||
|
// This lifecycle hook can be used if needed for post-render operations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,291 @@
|
|||||||
|
<div style="display: block; height: 100%; width: 100%;">
|
||||||
<div style="display: block">
|
<!-- Filter Controls Section -->
|
||||||
<canvas baseChart
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
[datasets]="polarAreaChartData"
|
<!-- Base Filters -->
|
||||||
[labels]="polarAreaChartLabels"
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
[type]="polarAreaChartType"
|
<h4>Base Filters</h4>
|
||||||
(chartHover)="chartHovered($event)"
|
<div class="filter-controls">
|
||||||
(chartClick)="chartClicked($event)">
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
</canvas>
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
</div>
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || 'Polar Chart'}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="position: relative; height: calc(100% - 50px);">
|
||||||
|
<canvas baseChart
|
||||||
|
[datasets]="polarAreaChartData"
|
||||||
|
[labels]="polarAreaChartLabels"
|
||||||
|
[type]="polarAreaChartType"
|
||||||
|
(chartHover)="chartHovered($event)"
|
||||||
|
(chartClick)="chartClicked($event)">
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,18 +1,192 @@
|
|||||||
// Polar Chart Component Styles
|
.filter-section {
|
||||||
div[style*="display: block"] {
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
flex: 1 1 300px;
|
||||||
|
min-width: 250px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.filter-text-input,
|
||||||
|
.filter-select,
|
||||||
|
.filter-date {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
.multiselect-display {
|
||||||
max-width: 100%;
|
display: flex;
|
||||||
max-height: 100%;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 34px;
|
||||||
|
|
||||||
|
.multiselect-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-value {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the chart container has proper sizing
|
.multiselect-dropdown {
|
||||||
:host {
|
position: absolute;
|
||||||
display: block;
|
top: 100%;
|
||||||
width: 100%;
|
left: 0;
|
||||||
height: 100%;
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range {
|
||||||
|
.date-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New header row styling
|
||||||
|
.header-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filter-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||||
|
import { FilterService } from '../../common-filter/filter.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-polar-chart',
|
selector: 'app-polar-chart',
|
||||||
@@ -33,9 +35,20 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
// Multi-layer drilldown configuration inputs
|
// Multi-layer drilldown configuration inputs
|
||||||
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
@Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
|
||||||
|
|
||||||
constructor(private dashboardService: Dashboard3Service) { }
|
constructor(
|
||||||
|
private dashboardService: Dashboard3Service,
|
||||||
|
private filterService: FilterService
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Subscribe to filter changes
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.filterService.filterState$.subscribe(filters => {
|
||||||
|
// When filters change, refresh the chart data
|
||||||
|
this.fetchChartData();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize with default data
|
// Initialize with default data
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
@@ -43,6 +56,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log('PolarChartComponent input changes:', changes);
|
console.log('PolarChartComponent input changes:', changes);
|
||||||
|
|
||||||
|
// Initialize filter values if they haven't been initialized yet
|
||||||
|
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||||
|
this.initializeFilterValues();
|
||||||
|
this.filtersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any of the key properties have changed
|
// Check if any of the key properties have changed
|
||||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||||
@@ -85,6 +104,324 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
// Flag to prevent infinite loops
|
// Flag to prevent infinite loops
|
||||||
private isFetchingData: boolean = false;
|
private isFetchingData: boolean = false;
|
||||||
|
|
||||||
|
// Subscriptions to unsubscribe on destroy
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
// Add properties for filter functionality
|
||||||
|
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||||
|
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||||
|
private filtersInitialized: boolean = false;
|
||||||
|
|
||||||
|
// Initialize filter values with proper default values based on type
|
||||||
|
private initializeFilterValues(): void {
|
||||||
|
console.log('Initializing filter values');
|
||||||
|
|
||||||
|
// Initialize base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Filter values initialized:', {
|
||||||
|
baseFilters: this.baseFilters,
|
||||||
|
drilldownFilters: this.drilldownFilters,
|
||||||
|
drilldownLayers: this.drilldownLayers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active filters
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||||
|
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||||
|
this.hasActiveLayerFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active layer filters for current drilldown level
|
||||||
|
hasActiveLayerFilters(): boolean {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
return layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters &&
|
||||||
|
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active layer filters for current drilldown level
|
||||||
|
getActiveLayerFilters(): any[] {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
if (layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters) {
|
||||||
|
return this.drilldownLayers[layerIndex].filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filter options for dropdown/multiselect filters
|
||||||
|
getFilterOptions(filter: any): string[] {
|
||||||
|
if (filter.options) {
|
||||||
|
if (Array.isArray(filter.options)) {
|
||||||
|
return filter.options;
|
||||||
|
} else if (typeof filter.options === 'string') {
|
||||||
|
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an option is selected for multiselect filters
|
||||||
|
isOptionSelected(filter: any, option: string): boolean {
|
||||||
|
if (!filter.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.includes(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.value === option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter changes
|
||||||
|
onBaseFilterChange(filter: any): void {
|
||||||
|
console.log('Base filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter changes
|
||||||
|
onDrilldownFilterChange(filter: any): void {
|
||||||
|
console.log('Drilldown filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter changes
|
||||||
|
onLayerFilterChange(filter: any): void {
|
||||||
|
console.log('Layer filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiselect changes
|
||||||
|
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
// Initialize filter.value as array if it's not already
|
||||||
|
if (!Array.isArray(filter.value)) {
|
||||||
|
filter.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add option to array if not already present
|
||||||
|
if (!filter.value.includes(option)) {
|
||||||
|
filter.value.push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove option from array
|
||||||
|
filter.value = filter.value.filter((item: string) => item !== option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||||
|
filter.value = dateRange;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
onToggleChange(filter: any, checked: boolean): void {
|
||||||
|
filter.value = checked;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselect(filter: any, context: string): void {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
if (this.isMultiselectOpen(filter, context)) {
|
||||||
|
this.openMultiselects.delete(filterId);
|
||||||
|
} else {
|
||||||
|
// Close all other multiselects first
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.openMultiselects.set(filterId, context);
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
this.addDocumentClickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add document click handler to close dropdowns when clicking outside
|
||||||
|
private addDocumentClickHandler(): void {
|
||||||
|
if (!this.documentClickHandler) {
|
||||||
|
this.documentClickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside any multiselect dropdown
|
||||||
|
if (!target.closest('.multiselect-container')) {
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler!);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
private removeDocumentClickHandler(): void {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
this.documentClickHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if multiselect dropdown is open
|
||||||
|
isMultiselectOpen(filter: any, context: string): boolean {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
return this.openMultiselects.has(filterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of selected options for a multiselect filter
|
||||||
|
getSelectedOptionsCount(filter: any): number {
|
||||||
|
if (!filter.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
clearAllFilters(): void {
|
||||||
|
// Clear base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all multiselect dropdowns
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
fetchChartData(): void {
|
fetchChartData(): void {
|
||||||
// Set flag to prevent recursive calls
|
// Set flag to prevent recursive calls
|
||||||
this.isFetchingData = true;
|
this.isFetchingData = true;
|
||||||
@@ -117,7 +454,49 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
filterParams = JSON.stringify(filterObj);
|
filterParams = JSON.stringify(filterObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Base filter parameters:', filterParams);
|
|
||||||
|
// Add common filters to filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
console.log('Common filters from service:', commonFilters);
|
||||||
|
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with base filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add base filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const baseFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, baseFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse base filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters using the field name as the key, not the filter id
|
||||||
|
Object.keys(commonFilters).forEach(filterId => {
|
||||||
|
const filterValue = commonFilters[filterId];
|
||||||
|
// Find the filter definition to get the field name
|
||||||
|
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||||
|
if (filterDef && filterDef.field) {
|
||||||
|
const fieldName = filterDef.field;
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[fieldName] = filterValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to using filterId as field name if no field is defined
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[filterId] = filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Final filter parameters:', filterParams);
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/polar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/polar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
@@ -287,6 +666,35 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add common filters to drilldown filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with drilldown filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add drilldown filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const drilldownFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters
|
||||||
|
Object.keys(commonFilters).forEach(key => {
|
||||||
|
const value = commonFilters[key];
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
mergedFilterObj[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/polar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/polar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
console.log('Drilldown data URL:', url);
|
console.log('Drilldown data URL:', url);
|
||||||
@@ -307,7 +715,6 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
// Handle the actual data structure returned by the API
|
// Handle the actual data structure returned by the API
|
||||||
if (data && data.chartLabels && data.chartData) {
|
if (data && data.chartLabels && data.chartData) {
|
||||||
// For polar charts, we need to extract the data differently
|
// For polar charts, we need to extract the data differently
|
||||||
// The first dataset's data array contains the values for the polar chart
|
|
||||||
this.noDataAvailable = data.chartLabels.length === 0;
|
this.noDataAvailable = data.chartLabels.length === 0;
|
||||||
this.polarAreaChartLabels = data.chartLabels;
|
this.polarAreaChartLabels = data.chartLabels;
|
||||||
if (data.chartData && data.chartData.length > 0) {
|
if (data.chartData && data.chartData.length > 0) {
|
||||||
@@ -417,13 +824,13 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
// Get the label of the clicked element
|
// Get the label of the clicked element
|
||||||
const clickedLabel = this.polarAreaChartLabels[clickedIndex];
|
const clickedLabel = this.polarAreaChartLabels[clickedIndex];
|
||||||
|
|
||||||
console.log('Clicked on polar area:', { index: clickedIndex, label: clickedLabel });
|
console.log('Clicked on polar point:', { index: clickedIndex, label: clickedLabel });
|
||||||
|
|
||||||
// If we're not at the base level, store original data
|
// If we're not at the base level, store original data
|
||||||
if (this.currentDrilldownLevel === 0) {
|
if (this.currentDrilldownLevel === 0) {
|
||||||
// Store original data before entering drilldown mode
|
// Store original data before entering drilldown mode
|
||||||
this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels];
|
this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels];
|
||||||
this.originalPolarAreaChartData = [...this.polarAreaChartData];
|
this.originalPolarAreaChartData = JSON.parse(JSON.stringify(this.polarAreaChartData));
|
||||||
console.log('Stored original data for drilldown');
|
console.log('Stored original data for drilldown');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,6 +898,12 @@ export class PolarChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public chartHovered(e: any): void {
|
public chartHovered(e: any): void {
|
||||||
console.log(e);
|
console.log('Polar chart hovered:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||||
|
// Clean up document click handler
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,282 @@
|
|||||||
<div style="display: block">
|
<div style="display: block; height: 100%; width: 100%;">
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Filter Controls Section -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<!-- Base Filters -->
|
||||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
Back to Level {{currentDrilldownLevel - 1}}
|
<h4>Base Filters</h4>
|
||||||
</button>
|
<div class="filter-controls">
|
||||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
Back to Main View
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
</button>
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || 'Radar Chart'}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No data message -->
|
<!-- No data message -->
|
||||||
@@ -16,7 +285,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chart display -->
|
<!-- Chart display -->
|
||||||
<div *ngIf="!noDataAvailable">
|
<div *ngIf="!noDataAvailable" style="position: relative; height: calc(100% - 50px);">
|
||||||
<canvas baseChart
|
<canvas baseChart
|
||||||
[datasets]="radarChartData"
|
[datasets]="radarChartData"
|
||||||
[labels]="radarChartLabels"
|
[labels]="radarChartLabels"
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
.filter-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
flex: 1 1 300px;
|
||||||
|
min-width: 250px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.filter-text-input,
|
||||||
|
.filter-select,
|
||||||
|
.filter-date {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 34px;
|
||||||
|
|
||||||
|
.multiselect-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-value {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range {
|
||||||
|
.date-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New header row styling
|
||||||
|
.header-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filter-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
|
||||||
|
import { FilterService } from '../../common-filter/filter.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-radar-chart',
|
selector: 'app-radar-chart',
|
||||||
@@ -61,16 +63,41 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
// Flag to prevent infinite loops
|
// Flag to prevent infinite loops
|
||||||
private isFetchingData: boolean = false;
|
private isFetchingData: boolean = false;
|
||||||
|
|
||||||
|
// Subscriptions to unsubscribe on destroy
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
constructor(private dashboardService: Dashboard3Service) { }
|
// Add properties for filter functionality
|
||||||
|
private openMultiselects: Map<string, string> = new Map(); // Map of filterId -> context
|
||||||
|
private documentClickHandler: ((event: MouseEvent) => void) | null = null;
|
||||||
|
private filtersInitialized: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dashboardService: Dashboard3Service,
|
||||||
|
private filterService: FilterService
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Subscribe to filter changes
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.filterService.filterState$.subscribe(filters => {
|
||||||
|
// When filters change, refresh the chart data
|
||||||
|
this.fetchChartData();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log('RadarChartComponent input changes:', changes);
|
console.log('RadarChartComponent input changes:', changes);
|
||||||
|
|
||||||
|
// Initialize filter values if they haven't been initialized yet
|
||||||
|
if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
|
||||||
|
this.initializeFilterValues();
|
||||||
|
this.filtersInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any of the key properties have changed
|
// Check if any of the key properties have changed
|
||||||
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange;
|
||||||
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange;
|
||||||
@@ -93,7 +120,317 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
|||||||
this.fetchChartData();
|
this.fetchChartData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize filter values with proper default values based on type
|
||||||
|
private initializeFilterValues(): void {
|
||||||
|
console.log('Initializing filter values');
|
||||||
|
|
||||||
|
// Initialize base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.value === undefined || filter.value === null) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'multiselect':
|
||||||
|
filter.value = [];
|
||||||
|
break;
|
||||||
|
case 'date-range':
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
filter.value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Filter values initialized:', {
|
||||||
|
baseFilters: this.baseFilters,
|
||||||
|
drilldownFilters: this.drilldownFilters,
|
||||||
|
drilldownLayers: this.drilldownLayers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active filters
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return (this.baseFilters && this.baseFilters.length > 0) ||
|
||||||
|
(this.drilldownFilters && this.drilldownFilters.length > 0) ||
|
||||||
|
this.hasActiveLayerFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are active layer filters for current drilldown level
|
||||||
|
hasActiveLayerFilters(): boolean {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
return layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters &&
|
||||||
|
this.drilldownLayers[layerIndex].filters.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active layer filters for current drilldown level
|
||||||
|
getActiveLayerFilters(): any[] {
|
||||||
|
if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
|
||||||
|
const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
|
||||||
|
if (layerIndex < this.drilldownLayers.length &&
|
||||||
|
this.drilldownLayers[layerIndex].filters) {
|
||||||
|
return this.drilldownLayers[layerIndex].filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filter options for dropdown/multiselect filters
|
||||||
|
getFilterOptions(filter: any): string[] {
|
||||||
|
if (filter.options) {
|
||||||
|
if (Array.isArray(filter.options)) {
|
||||||
|
return filter.options;
|
||||||
|
} else if (typeof filter.options === 'string') {
|
||||||
|
return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an option is selected for multiselect filters
|
||||||
|
isOptionSelected(filter: any, option: string): boolean {
|
||||||
|
if (!filter.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.includes(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.value === option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle base filter changes
|
||||||
|
onBaseFilterChange(filter: any): void {
|
||||||
|
console.log('Base filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drilldown filter changes
|
||||||
|
onDrilldownFilterChange(filter: any): void {
|
||||||
|
console.log('Drilldown filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle layer filter changes
|
||||||
|
onLayerFilterChange(filter: any): void {
|
||||||
|
console.log('Layer filter changed:', filter);
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiselect changes
|
||||||
|
onMultiSelectChange(filter: any, option: string, event: any): void {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
// Initialize filter.value as array if it's not already
|
||||||
|
if (!Array.isArray(filter.value)) {
|
||||||
|
filter.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Add option to array if not already present
|
||||||
|
if (!filter.value.includes(option)) {
|
||||||
|
filter.value.push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove option from array
|
||||||
|
filter.value = filter.value.filter((item: string) => item !== option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
|
||||||
|
filter.value = dateRange;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
onToggleChange(filter: any, checked: boolean): void {
|
||||||
|
filter.value = checked;
|
||||||
|
// Refresh data when filter changes
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle multiselect dropdown visibility
|
||||||
|
toggleMultiselect(filter: any, context: string): void {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
if (this.isMultiselectOpen(filter, context)) {
|
||||||
|
this.openMultiselects.delete(filterId);
|
||||||
|
} else {
|
||||||
|
// Close all other multiselects first
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.openMultiselects.set(filterId, context);
|
||||||
|
|
||||||
|
// Add document click handler to close dropdown when clicking outside
|
||||||
|
this.addDocumentClickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add document click handler to close dropdowns when clicking outside
|
||||||
|
private addDocumentClickHandler(): void {
|
||||||
|
if (!this.documentClickHandler) {
|
||||||
|
this.documentClickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside any multiselect dropdown
|
||||||
|
if (!target.closest('.multiselect-container')) {
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.documentClickHandler!);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove document click handler
|
||||||
|
private removeDocumentClickHandler(): void {
|
||||||
|
if (this.documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this.documentClickHandler);
|
||||||
|
this.documentClickHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if multiselect dropdown is open
|
||||||
|
isMultiselectOpen(filter: any, context: string): boolean {
|
||||||
|
const filterId = `${context}-${filter.field}`;
|
||||||
|
return this.openMultiselects.has(filterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get count of selected options for a multiselect filter
|
||||||
|
getSelectedOptionsCount(filter: any): number {
|
||||||
|
if (!filter.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.value)) {
|
||||||
|
return filter.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
clearAllFilters(): void {
|
||||||
|
// Clear base filters
|
||||||
|
if (this.baseFilters) {
|
||||||
|
this.baseFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drilldown filters
|
||||||
|
if (this.drilldownFilters) {
|
||||||
|
this.drilldownFilters.forEach(filter => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear layer filters
|
||||||
|
if (this.drilldownLayers) {
|
||||||
|
this.drilldownLayers.forEach(layer => {
|
||||||
|
if (layer.filters) {
|
||||||
|
layer.filters.forEach((filter: any) => {
|
||||||
|
if (filter.type === 'multiselect') {
|
||||||
|
filter.value = [];
|
||||||
|
} else if (filter.type === 'date-range') {
|
||||||
|
filter.value = { start: null, end: null };
|
||||||
|
} else if (filter.type === 'toggle') {
|
||||||
|
filter.value = false;
|
||||||
|
} else {
|
||||||
|
filter.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all multiselect dropdowns
|
||||||
|
this.openMultiselects.clear();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
this.fetchChartData();
|
||||||
|
}
|
||||||
|
|
||||||
fetchChartData(): void {
|
fetchChartData(): void {
|
||||||
// Set flag to prevent recursive calls
|
// Set flag to prevent recursive calls
|
||||||
this.isFetchingData = true;
|
this.isFetchingData = true;
|
||||||
@@ -126,7 +463,49 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
|||||||
filterParams = JSON.stringify(filterObj);
|
filterParams = JSON.stringify(filterObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Base filter parameters:', filterParams);
|
|
||||||
|
// Add common filters to filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
console.log('Common filters from service:', commonFilters);
|
||||||
|
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with base filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add base filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const baseFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, baseFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse base filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters using the field name as the key, not the filter id
|
||||||
|
Object.keys(commonFilters).forEach(filterId => {
|
||||||
|
const filterValue = commonFilters[filterId];
|
||||||
|
// Find the filter definition to get the field name
|
||||||
|
const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
|
||||||
|
if (filterDef && filterDef.field) {
|
||||||
|
const fieldName = filterDef.field;
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[fieldName] = filterValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to using filterId as field name if no field is defined
|
||||||
|
if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
|
||||||
|
mergedFilterObj[filterId] = filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Final filter parameters:', filterParams);
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/radar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/radar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
@@ -298,6 +677,35 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add common filters to drilldown filter parameters
|
||||||
|
const commonFilters = this.filterService.getFilterValues();
|
||||||
|
if (Object.keys(commonFilters).length > 0) {
|
||||||
|
// Merge common filters with drilldown filters
|
||||||
|
const mergedFilterObj = {};
|
||||||
|
|
||||||
|
// Add drilldown filters first
|
||||||
|
if (filterParams) {
|
||||||
|
try {
|
||||||
|
const drilldownFilterObj = JSON.parse(filterParams);
|
||||||
|
Object.assign(mergedFilterObj, drilldownFilterObj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse drilldown filter parameters:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common filters
|
||||||
|
Object.keys(commonFilters).forEach(key => {
|
||||||
|
const value = commonFilters[key];
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
mergedFilterObj[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/radar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/radar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
console.log('Drilldown data URL:', url);
|
console.log('Drilldown data URL:', url);
|
||||||
@@ -321,7 +729,6 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
|||||||
this.noDataAvailable = data.chartLabels.length === 0;
|
this.noDataAvailable = data.chartLabels.length === 0;
|
||||||
this.radarChartLabels = data.chartLabels;
|
this.radarChartLabels = data.chartLabels;
|
||||||
// For radar charts, we need to ensure the data is properly formatted
|
// For radar charts, we need to ensure the data is properly formatted
|
||||||
// Each dataset should have a data array with numeric values
|
|
||||||
this.radarChartData = data.chartData.map(dataset => ({
|
this.radarChartData = data.chartData.map(dataset => ({
|
||||||
...dataset,
|
...dataset,
|
||||||
data: dataset.data ? dataset.data.map(value => {
|
data: dataset.data ? dataset.data.map(value => {
|
||||||
@@ -358,6 +765,7 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
|||||||
this.noDataAvailable = true;
|
this.noDataAvailable = true;
|
||||||
this.radarChartLabels = [];
|
this.radarChartLabels = [];
|
||||||
this.radarChartData = [];
|
this.radarChartData = [];
|
||||||
|
// Keep current data in case of error
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -436,7 +844,7 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
|||||||
if (this.currentDrilldownLevel === 0) {
|
if (this.currentDrilldownLevel === 0) {
|
||||||
// Store original data before entering drilldown mode
|
// Store original data before entering drilldown mode
|
||||||
this.originalRadarChartLabels = [...this.radarChartLabels];
|
this.originalRadarChartLabels = [...this.radarChartLabels];
|
||||||
this.originalRadarChartData = [...this.radarChartData];
|
this.originalRadarChartData = JSON.parse(JSON.stringify(this.radarChartData));
|
||||||
console.log('Stored original data for drilldown');
|
console.log('Stored original data for drilldown');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,6 +912,12 @@ export class RadarChartComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public chartHovered(e: any): void {
|
public chartHovered(e: any): void {
|
||||||
console.log(e);
|
console.log('Radar chart hovered:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||||
|
// Clean up document click handler
|
||||||
|
this.removeDocumentClickHandler();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,282 @@
|
|||||||
<div style="display: block">
|
<div style="display: block; height: 100%; width: 100%;">
|
||||||
<!-- Drilldown mode indicator -->
|
<!-- Filter Controls Section -->
|
||||||
<div *ngIf="currentDrilldownLevel > 0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
<span style="font-weight: bold; color: #333;">Drilldown Level: {{currentDrilldownLevel}}</span>
|
<!-- Base Filters -->
|
||||||
<button (click)="navigateBack()" style="margin-left: 10px; padding: 2px 8px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
Back to Level {{currentDrilldownLevel - 1}}
|
<h4>Base Filters</h4>
|
||||||
</button>
|
<div class="filter-controls">
|
||||||
<button (click)="resetToOriginalData()" style="margin-left: 10px; padding: 2px 8px; background-color: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
Back to Main View
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
</button>
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || 'Scatter Chart'}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No data message -->
|
<!-- No data message -->
|
||||||
@@ -16,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)">
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
.filter-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
flex: 1 1 300px;
|
||||||
|
min-width: 250px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.filter-text-input,
|
||||||
|
.filter-select,
|
||||||
|
.filter-date {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 34px;
|
||||||
|
|
||||||
|
.multiselect-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-value {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range {
|
||||||
|
.date-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New header row styling
|
||||||
|
.header-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filter-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
import { ChartData,ChartDataset } from 'chart.js';
|
import { ChartData,ChartDataset,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);
|
if (Object.keys(mergedFilterObj).length > 0) {
|
||||||
|
filterParams = JSON.stringify(mergedFilterObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Drilldown filter parameters:', drilldownFilterParams);
|
|
||||||
|
|
||||||
// Use drilldown filters if available, otherwise use layer filters
|
|
||||||
const finalFilterParams = drilldownFilterParams || filterParams;
|
|
||||||
console.log('Final filter parameters:', finalFilterParams);
|
|
||||||
|
|
||||||
// Log the URL that will be called
|
// Log the URL that will be called
|
||||||
const url = `chart/getdashjson/scatter?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
const url = `chart/getdashjson/scatter?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
|
||||||
@@ -312,7 +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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,27 +1,307 @@
|
|||||||
<table class="table">
|
<div class="to-do-chart-container">
|
||||||
<thead>
|
<!-- Filter Controls Section -->
|
||||||
<th class="c-col">#</th>
|
<div class="filter-section" *ngIf="hasActiveFilters()">
|
||||||
<th>Item</th>
|
<!-- Base Filters -->
|
||||||
<th></th>
|
<div class="filter-group" *ngIf="baseFilters && baseFilters.length > 0">
|
||||||
</thead>
|
<h4>Base Filters</h4>
|
||||||
<tr class="ui basic segment" *ngFor="let todo of todoList; let i = index">
|
<div class="filter-controls">
|
||||||
<td class="c-col">{{i + 1}}</td>
|
<div *ngFor="let filter of baseFilters" class="filter-item">
|
||||||
<td>{{todo}}</td>
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
<td style="text-align:right">
|
|
||||||
<a routerLink="." (click)="removeTodo(i)">
|
<!-- Text Filter -->
|
||||||
<clr-icon shape="times"></clr-icon>
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
</a>
|
<input type="text"
|
||||||
</td>
|
[(ngModel)]="filter.value"
|
||||||
</tr>
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
<tr>
|
[placeholder]="filter.field"
|
||||||
<td></td>
|
class="clr-input filter-text-input">
|
||||||
<td>
|
</div>
|
||||||
<input [(ngModel)]="todo" placeholder="Add Todo" class="clr-input">
|
|
||||||
</td>
|
<!-- Dropdown Filter -->
|
||||||
<td style="text-align:right">
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
<a routerLink="." color='primary' (click)="addTodo(todo)">
|
<select [(ngModel)]="filter.value"
|
||||||
<clr-icon shape="plus"></clr-icon>
|
(ngModelChange)="onBaseFilterChange(filter)"
|
||||||
</a>
|
class="clr-select filter-select">
|
||||||
</td>
|
<option value="">Select {{ filter.field }}</option>
|
||||||
</tr>
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
</table>
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'base-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filter-group" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0">
|
||||||
|
<h4>Drilldown Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of drilldownFilters" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'drilldown-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filter-group" *ngIf="hasActiveLayerFilters()">
|
||||||
|
<h4>Layer Filters</h4>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div *ngFor="let filter of getActiveLayerFilters()" class="filter-item">
|
||||||
|
<div class="filter-label">{{ filter.field }} ({{ filter.type || 'text' }})</div>
|
||||||
|
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-input">
|
||||||
|
<input type="text"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
[placeholder]="filter.field"
|
||||||
|
class="clr-input filter-text-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-input">
|
||||||
|
<select [(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="clr-select filter-select">
|
||||||
|
<option value="">Select {{ filter.field }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Select Filter - Updated to show key first, then dropdown on click -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-input multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span class="multiselect-label">{{ filter.field }}</span>
|
||||||
|
<span class="multiselect-value" *ngIf="getSelectedOptionsCount(filter) > 0">
|
||||||
|
({{ getSelectedOptionsCount(filter) }} selected)
|
||||||
|
</span>
|
||||||
|
<clr-icon shape="caret down" class="dropdown-icon"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<div *ngFor="let option of getFilterOptions(filter); let i = index" class="checkbox-item">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
[id]="'layer-' + filter.field + '-' + i"
|
||||||
|
class="clr-checkbox">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + i" class="checkbox-label">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-input date-range">
|
||||||
|
<div class="date-input-group">
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.start"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: $event, end: filter.value.end })"
|
||||||
|
placeholder="Start Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
<span class="date-separator">to</span>
|
||||||
|
<input type="date"
|
||||||
|
[(ngModel)]="filter.value.end"
|
||||||
|
(ngModelChange)="onDateRangeChange(filter, { start: filter.value.start, end: $event })"
|
||||||
|
placeholder="End Date"
|
||||||
|
class="clr-input filter-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-input toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
[(ngModel)]="filter.value"
|
||||||
|
(ngModelChange)="onToggleChange(filter, $event)"
|
||||||
|
clrToggle
|
||||||
|
class="clr-toggle">
|
||||||
|
<label class="toggle-label">{{ filter.field }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">Clear All Filters</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header row with chart title and drilldown navigation -->
|
||||||
|
<div class="clr-row header-row">
|
||||||
|
<div class="clr-col-6">
|
||||||
|
<h3 class="chart-title">{{charttitle || 'To Do Chart'}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-6" style="text-align: right;">
|
||||||
|
<!-- Add drilldown navigation controls -->
|
||||||
|
<button class="btn btn-sm btn-link" *ngIf="drilldownEnabled && drilldownStack.length > 0" (click)="navigateBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show current drilldown level -->
|
||||||
|
<div class="clr-row" *ngIf="drilldownEnabled && drilldownStack.length > 0">
|
||||||
|
<div class="clr-col-12">
|
||||||
|
<div class="alert alert-info" style="padding: 8px 12px; margin-bottom: 12px;">
|
||||||
|
<div class="alert-items">
|
||||||
|
<div class="alert-item static">
|
||||||
|
<div class="alert-icon-wrapper">
|
||||||
|
<cds-icon class="alert-icon" shape="info-circle"></cds-icon>
|
||||||
|
</div>
|
||||||
|
<span class="alert-text">
|
||||||
|
Drilldown Level: {{currentDrilldownLevel}}
|
||||||
|
<span *ngIf="drilldownStack.length > 0">
|
||||||
|
(Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Existing content -->
|
||||||
|
<div class="todo-table-container">
|
||||||
|
<table class="table todo-table">
|
||||||
|
<thead>
|
||||||
|
<th class="c-col">#</th>
|
||||||
|
<th>Item</th>
|
||||||
|
<th></th>
|
||||||
|
</thead>
|
||||||
|
<tr class="ui basic segment" *ngFor="let todo of todoList; let i = index">
|
||||||
|
<td class="c-col">{{i + 1}}</td>
|
||||||
|
<td>{{todo}}</td>
|
||||||
|
<td style="text-align:right">
|
||||||
|
<a (click)="removeTodo(i)" class="remove-button">
|
||||||
|
<clr-icon shape="times"></clr-icon>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</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>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
// Multi-layer drilldown state tracking
|
||||||
|
drilldownStack: any[] = []; // Stack to track drilldown navigation history
|
||||||
|
currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
|
||||||
|
originalTodoList: string[] = [];
|
||||||
|
|
||||||
constructor() { }
|
constructor(
|
||||||
|
private 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 {
|
||||||
|
// 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 {
|
} else {
|
||||||
console.log('Missing required data for to-do chart:', { table: this.table });
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,64 +1,73 @@
|
|||||||
<!-- Display Mode - No configuration UI in runner -->
|
|
||||||
<div class="compact-filter">
|
<div class="compact-filter">
|
||||||
<div class="filter-header">
|
<div class="filter-header" (click)="toggleFilter()">
|
||||||
<span class="filter-label" *ngIf="filterLabel">{{ filterLabel }}</span>
|
<span class="filter-label" *ngIf="filterLabel">{{ filterLabel }}</span>
|
||||||
<span class="filter-key" *ngIf="!filterLabel && filterKey">{{ filterKey }}</span>
|
<span class="filter-key" *ngIf="!filterLabel && filterKey">{{ filterKey }}</span>
|
||||||
<span class="filter-type">({{ filterType }})</span>
|
<span class="filter-type">({{ filterType }})</span>
|
||||||
</div>
|
<clr-icon shape="caret down" class="expand-icon" *ngIf="!isExpanded"></clr-icon>
|
||||||
|
<clr-icon shape="caret up" class="expand-icon" *ngIf="isExpanded"></clr-icon>
|
||||||
<!-- 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>
|
</div>
|
||||||
|
|
||||||
<!-- Dropdown Filter -->
|
<div class="filter-content" *ngIf="isExpanded">
|
||||||
<div class="filter-control" *ngIf="filterType === 'dropdown'">
|
<!-- Text Filter -->
|
||||||
<select [(ngModel)]="filterValue"
|
<div class="filter-control" *ngIf="filterType === 'text'">
|
||||||
(ngModelChange)="onFilterValueChange($event)"
|
<input type="text"
|
||||||
class="clr-select compact-select">
|
[(ngModel)]="filterValue"
|
||||||
<option value="">{{ filterLabel || filterKey }}</option>
|
(ngModelChange)="onFilterValueChange($event)"
|
||||||
<option *ngFor="let option of filterOptions" [value]="option">{{ option }}</option>
|
[placeholder]="filterLabel || filterKey"
|
||||||
</select>
|
class="clr-input compact-input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Multi-Select Filter -->
|
<!-- Dropdown Filter -->
|
||||||
<div class="filter-control" *ngIf="filterType === 'multiselect'">
|
<div class="filter-control" *ngIf="filterType === 'dropdown'">
|
||||||
<div class="checkbox-group">
|
<select [(ngModel)]="filterValue"
|
||||||
<div *ngFor="let option of filterOptions" class="checkbox-item">
|
(ngModelChange)="onFilterValueChange($event)"
|
||||||
<input type="checkbox"
|
class="clr-select compact-select">
|
||||||
[checked]="filterValue && filterValue.includes(option)"
|
<option value="">{{ filterLabel || filterKey }}</option>
|
||||||
(change)="onMultiSelectChange(option, $event)"
|
<option *ngFor="let option of filterOptions; let i = index" [value]="option">{{ option }}</option>
|
||||||
[id]="'checkbox-' + option">
|
</select>
|
||||||
<label [for]="'checkbox-' + option">{{ option }}</label>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Date Range Filter -->
|
<!-- Date Range Filter -->
|
||||||
<div class="filter-control date-range" *ngIf="filterType === 'date-range'">
|
<div class="filter-control date-range" *ngIf="filterType === 'date-range'">
|
||||||
<input type="date"
|
<div class="date-input-group">
|
||||||
[(ngModel)]="filterValue.start"
|
<input type="date"
|
||||||
(ngModelChange)="onDateRangeChange({ start: $event, end: filterValue.end })"
|
[(ngModel)]="filterValue.start"
|
||||||
placeholder="Start Date"
|
(ngModelChange)="onDateRangeChange({ start: $event, end: filterValue.end })"
|
||||||
class="clr-input compact-date">
|
placeholder="Start Date"
|
||||||
<input type="date"
|
class="clr-input compact-date">
|
||||||
[(ngModel)]="filterValue.end"
|
<span class="date-separator">to</span>
|
||||||
(ngModelChange)="onDateRangeChange({ start: filterValue.start, end: $event })"
|
<input type="date"
|
||||||
placeholder="End Date"
|
[(ngModel)]="filterValue.end"
|
||||||
class="clr-input compact-date">
|
(ngModelChange)="onDateRangeChange({ start: filterValue.start, end: $event })"
|
||||||
</div>
|
placeholder="End Date"
|
||||||
|
class="clr-input compact-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Toggle Filter -->
|
<!-- Toggle Filter -->
|
||||||
<div class="filter-control toggle" *ngIf="filterType === 'toggle'">
|
<div class="filter-control toggle" *ngIf="filterType === 'toggle'">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[(ngModel)]="filterValue"
|
[(ngModel)]="filterValue"
|
||||||
(ngModelChange)="onToggleChange($event)"
|
(ngModelChange)="onToggleChange($event)"
|
||||||
clrToggle
|
clrToggle
|
||||||
class="clr-toggle">
|
class="clr-toggle">
|
||||||
<label class="toggle-label">{{ filterLabel || filterKey }}</label>
|
<label class="toggle-label">{{ filterLabel || filterKey }}</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,74 +1,149 @@
|
|||||||
.compact-filter {
|
.compact-filter {
|
||||||
padding: 10px;
|
display: block;
|
||||||
border: 1px solid #ddd;
|
min-width: 200px;
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 8px;
|
||||||
|
padding: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #d7d7d7;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-bottom: 10px;
|
font-size: 14px;
|
||||||
background-color: #f8f8f8;
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
.filter-header {
|
.filter-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
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 {
|
.filter-label, .filter-key {
|
||||||
font-weight: bold;
|
font-weight: 500;
|
||||||
|
color: #333333;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-type {
|
.filter-type {
|
||||||
font-size: 0.8em;
|
font-size: 12px;
|
||||||
color: #666;
|
color: #666666;
|
||||||
|
margin: 0 8px;
|
||||||
|
background: #eaeaea;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: #666666;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-control {
|
.filter-content {
|
||||||
margin-bottom: 10px;
|
padding: 15px;
|
||||||
|
|
||||||
.compact-input, .compact-select, .compact-date {
|
.filter-control {
|
||||||
width: 100%;
|
margin-bottom: 12px;
|
||||||
padding: 5px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact-multiselect {
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.checkbox-item {
|
&:last-child {
|
||||||
margin: 5px 0;
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
input[type="checkbox"] {
|
|
||||||
margin-right: 5px;
|
&.date-range {
|
||||||
|
.date-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.date-range {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
.compact-date {
|
&.toggle {
|
||||||
flex: 1;
|
display: flex;
|
||||||
}
|
align-items: center;
|
||||||
}
|
gap: 8px;
|
||||||
|
|
||||||
&.toggle {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.clr-toggle {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-label {
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
@@ -20,20 +20,24 @@ export class CompactFilterRunnerComponent implements OnInit, OnChanges {
|
|||||||
availableFilters: Filter[] = [];
|
availableFilters: Filter[] = [];
|
||||||
availableKeys: string[] = [];
|
availableKeys: string[] = [];
|
||||||
availableValues: string[] = [];
|
availableValues: string[] = [];
|
||||||
|
isExpanded: boolean = false; // Add expansion state
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private filterService: FilterService
|
private filterService: FilterService
|
||||||
) { }
|
) {
|
||||||
|
console.log('=== COMPACT FILTER RUNNER CONSTRUCTOR CALLED ===');
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
console.log('CompactFilterRunnerComponent initialized with inputs:', {
|
console.log('=== COMPACT FILTER RUNNER DEBUG INFO ===');
|
||||||
filterKey: this.filterKey,
|
console.log('Component initialized with inputs:');
|
||||||
filterType: this.filterType,
|
console.log('- filterKey:', this.filterKey);
|
||||||
filterOptions: this.filterOptions,
|
console.log('- filterType:', this.filterType);
|
||||||
filterLabel: this.filterLabel,
|
console.log('- filterOptions:', this.filterOptions);
|
||||||
apiUrl: this.apiUrl,
|
console.log('- filterLabel:', this.filterLabel);
|
||||||
connection: this.connection
|
console.log('- apiUrl:', this.apiUrl);
|
||||||
});
|
console.log('- connection:', this.connection);
|
||||||
|
console.log('========================================');
|
||||||
|
|
||||||
// Register this filter with the filter service
|
// Register this filter with the filter service
|
||||||
this.registerFilter();
|
this.registerFilter();
|
||||||
@@ -41,24 +45,35 @@ export class CompactFilterRunnerComponent implements OnInit, OnChanges {
|
|||||||
// Subscribe to filter definitions to get available filters
|
// Subscribe to filter definitions to get available filters
|
||||||
this.filterService.filters$.subscribe(filters => {
|
this.filterService.filters$.subscribe(filters => {
|
||||||
this.availableFilters = filters;
|
this.availableFilters = filters;
|
||||||
|
console.log('Available filters updated:', filters);
|
||||||
this.updateSelectedFilter();
|
this.updateSelectedFilter();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Subscribe to filter state changes
|
// Subscribe to filter state changes
|
||||||
this.filterService.filterState$.subscribe(state => {
|
this.filterService.filterState$.subscribe(state => {
|
||||||
|
console.log('Filter state updated:', state);
|
||||||
if (this.selectedFilter && state.hasOwnProperty(this.selectedFilter.id)) {
|
if (this.selectedFilter && state.hasOwnProperty(this.selectedFilter.id)) {
|
||||||
this.filterValue = state[this.selectedFilter.id];
|
this.filterValue = state[this.selectedFilter.id];
|
||||||
|
console.log('Filter value updated for', this.selectedFilter.id, ':', this.filterValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log('CompactFilterRunnerComponent inputs changed:', changes);
|
console.log('=== COMPACT FILTER RUNNER CHANGES DEBUG ===');
|
||||||
|
console.log('Component inputs changed:', changes);
|
||||||
|
|
||||||
// If filterKey or filterType changes, re-register the filter
|
// If filterKey or filterType changes, re-register the filter
|
||||||
if (changes.filterKey || changes.filterType || changes.filterOptions) {
|
if (changes.filterKey || changes.filterType || changes.filterOptions) {
|
||||||
|
console.log('Re-registering filter due to input changes');
|
||||||
this.registerFilter();
|
this.registerFilter();
|
||||||
}
|
}
|
||||||
|
console.log('==========================================');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle filter expansion
|
||||||
|
toggleFilter(): void {
|
||||||
|
this.isExpanded = !this.isExpanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register this filter with the filter service
|
// Register this filter with the filter service
|
||||||
@@ -68,6 +83,7 @@ export class CompactFilterRunnerComponent implements OnInit, OnChanges {
|
|||||||
if (this.filterKey) {
|
if (this.filterKey) {
|
||||||
// Get current filter values from the service
|
// Get current filter values from the service
|
||||||
const currentFilterValues = this.filterService.getFilterValues();
|
const currentFilterValues = this.filterService.getFilterValues();
|
||||||
|
console.log('Current filter values from service:', currentFilterValues);
|
||||||
|
|
||||||
// Create a filter definition for this compact filter
|
// Create a filter definition for this compact filter
|
||||||
const filterDef: Filter = {
|
const filterDef: Filter = {
|
||||||
@@ -83,9 +99,11 @@ export class CompactFilterRunnerComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
// Get current filters
|
// Get current filters
|
||||||
const currentFilters = this.filterService.getFilters();
|
const currentFilters = this.filterService.getFilters();
|
||||||
|
console.log('Current filters from service:', currentFilters);
|
||||||
|
|
||||||
// Check if this filter is already registered
|
// Check if this filter is already registered
|
||||||
const existingFilterIndex = currentFilters.findIndex(f => f.id === filterDef.id);
|
const existingFilterIndex = currentFilters.findIndex(f => f.id === filterDef.id);
|
||||||
|
console.log('Existing filter index:', existingFilterIndex);
|
||||||
|
|
||||||
if (existingFilterIndex >= 0) {
|
if (existingFilterIndex >= 0) {
|
||||||
// Preserve the existing filter configuration
|
// Preserve the existing filter configuration
|
||||||
@@ -130,15 +148,20 @@ export class CompactFilterRunnerComponent implements OnInit, OnChanges {
|
|||||||
// Update the selected filter reference
|
// Update the selected filter reference
|
||||||
this.selectedFilter = filterDef;
|
this.selectedFilter = filterDef;
|
||||||
console.log('Selected filter set to:', this.selectedFilter);
|
console.log('Selected filter set to:', this.selectedFilter);
|
||||||
|
} else {
|
||||||
|
console.log('No filterKey provided, skipping filter registration');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelectedFilter(): void {
|
updateSelectedFilter(): void {
|
||||||
|
console.log('Updating selected filter. Filter key:', this.filterKey, 'Available filters:', this.availableFilters);
|
||||||
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;
|
||||||
|
console.log('Found selected filter:', this.selectedFilter);
|
||||||
if (this.selectedFilter) {
|
if (this.selectedFilter) {
|
||||||
// Get current value for this filter from the service
|
// Get current value for this filter from the service
|
||||||
const currentState = this.filterService.getFilterValues();
|
const currentState = this.filterService.getFilterValues();
|
||||||
|
console.log('Current state from service:', currentState);
|
||||||
const filterValue = currentState[this.selectedFilter.id];
|
const filterValue = currentState[this.selectedFilter.id];
|
||||||
if (filterValue !== undefined) {
|
if (filterValue !== undefined) {
|
||||||
this.filterValue = filterValue;
|
this.filterValue = filterValue;
|
||||||
@@ -203,4 +226,20 @@ export class CompactFilterRunnerComponent implements OnInit, OnChanges {
|
|||||||
// Emit the change
|
// Emit the change
|
||||||
this.onFilterValueChange(this.filterValue);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,9 @@
|
|||||||
<!-- <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"
|
<ndc-dynamic class="no-drag"
|
||||||
[ndcDynamicComponent]="item.component"
|
[ndcDynamicComponent]="item.component"
|
||||||
[ndcDynamicInputs]="getComponentInputs(item)"
|
[ndcDynamicInputs]="getComponentInputs(item)"
|
||||||
|
|||||||
@@ -311,12 +311,18 @@ dashboard_name = "Dashtest";
|
|||||||
|
|
||||||
// Compact Filter specific inputs
|
// Compact Filter specific inputs
|
||||||
if (item.name === 'Compact Filter') {
|
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.filterKey !== undefined) inputs.filterKey = item.filterKey;
|
||||||
if (item.filterType !== undefined) inputs.filterType = item.filterType;
|
if (item.filterType !== undefined) inputs.filterType = item.filterType;
|
||||||
if (item.filterLabel !== undefined) inputs.filterLabel = item.filterLabel;
|
if (item.filterLabel !== undefined) inputs.filterLabel = item.filterLabel;
|
||||||
if (item.filterOptions !== undefined) inputs.filterOptions = item.filterOptions;
|
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.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;
|
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
|
// Grid View specific inputs
|
||||||
|
|||||||
Reference in New Issue
Block a user