diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bubble-chart/bubble-chart.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bubble-chart/bubble-chart.component.html index 874bbed..dfddf26 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bubble-chart/bubble-chart.component.html +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bubble-chart/bubble-chart.component.html @@ -1,13 +1,282 @@ -
- -
- Drilldown Level: {{currentDrilldownLevel}} - - +
+ +
+ +
+

Base Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Drilldown Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Layer Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+ +
+
+ + +
+
+

{{charttitle || 'Bubble Chart'}}

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

Base Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Drilldown Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Layer Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+ +
- -
- Drilldown Level: {{currentDrilldownLevel}} - - + +
+
+

{{ charttitle }}

+
+
+ + +
-
-

{{ charttitle }}

+ +
+
+
+
+
+
+ +
+ + Drilldown Level: {{currentDrilldownLevel}} + + (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}}) + + +
+
+
+
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/doughnut-chart/doughnut-chart.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/doughnut-chart/doughnut-chart.component.scss index 9d1cbdc..6d11281 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/doughnut-chart/doughnut-chart.component.scss +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/doughnut-chart/doughnut-chart.component.scss @@ -245,6 +245,186 @@ } } +// Filter section styles +.filter-section { + margin-bottom: 20px; + padding: 15px; + border: 1px solid #ddd; + border-radius: 4px; + background-color: #f9f9f9; +} + +.filter-group { + margin-bottom: 15px; + + h4 { + margin-top: 0; + margin-bottom: 10px; + color: #333; + font-weight: 600; + } +} + +.filter-controls { + display: flex; + flex-wrap: wrap; + gap: 15px; +} + +.filter-item { + flex: 1 1 300px; + min-width: 250px; + padding: 10px; + background: white; + border: 1px solid #e0e0e0; + border-radius: 4px; +} + +.filter-label { + font-weight: 500; + margin-bottom: 8px; + color: #555; + font-size: 14px; +} + +.filter-input { + width: 100%; + + .filter-text-input, + .filter-select, + .filter-date { + width: 100%; + padding: 6px 12px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + } + + .filter-select { + height: 34px; + } +} + +.multiselect-container { + position: relative; +} + +.multiselect-display { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px; + border: 1px solid #ccc; + border-radius: 4px; + background: white; + cursor: pointer; + min-height: 34px; + + .multiselect-label { + flex: 1; + font-size: 14px; + } + + .multiselect-value { + color: #666; + font-size: 12px; + margin-right: 8px; + } + + .dropdown-icon { + flex-shrink: 0; + transition: transform 0.2s ease; + } + + &:hover { + border-color: #999; + } +} + +.multiselect-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 1000; + background: white; + border: 1px solid #ccc; + border-top: none; + border-radius: 0 0 4px 4px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + max-height: 200px; + overflow-y: auto; + + .checkbox-group { + padding: 8px; + + .checkbox-item { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + + .checkbox-label { + margin: 0; + font-size: 14px; + cursor: pointer; + } + } + } +} + +.date-range { + .date-input-group { + display: flex; + align-items: center; + gap: 8px; + } + + .date-separator { + margin: 0 5px; + color: #777; + } +} + +.toggle { + display: flex; + align-items: center; + gap: 8px; + + .toggle-label { + margin: 0; + font-size: 14px; + cursor: pointer; + } +} + +.filter-actions { + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #eee; + + .btn { + font-size: 13px; + } +} + +// New header row styling +.header-row { + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px solid #eee; + + .chart-title { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #333; + text-align: left; + padding-bottom: 0; + border-bottom: none; + } +} + /* Responsive design */ @media (max-width: 768px) { .doughnut-chart-container { @@ -287,4 +467,18 @@ .compact-filters-container { flex-wrap: wrap; } + + .filter-controls { + flex-direction: column; + } + + .filter-item { + min-width: 100%; + } + + .header-row { + .chart-title { + font-size: 16px; + } + } } \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/doughnut-chart/doughnut-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/doughnut-chart/doughnut-chart.component.ts index 106e61c..c7373b5 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/doughnut-chart/doughnut-chart.component.ts +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/doughnut-chart/doughnut-chart.component.ts @@ -8,7 +8,7 @@ import { Subscription } from 'rxjs'; templateUrl: './doughnut-chart.component.html', styleUrls: ['./doughnut-chart.component.scss'] }) -export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked { +export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy { @Input() xAxis: string; @Input() yAxis: string | string[]; @Input() table: string; @@ -102,6 +102,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck // Subscriptions to unsubscribe on destroy private subscriptions: Subscription[] = []; + // Add properties for filter functionality + private openMultiselects: Map = new Map(); // Map of filterId -> context + private documentClickHandler: ((event: MouseEvent) => void) | null = null; + private filtersInitialized: boolean = false; + constructor( private dashboardService: Dashboard3Service, private filterService: FilterService @@ -164,6 +169,12 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck ngOnChanges(changes: SimpleChanges): void { console.log('DoughnutChartComponent input changes:', changes); + // Initialize filter values if they haven't been initialized yet + if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { + this.initializeFilterValues(); + this.filtersInitialized = true; + } + // Check if any of the key properties have changed const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; @@ -198,12 +209,318 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck ngOnDestroy(): void { this.subscriptions.forEach(sub => sub.unsubscribe()); + // Clean up document click handler + this.removeDocumentClickHandler(); } - // Handle filter changes from compact filters - onFilterChange(event: { filterId: string, value: any }): void { - console.log('Compact filter changed:', event); - // The filter service will automatically trigger chart updates through the subscription + // Initialize filter values with proper default values based on type + private initializeFilterValues(): void { + console.log('Initializing filter values'); + + // Initialize base filters + if (this.baseFilters) { + this.baseFilters.forEach(filter => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + + // Initialize drilldown filters + if (this.drilldownFilters) { + this.drilldownFilters.forEach(filter => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + + // Initialize layer filters + if (this.drilldownLayers) { + this.drilldownLayers.forEach(layer => { + if (layer.filters) { + layer.filters.forEach((filter: any) => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + }); + } + + console.log('Filter values initialized:', { + baseFilters: this.baseFilters, + drilldownFilters: this.drilldownFilters, + drilldownLayers: this.drilldownLayers + }); + } + + // Check if there are active filters + hasActiveFilters(): boolean { + return (this.baseFilters && this.baseFilters.length > 0) || + (this.drilldownFilters && this.drilldownFilters.length > 0) || + this.hasActiveLayerFilters(); + } + + // Check if there are active layer filters for current drilldown level + hasActiveLayerFilters(): boolean { + if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { + const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown + return layerIndex < this.drilldownLayers.length && + this.drilldownLayers[layerIndex].filters && + this.drilldownLayers[layerIndex].filters.length > 0; + } + return false; + } + + // Get active layer filters for current drilldown level + getActiveLayerFilters(): any[] { + if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { + const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown + if (layerIndex < this.drilldownLayers.length && + this.drilldownLayers[layerIndex].filters) { + return this.drilldownLayers[layerIndex].filters; + } + } + return []; + } + + // Get filter options for dropdown/multiselect filters + getFilterOptions(filter: any): string[] { + if (filter.options) { + if (Array.isArray(filter.options)) { + return filter.options; + } else if (typeof filter.options === 'string') { + return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt); + } + } + return []; + } + + // Check if an option is selected for multiselect filters + isOptionSelected(filter: any, option: string): boolean { + if (!filter.value) { + return false; + } + + if (Array.isArray(filter.value)) { + return filter.value.includes(option); + } + + return filter.value === option; + } + + // Handle base filter changes + onBaseFilterChange(filter: any): void { + console.log('Base filter changed:', filter); + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle drilldown filter changes + onDrilldownFilterChange(filter: any): void { + console.log('Drilldown filter changed:', filter); + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle layer filter changes + onLayerFilterChange(filter: any): void { + console.log('Layer filter changed:', filter); + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle multiselect changes + onMultiSelectChange(filter: any, option: string, event: any): void { + const checked = event.target.checked; + + // Initialize filter.value as array if it's not already + if (!Array.isArray(filter.value)) { + filter.value = []; + } + + if (checked) { + // Add option to array if not already present + if (!filter.value.includes(option)) { + filter.value.push(option); + } + } else { + // Remove option from array + filter.value = filter.value.filter((item: string) => item !== option); + } + + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle date range changes + onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void { + filter.value = dateRange; + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle toggle changes + onToggleChange(filter: any, checked: boolean): void { + filter.value = checked; + // Refresh data when filter changes + this.fetchChartData(); + } + + // Toggle multiselect dropdown visibility + toggleMultiselect(filter: any, context: string): void { + const filterId = `${context}-${filter.field}`; + if (this.isMultiselectOpen(filter, context)) { + this.openMultiselects.delete(filterId); + } else { + // Close all other multiselects first + this.openMultiselects.clear(); + this.openMultiselects.set(filterId, context); + + // Add document click handler to close dropdown when clicking outside + this.addDocumentClickHandler(); + } + } + + // Add document click handler to close dropdowns when clicking outside + private addDocumentClickHandler(): void { + if (!this.documentClickHandler) { + this.documentClickHandler = (event: MouseEvent) => { + const target = event.target as HTMLElement; + // Check if click is outside any multiselect dropdown + if (!target.closest('.multiselect-container')) { + this.openMultiselects.clear(); + this.removeDocumentClickHandler(); + } + }; + + // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it + setTimeout(() => { + document.addEventListener('click', this.documentClickHandler!); + }, 0); + } + } + + // Remove document click handler + private removeDocumentClickHandler(): void { + if (this.documentClickHandler) { + document.removeEventListener('click', this.documentClickHandler); + this.documentClickHandler = null; + } + } + + // Check if multiselect dropdown is open + isMultiselectOpen(filter: any, context: string): boolean { + const filterId = `${context}-${filter.field}`; + return this.openMultiselects.has(filterId); + } + + // Get count of selected options for a multiselect filter + getSelectedOptionsCount(filter: any): number { + if (!filter.value) { + return 0; + } + + if (Array.isArray(filter.value)) { + return filter.value.length; + } + + return 0; + } + + // Clear all filters + clearAllFilters(): void { + // Clear base filters + if (this.baseFilters) { + this.baseFilters.forEach(filter => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + + // Clear drilldown filters + if (this.drilldownFilters) { + this.drilldownFilters.forEach(filter => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + + // Clear layer filters + if (this.drilldownLayers) { + this.drilldownLayers.forEach(layer => { + if (layer.filters) { + layer.filters.forEach((filter: any) => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + }); + } + + // Close all multiselect dropdowns + this.openMultiselects.clear(); + + // Refresh data + this.fetchChartData(); } // Public method to refresh data when filters change @@ -289,7 +606,7 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck // Log the URL that will be called const url = `chart/getdashjson/doughnut?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; - console.log('Doughnut chart data URL:', url); + console.log('Chart data URL:', url); // Fetch data from the dashboard service with parameter field and value // For base level, we pass empty parameter and value, but now also pass filters @@ -297,12 +614,10 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck (data: any) => { console.log('Received doughnut chart data:', data); if (data === null) { - console.warn('Doughnut chart API returned null data. Check if the API endpoint is working correctly.'); + console.warn('API returned null data. Check if the API endpoint is working correctly.'); this.noDataAvailable = true; this.doughnutChartLabels = []; this.doughnutChartData = []; - // Validate and sanitize data to show default data - this.validateChartData(); // Reset flag after fetching this.isFetchingData = false; return; @@ -310,50 +625,26 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck // Handle the actual data structure returned by the API if (data && data.chartLabels && data.chartData) { - // For doughnut charts, we need to extract the data differently - // The first dataset's data array contains the values for the doughnut chart + // Backend has already filtered the data, just display it this.noDataAvailable = data.chartLabels.length === 0; - this.doughnutChartLabels = data.chartLabels || []; - if (data.chartData && data.chartData.length > 0) { - this.doughnutChartData = data.chartData[0].data.map(value => { - // Convert to number if it's not already - const numValue = Number(value); - return isNaN(numValue) ? 0 : numValue; - }); - } else { - this.doughnutChartData = []; - } - // Ensure labels and data arrays have the same length - this.syncLabelAndDataArrays(); - // Validate and sanitize data - this.validateChartData(); + this.doughnutChartLabels = data.chartLabels; + this.doughnutChartData = data.chartData; // Trigger change detection this.doughnutChartData = [...this.doughnutChartData]; console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); - } else if (data && data.labels && data.data) { - // Handle the original expected format as fallback + } else if (data && data.labels && data.datasets) { + // Backend has already filtered the data, just display it this.noDataAvailable = data.labels.length === 0; - this.doughnutChartLabels = data.labels || []; - this.doughnutChartData = data.data.map(value => { - // Convert to number if it's not already - const numValue = Number(value); - return isNaN(numValue) ? 0 : numValue; - }); - // Ensure labels and data arrays have the same length - this.syncLabelAndDataArrays(); - // Validate and sanitize data - this.validateChartData(); + this.doughnutChartLabels = data.labels; + this.doughnutChartData = data.datasets[0]?.data || []; // Trigger change detection this.doughnutChartData = [...this.doughnutChartData]; console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); } else { - console.warn('Doughnut chart received data does not have expected structure', data); - // Reset to default data + console.warn('Received data does not have expected structure', data); this.noDataAvailable = true; this.doughnutChartLabels = []; this.doughnutChartData = []; - // Validate and sanitize data to show default data - this.validateChartData(); } // Reset flag after fetching this.isFetchingData = false; @@ -363,21 +654,16 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck this.noDataAvailable = true; this.doughnutChartLabels = []; this.doughnutChartData = []; - // Validate and sanitize data to show default data - this.validateChartData(); // Reset flag after fetching this.isFetchingData = false; + // Keep default data in case of error } ); } else { - console.log('Missing required data for doughnut chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); - // Don't set noDataAvailable to true when there's no required data - // This allows static data to be displayed - this.noDataAvailable = false; - // Validate the chart data to ensure we have some data to display - this.validateChartData(); - // Force a redraw to ensure the chart displays - this.doughnutChartData = [...this.doughnutChartData]; + console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); + this.noDataAvailable = true; + this.doughnutChartLabels = []; + this.doughnutChartData = []; // Reset flag after fetching this.isFetchingData = false; } @@ -475,6 +761,35 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck } } + // Add common filters to drilldown filter parameters + const commonFilters = this.filterService.getFilterValues(); + if (Object.keys(commonFilters).length > 0) { + // Merge common filters with drilldown filters + const mergedFilterObj = {}; + + // Add drilldown filters first + if (filterParams) { + try { + const drilldownFilterObj = JSON.parse(filterParams); + Object.assign(mergedFilterObj, drilldownFilterObj); + } catch (e) { + console.warn('Failed to parse drilldown filter parameters:', e); + } + } + + // Add common filters + Object.keys(commonFilters).forEach(key => { + const value = commonFilters[key]; + if (value !== undefined && value !== null && value !== '') { + mergedFilterObj[key] = value; + } + }); + + if (Object.keys(mergedFilterObj).length > 0) { + filterParams = JSON.stringify(mergedFilterObj); + } + } + // Log the URL that will be called const url = `chart/getdashjson/doughnut?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; console.log('Drilldown data URL:', url); @@ -494,39 +809,18 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck // Handle the actual data structure returned by the API if (data && data.chartLabels && data.chartData) { - // For doughnut charts, we need to extract the data differently - // The first dataset's data array contains the values for the doughnut chart + // Backend has already filtered the data, just display it this.noDataAvailable = data.chartLabels.length === 0; - this.doughnutChartLabels = data.chartLabels || []; - if (data.chartData && data.chartData.length > 0) { - this.doughnutChartData = data.chartData[0].data.map(value => { - // Convert to number if it's not already - const numValue = Number(value); - return isNaN(numValue) ? 0 : numValue; - }); - } else { - this.doughnutChartData = []; - } - // Ensure labels and data arrays have the same length - this.syncLabelAndDataArrays(); - // Validate and sanitize data - this.validateChartData(); + this.doughnutChartLabels = data.chartLabels; + this.doughnutChartData = data.chartData; // Trigger change detection this.doughnutChartData = [...this.doughnutChartData]; console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); - } else if (data && data.labels && data.data) { - // Handle the original expected format as fallback + } else if (data && data.labels && data.datasets) { + // Backend has already filtered the data, just display it this.noDataAvailable = data.labels.length === 0; - this.doughnutChartLabels = data.labels || []; - this.doughnutChartData = data.data.map(value => { - // Convert to number if it's not already - const numValue = Number(value); - return isNaN(numValue) ? 0 : numValue; - }); - // Ensure labels and data arrays have the same length - this.syncLabelAndDataArrays(); - // Validate and sanitize data - this.validateChartData(); + this.doughnutChartLabels = data.labels; + this.doughnutChartData = data.datasets[0]?.data || []; // Trigger change detection this.doughnutChartData = [...this.doughnutChartData]; console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData }); @@ -535,8 +829,6 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck this.noDataAvailable = true; this.doughnutChartLabels = []; this.doughnutChartData = []; - // Validate and sanitize data - this.validateChartData(); } }, (error) => { @@ -604,44 +896,11 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck this.resetToOriginalData(); } } - - /** - * Get color for legend item - * @param index Index of the legend item - */ - public getLegendColor(index: number): string { + + // Get legend color for a specific index + getLegendColor(index: number): string { return this.chartColors[index % this.chartColors.length]; } - - /** - * Ensure labels and data arrays have the same length - */ - private syncLabelAndDataArrays(): void { - // Handle empty arrays - if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) { - return; - } - - const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData.length); - - // Pad the shorter array with default values - while (this.doughnutChartLabels.length < maxLength) { - this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`); - } - - while (this.doughnutChartData.length < maxLength) { - this.doughnutChartData.push(0); - } - - // Truncate the longer array if needed - if (this.doughnutChartLabels.length > maxLength) { - this.doughnutChartLabels = this.doughnutChartLabels.slice(0, maxLength); - } - - if (this.doughnutChartData.length > maxLength) { - this.doughnutChartData = this.doughnutChartData.slice(0, maxLength); - } - } // events public chartClicked(e: any): void { @@ -729,6 +988,6 @@ export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewCheck } public chartHovered(e: any): void { - console.log(e); + console.log('Doughnut chart hovered:', e); } } \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/dynamic-chart/dynamic-chart.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/dynamic-chart/dynamic-chart.component.html index ea261c3..b49f170 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/dynamic-chart/dynamic-chart.component.html +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/dynamic-chart/dynamic-chart.component.html @@ -1,4 +1,285 @@
+ +
+ +
+

Base Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Drilldown Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Layer Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+ +
+
+ + +
+
+

{{charttitle || 'Dynamic Chart'}}

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

Base Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Drilldown Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Layer Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+ +
+
+ + +
+
+

{{charttitle || 'Financial Chart'}}

+
+
+ + +
+
+ + +
+
+
+
+
+
+ +
+ + Drilldown Level: {{currentDrilldownLevel}} + + (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}}) + + +
+
+
+
+
+ +
Drilldown Level: {{currentDrilldownLevel}} diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/financial-chart/financial-chart.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/financial-chart/financial-chart.component.scss index 03eca40..a9282e4 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/financial-chart/financial-chart.component.scss +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/financial-chart/financial-chart.component.scss @@ -1,108 +1,192 @@ -.financial-chart-container { - display: flex; - flex-direction: column; - height: 400px; - min-height: 400px; - padding: 20px; - background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); - border-radius: 12px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - transition: all 0.3s ease; - border: 1px solid #eaeaea; -} - -.financial-chart-container:hover { - box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2); - transform: translateY(-2px); -} - -.chart-title { - font-size: 26px; - font-weight: 700; - color: #2c3e50; +.filter-section { margin-bottom: 20px; - text-align: center; - padding-bottom: 15px; - border-bottom: 2px solid #3498db; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + padding: 15px; + border: 1px solid #ddd; + border-radius: 4px; + background-color: #f9f9f9; } -.chart-wrapper { - position: relative; - flex: 1; - min-height: 250px; - margin: 15px 0; - background: #f8f9fa; - border: 1px solid #e9ecef; - border-radius: 8px; - padding: 10px; - display: flex; - align-items: center; - justify-content: center; -} - -.chart-wrapper canvas { - max-width: 100%; - max-height: 100%; - filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1)); -} - -.chart-wrapper canvas:hover { - filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15)); - transform: scale(1.02); - transition: all 0.3s ease; -} - -.loading-indicator, .no-data-message { - text-align: center; - padding: 30px; - color: #666; - font-size: 18px; - font-style: italic; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; -} - -.loading-indicator p, .no-data-message p { - margin: 10px 0 0 0; -} - -.spinner { - border: 4px solid #f3f3f3; - border-top: 4px solid #3498db; - border-radius: 50%; - width: 40px; - height: 40px; - animation: spin 1s linear infinite; - margin-bottom: 10px; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -/* Responsive design */ -@media (max-width: 768px) { - .financial-chart-container { - padding: 15px; +.filter-group { + margin-bottom: 15px; + + h4 { + margin-top: 0; + margin-bottom: 10px; + color: #333; + font-weight: 600; } +} + +.filter-controls { + display: flex; + flex-wrap: wrap; + gap: 15px; +} + +.filter-item { + flex: 1 1 300px; + min-width: 250px; + padding: 10px; + background: white; + border: 1px solid #e0e0e0; + border-radius: 4px; +} + +.filter-label { + font-weight: 500; + margin-bottom: 8px; + color: #555; + font-size: 14px; +} + +.filter-input { + width: 100%; + + .filter-text-input, + .filter-select, + .filter-date { + width: 100%; + padding: 6px 12px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + } + + .filter-select { + height: 34px; + } +} + +.multiselect-container { + position: relative; +} + +.multiselect-display { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px; + border: 1px solid #ccc; + border-radius: 4px; + background: white; + cursor: pointer; + min-height: 34px; + + .multiselect-label { + flex: 1; + font-size: 14px; + } + + .multiselect-value { + color: #666; + font-size: 12px; + margin-right: 8px; + } + + .dropdown-icon { + flex-shrink: 0; + transition: transform 0.2s ease; + } + + &:hover { + border-color: #999; + } +} + +.multiselect-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 1000; + background: white; + border: 1px solid #ccc; + border-top: none; + border-radius: 0 0 4px 4px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + max-height: 200px; + overflow-y: auto; + + .checkbox-group { + padding: 8px; + + .checkbox-item { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + + .checkbox-label { + margin: 0; + font-size: 14px; + cursor: pointer; + } + } + } +} + +.date-range { + .date-input-group { + display: flex; + align-items: center; + gap: 8px; + } + + .date-separator { + margin: 0 5px; + color: #777; + } +} + +.toggle { + display: flex; + align-items: center; + gap: 8px; + + .toggle-label { + margin: 0; + font-size: 14px; + cursor: pointer; + } +} + +.filter-actions { + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #eee; + + .btn { + font-size: 13px; + } +} + +// New header row styling +.header-row { + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px solid #eee; .chart-title { - font-size: 20px; - margin-bottom: 15px; + margin: 0; + font-size: 18px; + font-weight: 600; + color: #333; + } +} + +// Responsive design +@media (max-width: 768px) { + .filter-controls { + flex-direction: column; } - .chart-wrapper { - min-height: 200px; + .filter-item { + min-width: 100%; } - .no-data-message { - font-size: 16px; - padding: 20px; + .header-row { + .chart-title { + font-size: 16px; + } } } \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/financial-chart/financial-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/financial-chart/financial-chart.component.ts index 7c11055..d46da04 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/financial-chart/financial-chart.component.ts +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/financial-chart/financial-chart.component.ts @@ -1,5 +1,7 @@ import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; +import { FilterService } from '../../common-filter/filter.service'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-financial-chart', @@ -33,9 +35,20 @@ export class FinancialChartComponent implements OnInit, OnChanges { // Multi-layer drilldown configuration inputs @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations - constructor(private dashboardService: Dashboard3Service) { } + constructor( + private dashboardService: Dashboard3Service, + private filterService: FilterService + ) { } ngOnInit(): void { + // Subscribe to filter changes + this.subscriptions.push( + this.filterService.filterState$.subscribe(filters => { + // When filters change, refresh the chart data + this.fetchChartData(); + }) + ); + // Initialize with default data this.fetchChartData(); } @@ -43,6 +56,12 @@ export class FinancialChartComponent implements OnInit, OnChanges { ngOnChanges(changes: SimpleChanges): void { console.log('FinancialChartComponent input changes:', changes); + // Initialize filter values if they haven't been initialized yet + if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { + this.initializeFilterValues(); + this.filtersInitialized = true; + } + // Check if any of the key properties have changed const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; @@ -86,6 +105,14 @@ export class FinancialChartComponent implements OnInit, OnChanges { // Flag to prevent infinite loops private isFetchingData: boolean = false; + // Add properties for filter functionality + private openMultiselects: Map = new Map(); // Map of filterId -> context + private documentClickHandler: ((event: MouseEvent) => void) | null = null; + private filtersInitialized: boolean = false; + + // Subscriptions to unsubscribe on destroy + private subscriptions: Subscription[] = []; + fetchChartData(): void { // Set flag to prevent recursive calls this.isFetchingData = true; @@ -118,7 +145,49 @@ export class FinancialChartComponent implements OnInit, OnChanges { filterParams = JSON.stringify(filterObj); } } - console.log('Base filter parameters:', filterParams); + + // Add common filters to filter parameters + const commonFilters = this.filterService.getFilterValues(); + console.log('Common filters from service:', commonFilters); + + if (Object.keys(commonFilters).length > 0) { + // Merge common filters with base filters + const mergedFilterObj = {}; + + // Add base filters first + if (filterParams) { + try { + const baseFilterObj = JSON.parse(filterParams); + Object.assign(mergedFilterObj, baseFilterObj); + } catch (e) { + console.warn('Failed to parse base filter parameters:', e); + } + } + + // Add common filters using the field name as the key, not the filter id + Object.keys(commonFilters).forEach(filterId => { + const filterValue = commonFilters[filterId]; + // Find the filter definition to get the field name + const filterDef = this.filterService.getFilters().find(f => f.id === filterId); + if (filterDef && filterDef.field) { + const fieldName = filterDef.field; + if (filterValue !== undefined && filterValue !== null && filterValue !== '') { + mergedFilterObj[fieldName] = filterValue; + } + } else { + // Fallback to using filterId as field name if no field is defined + if (filterValue !== undefined && filterValue !== null && filterValue !== '') { + mergedFilterObj[filterId] = filterValue; + } + } + }); + + if (Object.keys(mergedFilterObj).length > 0) { + filterParams = JSON.stringify(mergedFilterObj); + } + } + + console.log('Final filter parameters:', filterParams); // Log the URL that will be called const url = `chart/getdashjson/financial?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; @@ -496,4 +565,322 @@ export class FinancialChartComponent implements OnInit, OnChanges { public chartHovered(e: any): void { console.log(e); } + + // Initialize filter values with proper default values based on type + private initializeFilterValues(): void { + console.log('Initializing filter values'); + + // Initialize base filters + if (this.baseFilters) { + this.baseFilters.forEach(filter => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + + // Initialize drilldown filters + if (this.drilldownFilters) { + this.drilldownFilters.forEach(filter => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + + // Initialize layer filters + if (this.drilldownLayers) { + this.drilldownLayers.forEach(layer => { + if (layer.filters) { + layer.filters.forEach((filter: any) => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + }); + } + + console.log('Filter values initialized:', { + baseFilters: this.baseFilters, + drilldownFilters: this.drilldownFilters, + drilldownLayers: this.drilldownLayers + }); + } + + // Check if there are active filters + hasActiveFilters(): boolean { + return (this.baseFilters && this.baseFilters.length > 0) || + (this.drilldownFilters && this.drilldownFilters.length > 0) || + this.hasActiveLayerFilters(); + } + + // Check if there are active layer filters for current drilldown level + hasActiveLayerFilters(): boolean { + if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { + const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown + return layerIndex < this.drilldownLayers.length && + this.drilldownLayers[layerIndex].filters && + this.drilldownLayers[layerIndex].filters.length > 0; + } + return false; + } + + // Get active layer filters for current drilldown level + getActiveLayerFilters(): any[] { + if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { + const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown + if (layerIndex < this.drilldownLayers.length && + this.drilldownLayers[layerIndex].filters) { + return this.drilldownLayers[layerIndex].filters; + } + } + return []; + } + + // Get filter options for dropdown/multiselect filters + getFilterOptions(filter: any): string[] { + if (filter.options) { + if (Array.isArray(filter.options)) { + return filter.options; + } else if (typeof filter.options === 'string') { + return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt); + } + } + return []; + } + + // Check if an option is selected for multiselect filters + isOptionSelected(filter: any, option: string): boolean { + if (!filter.value) { + return false; + } + + if (Array.isArray(filter.value)) { + return filter.value.includes(option); + } + + return filter.value === option; + } + + // Handle base filter changes + onBaseFilterChange(filter: any): void { + console.log('Base filter changed:', filter); + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle drilldown filter changes + onDrilldownFilterChange(filter: any): void { + console.log('Drilldown filter changed:', filter); + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle layer filter changes + onLayerFilterChange(filter: any): void { + console.log('Layer filter changed:', filter); + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle multiselect changes + onMultiSelectChange(filter: any, option: string, event: any): void { + const checked = event.target.checked; + + // Initialize filter.value as array if it's not already + if (!Array.isArray(filter.value)) { + filter.value = []; + } + + if (checked) { + // Add option to array if not already present + if (!filter.value.includes(option)) { + filter.value.push(option); + } + } else { + // Remove option from array + filter.value = filter.value.filter((item: string) => item !== option); + } + + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle date range changes + onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void { + filter.value = dateRange; + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle toggle changes + onToggleChange(filter: any, checked: boolean): void { + filter.value = checked; + // Refresh data when filter changes + this.fetchChartData(); + } + + // Toggle multiselect dropdown visibility + toggleMultiselect(filter: any, context: string): void { + const filterId = `${context}-${filter.field}`; + if (this.isMultiselectOpen(filter, context)) { + this.openMultiselects.delete(filterId); + } else { + // Close all other multiselects first + this.openMultiselects.clear(); + this.openMultiselects.set(filterId, context); + + // Add document click handler to close dropdown when clicking outside + this.addDocumentClickHandler(); + } + } + + // Add document click handler to close dropdowns when clicking outside + private addDocumentClickHandler(): void { + if (!this.documentClickHandler) { + this.documentClickHandler = (event: MouseEvent) => { + const target = event.target as HTMLElement; + // Check if click is outside any multiselect dropdown + if (!target.closest('.multiselect-container')) { + this.openMultiselects.clear(); + this.removeDocumentClickHandler(); + } + }; + + // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it + setTimeout(() => { + document.addEventListener('click', this.documentClickHandler!); + }, 0); + } + } + + // Remove document click handler + private removeDocumentClickHandler(): void { + if (this.documentClickHandler) { + document.removeEventListener('click', this.documentClickHandler); + this.documentClickHandler = null; + } + } + + // Check if multiselect dropdown is open + isMultiselectOpen(filter: any, context: string): boolean { + const filterId = `${context}-${filter.field}`; + return this.openMultiselects.has(filterId); + } + + // Get count of selected options for a multiselect filter + getSelectedOptionsCount(filter: any): number { + if (!filter.value) { + return 0; + } + + if (Array.isArray(filter.value)) { + return filter.value.length; + } + + return 0; + } + + // Clear all filters + clearAllFilters(): void { + // Clear base filters + if (this.baseFilters) { + this.baseFilters.forEach(filter => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + + // Clear drilldown filters + if (this.drilldownFilters) { + this.drilldownFilters.forEach(filter => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + + // Clear layer filters + if (this.drilldownLayers) { + this.drilldownLayers.forEach(layer => { + if (layer.filters) { + layer.filters.forEach((filter: any) => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + }); + } + + // Close all multiselect dropdowns + this.openMultiselects.clear(); + + // Refresh data + this.fetchChartData(); + } + + ngOnDestroy(): void { + // Unsubscribe from all subscriptions to prevent memory leaks + this.subscriptions.forEach(subscription => subscription.unsubscribe()); + + // Remove document click handler if it exists + this.removeDocumentClickHandler(); + } } \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.html index 87ceda0..5fa8fc6 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.html +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.html @@ -1,16 +1,284 @@
- -
- Drilldown Level: {{currentDrilldownLevel}} - - + +
+ +
+

Base Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Drilldown Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Layer Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+ +
+
+ + +
+
+

{{ charttitle }}

+
+
+ + +
+
+ + +
+
+
+
+
+
+ +
+ + Drilldown Level: {{currentDrilldownLevel}} + + (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}}) + + +
+
+
+
-

{{ charttitle }}

diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.scss index ba07969..de8a953 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.scss +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.scss @@ -149,10 +149,192 @@ 100% { transform: rotate(360deg); } } -/* Responsive design */ +// Filter section styles +.filter-section { + margin-bottom: 20px; + padding: 15px; + border: 1px solid #ddd; + border-radius: 4px; + background-color: #f9f9f9; +} + +.filter-group { + margin-bottom: 15px; + + h4 { + margin-top: 0; + margin-bottom: 10px; + color: #333; + font-weight: 600; + } +} + +.filter-controls { + display: flex; + flex-wrap: wrap; + gap: 15px; +} + +.filter-item { + flex: 1 1 300px; + min-width: 250px; + padding: 10px; + background: white; + border: 1px solid #e0e0e0; + border-radius: 4px; +} + +.filter-label { + font-weight: 500; + margin-bottom: 8px; + color: #555; + font-size: 14px; +} + +.filter-input { + width: 100%; + + .filter-text-input, + .filter-select, + .filter-date { + width: 100%; + padding: 6px 12px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + } + + .filter-select { + height: 34px; + } +} + +.multiselect-container { + position: relative; +} + +.multiselect-display { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px; + border: 1px solid #ccc; + border-radius: 4px; + background: white; + cursor: pointer; + min-height: 34px; + + .multiselect-label { + flex: 1; + font-size: 14px; + } + + .multiselect-value { + color: #666; + font-size: 12px; + margin-right: 8px; + } + + .dropdown-icon { + flex-shrink: 0; + transition: transform 0.2s ease; + } + + &:hover { + border-color: #999; + } +} + +.multiselect-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 1000; + background: white; + border: 1px solid #ccc; + border-top: none; + border-radius: 0 0 4px 4px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + max-height: 200px; + overflow-y: auto; + + .checkbox-group { + padding: 8px; + + .checkbox-item { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + + .checkbox-label { + margin: 0; + font-size: 14px; + cursor: pointer; + } + } + } +} + +.date-range { + .date-input-group { + display: flex; + align-items: center; + gap: 8px; + } + + .date-separator { + margin: 0 5px; + color: #777; + } +} + +.toggle { + display: flex; + align-items: center; + gap: 8px; + + .toggle-label { + margin: 0; + font-size: 14px; + cursor: pointer; + } +} + +.filter-actions { + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #eee; + + .btn { + font-size: 13px; + } +} + +// New header row styling +.header-row { + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px solid #eee; + + .chart-title { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #333; + text-align: left; + padding-bottom: 0; + border-bottom: none; + } +} + +// Responsive design @media (max-width: 768px) { .pie-chart-container { padding: 15px; + height: auto; + min-height: 300px; } .chart-title { @@ -179,4 +361,18 @@ font-size: 16px; padding: 20px; } + + .filter-controls { + flex-direction: column; + } + + .filter-item { + min-width: 100%; + } + + .header-row { + .chart-title { + font-size: 16px; + } + } } \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.ts index d074ef5..4b735de 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.ts +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.ts @@ -8,7 +8,7 @@ import { Subscription } from 'rxjs'; templateUrl: './pie-chart.component.html', styleUrls: ['./pie-chart.component.scss'] }) -export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { +export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy { @Input() xAxis: string; @Input() yAxis: string | string[]; @Input() table: string; @@ -101,6 +101,11 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { // Subscriptions to unsubscribe on destroy private subscriptions: Subscription[] = []; + // Add properties for filter functionality + private openMultiselects: Map = new Map(); // Map of filterId -> context + private documentClickHandler: ((event: MouseEvent) => void) | null = null; + private filtersInitialized: boolean = false; + constructor( private dashboardService: Dashboard3Service, private filterService: FilterService @@ -133,6 +138,12 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { ngOnChanges(changes: SimpleChanges): void { console.log('PieChartComponent input changes:', changes); + // Initialize filter values if they haven't been initialized yet + if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { + this.initializeFilterValues(); + this.filtersInitialized = true; + } + // Check if any of the key properties have changed const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; @@ -158,6 +169,318 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { ngOnDestroy(): void { this.subscriptions.forEach(sub => sub.unsubscribe()); + // Clean up document click handler + this.removeDocumentClickHandler(); + } + + // Initialize filter values with proper default values based on type + private initializeFilterValues(): void { + console.log('Initializing filter values'); + + // Initialize base filters + if (this.baseFilters) { + this.baseFilters.forEach(filter => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + + // Initialize drilldown filters + if (this.drilldownFilters) { + this.drilldownFilters.forEach(filter => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + + // Initialize layer filters + if (this.drilldownLayers) { + this.drilldownLayers.forEach(layer => { + if (layer.filters) { + layer.filters.forEach((filter: any) => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + }); + } + + console.log('Filter values initialized:', { + baseFilters: this.baseFilters, + drilldownFilters: this.drilldownFilters, + drilldownLayers: this.drilldownLayers + }); + } + + // Check if there are active filters + hasActiveFilters(): boolean { + return (this.baseFilters && this.baseFilters.length > 0) || + (this.drilldownFilters && this.drilldownFilters.length > 0) || + this.hasActiveLayerFilters(); + } + + // Check if there are active layer filters for current drilldown level + hasActiveLayerFilters(): boolean { + if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { + const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown + return layerIndex < this.drilldownLayers.length && + this.drilldownLayers[layerIndex].filters && + this.drilldownLayers[layerIndex].filters.length > 0; + } + return false; + } + + // Get active layer filters for current drilldown level + getActiveLayerFilters(): any[] { + if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { + const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown + if (layerIndex < this.drilldownLayers.length && + this.drilldownLayers[layerIndex].filters) { + return this.drilldownLayers[layerIndex].filters; + } + } + return []; + } + + // Get filter options for dropdown/multiselect filters + getFilterOptions(filter: any): string[] { + if (filter.options) { + if (Array.isArray(filter.options)) { + return filter.options; + } else if (typeof filter.options === 'string') { + return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt); + } + } + return []; + } + + // Check if an option is selected for multiselect filters + isOptionSelected(filter: any, option: string): boolean { + if (!filter.value) { + return false; + } + + if (Array.isArray(filter.value)) { + return filter.value.includes(option); + } + + return filter.value === option; + } + + // Handle base filter changes + onBaseFilterChange(filter: any): void { + console.log('Base filter changed:', filter); + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle drilldown filter changes + onDrilldownFilterChange(filter: any): void { + console.log('Drilldown filter changed:', filter); + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle layer filter changes + onLayerFilterChange(filter: any): void { + console.log('Layer filter changed:', filter); + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle multiselect changes + onMultiSelectChange(filter: any, option: string, event: any): void { + const checked = event.target.checked; + + // Initialize filter.value as array if it's not already + if (!Array.isArray(filter.value)) { + filter.value = []; + } + + if (checked) { + // Add option to array if not already present + if (!filter.value.includes(option)) { + filter.value.push(option); + } + } else { + // Remove option from array + filter.value = filter.value.filter((item: string) => item !== option); + } + + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle date range changes + onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void { + filter.value = dateRange; + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle toggle changes + onToggleChange(filter: any, checked: boolean): void { + filter.value = checked; + // Refresh data when filter changes + this.fetchChartData(); + } + + // Toggle multiselect dropdown visibility + toggleMultiselect(filter: any, context: string): void { + const filterId = `${context}-${filter.field}`; + if (this.isMultiselectOpen(filter, context)) { + this.openMultiselects.delete(filterId); + } else { + // Close all other multiselects first + this.openMultiselects.clear(); + this.openMultiselects.set(filterId, context); + + // Add document click handler to close dropdown when clicking outside + this.addDocumentClickHandler(); + } + } + + // Add document click handler to close dropdowns when clicking outside + private addDocumentClickHandler(): void { + if (!this.documentClickHandler) { + this.documentClickHandler = (event: MouseEvent) => { + const target = event.target as HTMLElement; + // Check if click is outside any multiselect dropdown + if (!target.closest('.multiselect-container')) { + this.openMultiselects.clear(); + this.removeDocumentClickHandler(); + } + }; + + // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it + setTimeout(() => { + document.addEventListener('click', this.documentClickHandler!); + }, 0); + } + } + + // Remove document click handler + private removeDocumentClickHandler(): void { + if (this.documentClickHandler) { + document.removeEventListener('click', this.documentClickHandler); + this.documentClickHandler = null; + } + } + + // Check if multiselect dropdown is open + isMultiselectOpen(filter: any, context: string): boolean { + const filterId = `${context}-${filter.field}`; + return this.openMultiselects.has(filterId); + } + + // Get count of selected options for a multiselect filter + getSelectedOptionsCount(filter: any): number { + if (!filter.value) { + return 0; + } + + if (Array.isArray(filter.value)) { + return filter.value.length; + } + + return 0; + } + + // Clear all filters + clearAllFilters(): void { + // Clear base filters + if (this.baseFilters) { + this.baseFilters.forEach(filter => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + + // Clear drilldown filters + if (this.drilldownFilters) { + this.drilldownFilters.forEach(filter => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + + // Clear layer filters + if (this.drilldownLayers) { + this.drilldownLayers.forEach(layer => { + if (layer.filters) { + layer.filters.forEach((filter: any) => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + }); + } + + // Close all multiselect dropdowns + this.openMultiselects.clear(); + + // Refresh data + this.fetchChartData(); } // Public method to refresh data when filters change @@ -243,7 +566,7 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { // Log the URL that will be called const url = `chart/getdashjson/pie?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; - console.log('Pie chart data URL:', url); + console.log('Chart data URL:', url); // Fetch data from the dashboard service with parameter field and value // For base level, we pass empty parameter and value, but now also pass filters @@ -251,12 +574,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { (data: any) => { console.log('Received pie chart data:', data); if (data === null) { - console.warn('Pie chart API returned null data. Check if the API endpoint is working correctly.'); + console.warn('API returned null data. Check if the API endpoint is working correctly.'); this.noDataAvailable = true; this.pieChartLabels = []; this.pieChartData = []; - // Validate and sanitize data to show default data - this.validateChartData(); // Reset flag after fetching this.isFetchingData = false; return; @@ -264,50 +585,26 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { // Handle the actual data structure returned by the API if (data && data.chartLabels && data.chartData) { - // For pie charts, we need to extract the data differently - // The first dataset's data array contains the values for the pie chart + // Backend has already filtered the data, just display it this.noDataAvailable = data.chartLabels.length === 0; - this.pieChartLabels = data.chartLabels || []; - if (data.chartData && data.chartData.length > 0) { - this.pieChartData = data.chartData[0].data.map(value => { - // Convert to number if it's not already - const numValue = Number(value); - return isNaN(numValue) ? 0 : numValue; - }); - } else { - this.pieChartData = []; - } - // Ensure labels and data arrays have the same length - this.syncLabelAndDataArrays(); - // Validate and sanitize data - this.validateChartData(); + this.pieChartLabels = data.chartLabels; + this.pieChartData = data.chartData; // Trigger change detection this.pieChartData = [...this.pieChartData]; console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData }); - } else if (data && data.labels && data.data) { - // Handle the original expected format as fallback + } else if (data && data.labels && data.datasets) { + // Backend has already filtered the data, just display it this.noDataAvailable = data.labels.length === 0; - this.pieChartLabels = data.labels || []; - this.pieChartData = data.data.map(value => { - // Convert to number if it's not already - const numValue = Number(value); - return isNaN(numValue) ? 0 : numValue; - }); - // Ensure labels and data arrays have the same length - this.syncLabelAndDataArrays(); - // Validate and sanitize data - this.validateChartData(); + this.pieChartLabels = data.labels; + this.pieChartData = data.datasets[0]?.data || []; // Trigger change detection this.pieChartData = [...this.pieChartData]; console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData }); } else { - console.warn('Pie chart received data does not have expected structure', data); - // Reset to default data + console.warn('Received data does not have expected structure', data); this.noDataAvailable = true; this.pieChartLabels = []; this.pieChartData = []; - // Validate and sanitize data to show default data - this.validateChartData(); } // Reset flag after fetching this.isFetchingData = false; @@ -317,21 +614,16 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { this.noDataAvailable = true; this.pieChartLabels = []; this.pieChartData = []; - // Validate and sanitize data to show default data - this.validateChartData(); // Reset flag after fetching this.isFetchingData = false; + // Keep default data in case of error } ); } else { - console.log('Missing required data for pie chart, showing default data:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); - // Don't set noDataAvailable to true when there's no required data - // This allows static data to be displayed - this.noDataAvailable = false; - // Validate the chart data to ensure we have some data to display - this.validateChartData(); - // Force a redraw to ensure the chart displays - this.pieChartData = [...this.pieChartData]; + console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection }); + this.noDataAvailable = true; + this.pieChartLabels = []; + this.pieChartData = []; // Reset flag after fetching this.isFetchingData = false; } @@ -477,39 +769,18 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { // Handle the actual data structure returned by the API if (data && data.chartLabels && data.chartData) { - // For pie charts, we need to extract the data differently - // The first dataset's data array contains the values for the pie chart + // Backend has already filtered the data, just display it this.noDataAvailable = data.chartLabels.length === 0; - this.pieChartLabels = data.chartLabels || []; - if (data.chartData && data.chartData.length > 0) { - this.pieChartData = data.chartData[0].data.map(value => { - // Convert to number if it's not already - const numValue = Number(value); - return isNaN(numValue) ? 0 : numValue; - }); - } else { - this.pieChartData = []; - } - // Ensure labels and data arrays have the same length - this.syncLabelAndDataArrays(); - // Validate and sanitize data - this.validateChartData(); + this.pieChartLabels = data.chartLabels; + this.pieChartData = data.chartData; // Trigger change detection this.pieChartData = [...this.pieChartData]; console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData }); - } else if (data && data.labels && data.data) { - // Handle the original expected format as fallback + } else if (data && data.labels && data.datasets) { + // Backend has already filtered the data, just display it this.noDataAvailable = data.labels.length === 0; - this.pieChartLabels = data.labels || []; - this.pieChartData = data.data.map(value => { - // Convert to number if it's not already - const numValue = Number(value); - return isNaN(numValue) ? 0 : numValue; - }); - // Ensure labels and data arrays have the same length - this.syncLabelAndDataArrays(); - // Validate and sanitize data - this.validateChartData(); + this.pieChartLabels = data.labels; + this.pieChartData = data.datasets[0]?.data || []; // Trigger change detection this.pieChartData = [...this.pieChartData]; console.log('Updated pie chart with drilldown legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData }); @@ -518,8 +789,6 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { this.noDataAvailable = true; this.pieChartLabels = []; this.pieChartData = []; - // Validate and sanitize data - this.validateChartData(); } }, (error) => { @@ -588,84 +857,34 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { } } - /** - * Get color for legend item - * @param index Index of the legend item - */ - public getLegendColor(index: number): string { + // Validate chart data to ensure labels and data arrays have the same length + private validateChartData(): void { + if (this.pieChartLabels && this.pieChartData) { + // For pie charts, we need to ensure labels and data arrays have the same length + const labelCount = this.pieChartLabels.length; + const dataCount = this.pieChartData.length; + + 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]; } - /** - * 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 public chartClicked(e: any): void { console.log('Pie chart clicked:', e); @@ -752,6 +971,10 @@ export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked { } public chartHovered(e: any): void { - console.log(e); + console.log('Pie chart hovered:', e); + } + + ngAfterViewChecked(): void { + // This lifecycle hook can be used if needed for post-render operations } } \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/polar-chart/polar-chart.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/polar-chart/polar-chart.component.html index 421e078..729a623 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/polar-chart/polar-chart.component.html +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/polar-chart/polar-chart.component.html @@ -1,10 +1,291 @@ - -
- - -
+
+ +
+ +
+

Base Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Drilldown Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Layer Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+ +
+
+ + +
+
+

{{charttitle || 'Polar Chart'}}

+
+
+ + +
+
+ + +
+
+
+
+
+
+ +
+ + Drilldown Level: {{currentDrilldownLevel}} + + (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}}) + + +
+
+
+
+
+ +
+ + +
+
\ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/polar-chart/polar-chart.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/polar-chart/polar-chart.component.scss index df302b2..a9282e4 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/polar-chart/polar-chart.component.scss +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/polar-chart/polar-chart.component.scss @@ -1,18 +1,192 @@ -// Polar Chart Component Styles -div[style*="display: block"] { +.filter-section { + margin-bottom: 20px; + padding: 15px; + border: 1px solid #ddd; + border-radius: 4px; + background-color: #f9f9f9; +} + +.filter-group { + margin-bottom: 15px; + + h4 { + margin-top: 0; + margin-bottom: 10px; + color: #333; + font-weight: 600; + } +} + +.filter-controls { + display: flex; + flex-wrap: wrap; + gap: 15px; +} + +.filter-item { + flex: 1 1 300px; + min-width: 250px; + padding: 10px; + background: white; + border: 1px solid #e0e0e0; + border-radius: 4px; +} + +.filter-label { + font-weight: 500; + margin-bottom: 8px; + color: #555; + font-size: 14px; +} + +.filter-input { + width: 100%; + + .filter-text-input, + .filter-select, + .filter-date { + width: 100%; + padding: 6px 12px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + } + + .filter-select { + height: 34px; + } +} + +.multiselect-container { position: relative; - width: 100%; - height: 100%; } -canvas { - max-width: 100%; - max-height: 100%; +.multiselect-display { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px; + border: 1px solid #ccc; + border-radius: 4px; + background: white; + cursor: pointer; + min-height: 34px; + + .multiselect-label { + flex: 1; + font-size: 14px; + } + + .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 -:host { - display: block; - width: 100%; - height: 100%; +.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; + } + } } \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/polar-chart/polar-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/polar-chart/polar-chart.component.ts index 871b8c2..0c01a8a 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/polar-chart/polar-chart.component.ts +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/polar-chart/polar-chart.component.ts @@ -1,5 +1,7 @@ import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service'; +import { FilterService } from '../../common-filter/filter.service'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-polar-chart', @@ -33,9 +35,20 @@ export class PolarChartComponent implements OnInit, OnChanges { // Multi-layer drilldown configuration inputs @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations - constructor(private dashboardService: Dashboard3Service) { } + constructor( + private dashboardService: Dashboard3Service, + private filterService: FilterService + ) { } ngOnInit(): void { + // Subscribe to filter changes + this.subscriptions.push( + this.filterService.filterState$.subscribe(filters => { + // When filters change, refresh the chart data + this.fetchChartData(); + }) + ); + // Initialize with default data this.fetchChartData(); } @@ -43,6 +56,12 @@ export class PolarChartComponent implements OnInit, OnChanges { ngOnChanges(changes: SimpleChanges): void { console.log('PolarChartComponent input changes:', changes); + // Initialize filter values if they haven't been initialized yet + if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { + this.initializeFilterValues(); + this.filtersInitialized = true; + } + // Check if any of the key properties have changed const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; @@ -85,6 +104,324 @@ export class PolarChartComponent implements OnInit, OnChanges { // Flag to prevent infinite loops private isFetchingData: boolean = false; + // Subscriptions to unsubscribe on destroy + private subscriptions: Subscription[] = []; + + // Add properties for filter functionality + private openMultiselects: Map = new Map(); // Map of filterId -> context + private documentClickHandler: ((event: MouseEvent) => void) | null = null; + private filtersInitialized: boolean = false; + + // Initialize filter values with proper default values based on type + private initializeFilterValues(): void { + console.log('Initializing filter values'); + + // Initialize base filters + if (this.baseFilters) { + this.baseFilters.forEach(filter => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + + // Initialize drilldown filters + if (this.drilldownFilters) { + this.drilldownFilters.forEach(filter => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + + // Initialize layer filters + if (this.drilldownLayers) { + this.drilldownLayers.forEach(layer => { + if (layer.filters) { + layer.filters.forEach((filter: any) => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + }); + } + + console.log('Filter values initialized:', { + baseFilters: this.baseFilters, + drilldownFilters: this.drilldownFilters, + drilldownLayers: this.drilldownLayers + }); + } + + // Check if there are active filters + hasActiveFilters(): boolean { + return (this.baseFilters && this.baseFilters.length > 0) || + (this.drilldownFilters && this.drilldownFilters.length > 0) || + this.hasActiveLayerFilters(); + } + + // Check if there are active layer filters for current drilldown level + hasActiveLayerFilters(): boolean { + if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { + const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown + return layerIndex < this.drilldownLayers.length && + this.drilldownLayers[layerIndex].filters && + this.drilldownLayers[layerIndex].filters.length > 0; + } + return false; + } + + // Get active layer filters for current drilldown level + getActiveLayerFilters(): any[] { + if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { + const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown + if (layerIndex < this.drilldownLayers.length && + this.drilldownLayers[layerIndex].filters) { + return this.drilldownLayers[layerIndex].filters; + } + } + return []; + } + + // Get filter options for dropdown/multiselect filters + getFilterOptions(filter: any): string[] { + if (filter.options) { + if (Array.isArray(filter.options)) { + return filter.options; + } else if (typeof filter.options === 'string') { + return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt); + } + } + return []; + } + + // Check if an option is selected for multiselect filters + isOptionSelected(filter: any, option: string): boolean { + if (!filter.value) { + return false; + } + + if (Array.isArray(filter.value)) { + return filter.value.includes(option); + } + + return filter.value === option; + } + + // Handle base filter changes + onBaseFilterChange(filter: any): void { + console.log('Base filter changed:', filter); + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle drilldown filter changes + onDrilldownFilterChange(filter: any): void { + console.log('Drilldown filter changed:', filter); + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle layer filter changes + onLayerFilterChange(filter: any): void { + console.log('Layer filter changed:', filter); + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle multiselect changes + onMultiSelectChange(filter: any, option: string, event: any): void { + const checked = event.target.checked; + + // Initialize filter.value as array if it's not already + if (!Array.isArray(filter.value)) { + filter.value = []; + } + + if (checked) { + // Add option to array if not already present + if (!filter.value.includes(option)) { + filter.value.push(option); + } + } else { + // Remove option from array + filter.value = filter.value.filter((item: string) => item !== option); + } + + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle date range changes + onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void { + filter.value = dateRange; + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle toggle changes + onToggleChange(filter: any, checked: boolean): void { + filter.value = checked; + // Refresh data when filter changes + this.fetchChartData(); + } + + // Toggle multiselect dropdown visibility + toggleMultiselect(filter: any, context: string): void { + const filterId = `${context}-${filter.field}`; + if (this.isMultiselectOpen(filter, context)) { + this.openMultiselects.delete(filterId); + } else { + // Close all other multiselects first + this.openMultiselects.clear(); + this.openMultiselects.set(filterId, context); + + // Add document click handler to close dropdown when clicking outside + this.addDocumentClickHandler(); + } + } + + // Add document click handler to close dropdowns when clicking outside + private addDocumentClickHandler(): void { + if (!this.documentClickHandler) { + this.documentClickHandler = (event: MouseEvent) => { + const target = event.target as HTMLElement; + // Check if click is outside any multiselect dropdown + if (!target.closest('.multiselect-container')) { + this.openMultiselects.clear(); + this.removeDocumentClickHandler(); + } + }; + + // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it + setTimeout(() => { + document.addEventListener('click', this.documentClickHandler!); + }, 0); + } + } + + // Remove document click handler + private removeDocumentClickHandler(): void { + if (this.documentClickHandler) { + document.removeEventListener('click', this.documentClickHandler); + this.documentClickHandler = null; + } + } + + // Check if multiselect dropdown is open + isMultiselectOpen(filter: any, context: string): boolean { + const filterId = `${context}-${filter.field}`; + return this.openMultiselects.has(filterId); + } + + // Get count of selected options for a multiselect filter + getSelectedOptionsCount(filter: any): number { + if (!filter.value) { + return 0; + } + + if (Array.isArray(filter.value)) { + return filter.value.length; + } + + return 0; + } + + // Clear all filters + clearAllFilters(): void { + // Clear base filters + if (this.baseFilters) { + this.baseFilters.forEach(filter => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + + // Clear drilldown filters + if (this.drilldownFilters) { + this.drilldownFilters.forEach(filter => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + + // Clear layer filters + if (this.drilldownLayers) { + this.drilldownLayers.forEach(layer => { + if (layer.filters) { + layer.filters.forEach((filter: any) => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + }); + } + + // Close all multiselect dropdowns + this.openMultiselects.clear(); + + // Refresh data + this.fetchChartData(); + } + fetchChartData(): void { // Set flag to prevent recursive calls this.isFetchingData = true; @@ -117,7 +454,49 @@ export class PolarChartComponent implements OnInit, OnChanges { filterParams = JSON.stringify(filterObj); } } - console.log('Base filter parameters:', filterParams); + + // Add common filters to filter parameters + const commonFilters = this.filterService.getFilterValues(); + console.log('Common filters from service:', commonFilters); + + if (Object.keys(commonFilters).length > 0) { + // Merge common filters with base filters + const mergedFilterObj = {}; + + // Add base filters first + if (filterParams) { + try { + const baseFilterObj = JSON.parse(filterParams); + Object.assign(mergedFilterObj, baseFilterObj); + } catch (e) { + console.warn('Failed to parse base filter parameters:', e); + } + } + + // Add common filters using the field name as the key, not the filter id + Object.keys(commonFilters).forEach(filterId => { + const filterValue = commonFilters[filterId]; + // Find the filter definition to get the field name + const filterDef = this.filterService.getFilters().find(f => f.id === filterId); + if (filterDef && filterDef.field) { + const fieldName = filterDef.field; + if (filterValue !== undefined && filterValue !== null && filterValue !== '') { + mergedFilterObj[fieldName] = filterValue; + } + } else { + // Fallback to using filterId as field name if no field is defined + if (filterValue !== undefined && filterValue !== null && filterValue !== '') { + mergedFilterObj[filterId] = filterValue; + } + } + }); + + if (Object.keys(mergedFilterObj).length > 0) { + filterParams = JSON.stringify(mergedFilterObj); + } + } + + console.log('Final filter parameters:', filterParams); // Log the URL that will be called const url = `chart/getdashjson/polar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; @@ -287,6 +666,35 @@ export class PolarChartComponent implements OnInit, OnChanges { } } + // Add common filters to drilldown filter parameters + const commonFilters = this.filterService.getFilterValues(); + if (Object.keys(commonFilters).length > 0) { + // Merge common filters with drilldown filters + const mergedFilterObj = {}; + + // Add drilldown filters first + if (filterParams) { + try { + const drilldownFilterObj = JSON.parse(filterParams); + Object.assign(mergedFilterObj, drilldownFilterObj); + } catch (e) { + console.warn('Failed to parse drilldown filter parameters:', e); + } + } + + // Add common filters + Object.keys(commonFilters).forEach(key => { + const value = commonFilters[key]; + if (value !== undefined && value !== null && value !== '') { + mergedFilterObj[key] = value; + } + }); + + if (Object.keys(mergedFilterObj).length > 0) { + filterParams = JSON.stringify(mergedFilterObj); + } + } + // Log the URL that will be called const url = `chart/getdashjson/polar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; console.log('Drilldown data URL:', url); @@ -307,7 +715,6 @@ export class PolarChartComponent implements OnInit, OnChanges { // Handle the actual data structure returned by the API if (data && data.chartLabels && data.chartData) { // For polar charts, we need to extract the data differently - // The first dataset's data array contains the values for the polar chart this.noDataAvailable = data.chartLabels.length === 0; this.polarAreaChartLabels = data.chartLabels; if (data.chartData && data.chartData.length > 0) { @@ -417,13 +824,13 @@ export class PolarChartComponent implements OnInit, OnChanges { // Get the label of the clicked element const clickedLabel = this.polarAreaChartLabels[clickedIndex]; - console.log('Clicked on polar area:', { index: clickedIndex, label: clickedLabel }); + console.log('Clicked on polar point:', { index: clickedIndex, label: clickedLabel }); // If we're not at the base level, store original data if (this.currentDrilldownLevel === 0) { // Store original data before entering drilldown mode this.originalPolarAreaChartLabels = [...this.polarAreaChartLabels]; - this.originalPolarAreaChartData = [...this.polarAreaChartData]; + this.originalPolarAreaChartData = JSON.parse(JSON.stringify(this.polarAreaChartData)); console.log('Stored original data for drilldown'); } @@ -491,6 +898,12 @@ export class PolarChartComponent implements OnInit, OnChanges { } public chartHovered(e: any): void { - console.log(e); + console.log('Polar chart hovered:', e); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + // Clean up document click handler + this.removeDocumentClickHandler(); } } \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/radar-chart/radar-chart.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/radar-chart/radar-chart.component.html index 50f80cd..fca1ae0 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/radar-chart/radar-chart.component.html +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/radar-chart/radar-chart.component.html @@ -1,13 +1,282 @@ -
- -
- Drilldown Level: {{currentDrilldownLevel}} - - +
+ +
+ +
+

Base Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Drilldown Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Layer Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+ +
+
+ + +
+
+

{{charttitle || 'Radar Chart'}}

+
+
+ + +
+
+ + +
+
+
+
+
+
+ +
+ + Drilldown Level: {{currentDrilldownLevel}} + + (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}}) + + +
+
+
+
@@ -16,7 +285,7 @@
-
+
= new Map(); // Map of filterId -> context + private documentClickHandler: ((event: MouseEvent) => void) | null = null; + private filtersInitialized: boolean = false; + + constructor( + private dashboardService: Dashboard3Service, + private filterService: FilterService + ) { } ngOnInit(): void { + // Subscribe to filter changes + this.subscriptions.push( + this.filterService.filterState$.subscribe(filters => { + // When filters change, refresh the chart data + this.fetchChartData(); + }) + ); + this.fetchChartData(); } ngOnChanges(changes: SimpleChanges): void { console.log('RadarChartComponent input changes:', changes); + // Initialize filter values if they haven't been initialized yet + if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { + this.initializeFilterValues(); + this.filtersInitialized = true; + } + // Check if any of the key properties have changed const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; @@ -93,7 +120,317 @@ export class RadarChartComponent implements OnInit, OnChanges { 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 { // Set flag to prevent recursive calls this.isFetchingData = true; @@ -126,7 +463,49 @@ export class RadarChartComponent implements OnInit, OnChanges { filterParams = JSON.stringify(filterObj); } } - console.log('Base filter parameters:', filterParams); + + // Add common filters to filter parameters + const commonFilters = this.filterService.getFilterValues(); + console.log('Common filters from service:', commonFilters); + + if (Object.keys(commonFilters).length > 0) { + // Merge common filters with base filters + const mergedFilterObj = {}; + + // Add base filters first + if (filterParams) { + try { + const baseFilterObj = JSON.parse(filterParams); + Object.assign(mergedFilterObj, baseFilterObj); + } catch (e) { + console.warn('Failed to parse base filter parameters:', e); + } + } + + // Add common filters using the field name as the key, not the filter id + Object.keys(commonFilters).forEach(filterId => { + const filterValue = commonFilters[filterId]; + // Find the filter definition to get the field name + const filterDef = this.filterService.getFilters().find(f => f.id === filterId); + if (filterDef && filterDef.field) { + const fieldName = filterDef.field; + if (filterValue !== undefined && filterValue !== null && filterValue !== '') { + mergedFilterObj[fieldName] = filterValue; + } + } else { + // Fallback to using filterId as field name if no field is defined + if (filterValue !== undefined && filterValue !== null && filterValue !== '') { + mergedFilterObj[filterId] = filterValue; + } + } + }); + + if (Object.keys(mergedFilterObj).length > 0) { + filterParams = JSON.stringify(mergedFilterObj); + } + } + + console.log('Final filter parameters:', filterParams); // Log the URL that will be called const url = `chart/getdashjson/radar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; @@ -298,6 +677,35 @@ export class RadarChartComponent implements OnInit, OnChanges { } } + // Add common filters to drilldown filter parameters + const commonFilters = this.filterService.getFilterValues(); + if (Object.keys(commonFilters).length > 0) { + // Merge common filters with drilldown filters + const mergedFilterObj = {}; + + // Add drilldown filters first + if (filterParams) { + try { + const drilldownFilterObj = JSON.parse(filterParams); + Object.assign(mergedFilterObj, drilldownFilterObj); + } catch (e) { + console.warn('Failed to parse drilldown filter parameters:', e); + } + } + + // Add common filters + Object.keys(commonFilters).forEach(key => { + const value = commonFilters[key]; + if (value !== undefined && value !== null && value !== '') { + mergedFilterObj[key] = value; + } + }); + + if (Object.keys(mergedFilterObj).length > 0) { + filterParams = JSON.stringify(mergedFilterObj); + } + } + // Log the URL that will be called const url = `chart/getdashjson/radar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; console.log('Drilldown data URL:', url); @@ -321,7 +729,6 @@ export class RadarChartComponent implements OnInit, OnChanges { this.noDataAvailable = data.chartLabels.length === 0; this.radarChartLabels = data.chartLabels; // For radar charts, we need to ensure the data is properly formatted - // Each dataset should have a data array with numeric values this.radarChartData = data.chartData.map(dataset => ({ ...dataset, data: dataset.data ? dataset.data.map(value => { @@ -358,6 +765,7 @@ export class RadarChartComponent implements OnInit, OnChanges { this.noDataAvailable = true; this.radarChartLabels = []; this.radarChartData = []; + // Keep current data in case of error } ); } @@ -436,7 +844,7 @@ export class RadarChartComponent implements OnInit, OnChanges { if (this.currentDrilldownLevel === 0) { // Store original data before entering drilldown mode this.originalRadarChartLabels = [...this.radarChartLabels]; - this.originalRadarChartData = [...this.radarChartData]; + this.originalRadarChartData = JSON.parse(JSON.stringify(this.radarChartData)); console.log('Stored original data for drilldown'); } @@ -504,6 +912,12 @@ export class RadarChartComponent implements OnInit, OnChanges { } public chartHovered(e: any): void { - console.log(e); + console.log('Radar chart hovered:', e); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + // Clean up document click handler + this.removeDocumentClickHandler(); } } \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/scatter-chart/scatter-chart.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/scatter-chart/scatter-chart.component.html index 03d532b..4b606a3 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/scatter-chart/scatter-chart.component.html +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/scatter-chart/scatter-chart.component.html @@ -1,13 +1,282 @@ -
- -
- Drilldown Level: {{currentDrilldownLevel}} - - +
+ +
+ +
+

Base Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Drilldown Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Layer Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+ +
+
+ + +
+
+

{{charttitle || 'Scatter Chart'}}

+
+
+ + +
+
+ + +
+
+
+
+
+
+ +
+ + Drilldown Level: {{currentDrilldownLevel}} + + (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}}) + + +
+
+
+
@@ -16,7 +285,7 @@
-
+
{ + // When filters change, refresh the chart data + this.fetchChartData(); + }) + ); + // Initialize with default data this.fetchChartData(); } @@ -44,6 +57,12 @@ export class ScatterChartComponent implements OnInit, OnChanges { ngOnChanges(changes: SimpleChanges): void { console.log('ScatterChartComponent input changes:', changes); + // Initialize filter values if they haven't been initialized yet + if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { + this.initializeFilterValues(); + this.filtersInitialized = true; + } + // Check if any of the key properties have changed const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; @@ -107,6 +126,367 @@ export class ScatterChartComponent implements OnInit, OnChanges { // Flag to prevent infinite loops private isFetchingData: boolean = false; + // Subscriptions to unsubscribe on destroy + private subscriptions: Subscription[] = []; + + // Add properties for filter functionality + private openMultiselects: Map = new Map(); // Map of filterId -> context + private documentClickHandler: ((event: MouseEvent) => void) | null = null; + private filtersInitialized: boolean = false; + + // Initialize filter values with proper default values based on type + private initializeFilterValues(): void { + console.log('Initializing filter values'); + + // Initialize base filters + if (this.baseFilters) { + this.baseFilters.forEach(filter => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + + // Initialize drilldown filters + if (this.drilldownFilters) { + this.drilldownFilters.forEach(filter => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + + // Initialize layer filters + if (this.drilldownLayers) { + this.drilldownLayers.forEach(layer => { + if (layer.filters) { + layer.filters.forEach((filter: any) => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + }); + } + + console.log('Filter values initialized:', { + baseFilters: this.baseFilters, + drilldownFilters: this.drilldownFilters, + drilldownLayers: this.drilldownLayers + }); + } + + // Check if there are active filters + hasActiveFilters(): boolean { + return (this.baseFilters && this.baseFilters.length > 0) || + (this.drilldownFilters && this.drilldownFilters.length > 0) || + this.hasActiveLayerFilters(); + } + + // Check if there are active layer filters for current drilldown level + hasActiveLayerFilters(): boolean { + if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { + const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown + return layerIndex < this.drilldownLayers.length && + this.drilldownLayers[layerIndex].filters && + this.drilldownLayers[layerIndex].filters.length > 0; + } + return false; + } + + // Get active layer filters for current drilldown level + getActiveLayerFilters(): any[] { + if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { + const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown + if (layerIndex < this.drilldownLayers.length && + this.drilldownLayers[layerIndex].filters) { + return this.drilldownLayers[layerIndex].filters; + } + } + return []; + } + + // Get filter options for dropdown/multiselect filters + getFilterOptions(filter: any): string[] { + if (filter.options) { + if (Array.isArray(filter.options)) { + return filter.options; + } else if (typeof filter.options === 'string') { + return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt); + } + } + return []; + } + + // Check if an option is selected for multiselect filters + isOptionSelected(filter: any, option: string): boolean { + if (!filter.value) { + return false; + } + + if (Array.isArray(filter.value)) { + return filter.value.includes(option); + } + + return filter.value === option; + } + + // Handle base filter changes + onBaseFilterChange(filter: any): void { + console.log('Base filter changed:', filter); + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle drilldown filter changes + onDrilldownFilterChange(filter: any): void { + console.log('Drilldown filter changed:', filter); + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle layer filter changes + onLayerFilterChange(filter: any): void { + console.log('Layer filter changed:', filter); + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle multiselect changes + onMultiSelectChange(filter: any, option: string, event: any): void { + const checked = event.target.checked; + + // Initialize filter.value as array if it's not already + if (!Array.isArray(filter.value)) { + filter.value = []; + } + + if (checked) { + // Add option to array if not already present + if (!filter.value.includes(option)) { + filter.value.push(option); + } + } else { + // Remove option from array + filter.value = filter.value.filter((item: string) => item !== option); + } + + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle date range changes + onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void { + filter.value = dateRange; + // Refresh data when filter changes + this.fetchChartData(); + } + + // Handle toggle changes + onToggleChange(filter: any, checked: boolean): void { + filter.value = checked; + // Refresh data when filter changes + this.fetchChartData(); + } + + // Toggle multiselect dropdown visibility + toggleMultiselect(filter: any, context: string): void { + const filterId = `${context}-${filter.field}`; + if (this.isMultiselectOpen(filter, context)) { + this.openMultiselects.delete(filterId); + } else { + // Close all other multiselects first + this.openMultiselects.clear(); + this.openMultiselects.set(filterId, context); + + // Add document click handler to close dropdown when clicking outside + this.addDocumentClickHandler(); + } + } + + // Add document click handler to close dropdowns when clicking outside + private addDocumentClickHandler(): void { + if (!this.documentClickHandler) { + this.documentClickHandler = (event: MouseEvent) => { + const target = event.target as HTMLElement; + // Check if click is outside any multiselect dropdown + if (!target.closest('.multiselect-container')) { + this.openMultiselects.clear(); + this.removeDocumentClickHandler(); + } + }; + + // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it + setTimeout(() => { + document.addEventListener('click', this.documentClickHandler!); + }, 0); + } + } + + // Remove document click handler + private removeDocumentClickHandler(): void { + if (this.documentClickHandler) { + document.removeEventListener('click', this.documentClickHandler); + this.documentClickHandler = null; + } + } + + // Check if multiselect dropdown is open + isMultiselectOpen(filter: any, context: string): boolean { + const filterId = `${context}-${filter.field}`; + return this.openMultiselects.has(filterId); + } + + // Get count of selected options for a multiselect filter + getSelectedOptionsCount(filter: any): number { + if (!filter.value) { + return 0; + } + + if (Array.isArray(filter.value)) { + return filter.value.length; + } + + return 0; + } + + // Clear all filters + clearAllFilters(): void { + // Clear base filters + if (this.baseFilters) { + this.baseFilters.forEach(filter => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + + // Clear drilldown filters + if (this.drilldownFilters) { + this.drilldownFilters.forEach(filter => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + + // Clear layer filters + if (this.drilldownLayers) { + this.drilldownLayers.forEach(layer => { + if (layer.filters) { + layer.filters.forEach((filter: any) => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + }); + } + + // Close all multiselect dropdowns + this.openMultiselects.clear(); + + // Refresh data + this.fetchChartData(); + } + + // Transform data to scatter chart format + private transformToScatterData(labels: any[], data: any[]): ChartDataset[] { + // For scatter charts, we need to transform the data into scatter format + // Scatter charts expect data in the format: {x: number, y: number} + console.log('Transforming data to scatter format:', { labels, data }); + + // If we have the expected scatter data format, return it as is + if (data && data.length > 0 && data[0].data && data[0].data.length > 0 && + typeof data[0].data[0] === 'object' && data[0].data[0].hasOwnProperty('x') && + data[0].data[0].hasOwnProperty('y')) { + return data; + } + + // Otherwise, create a default scatter dataset + const scatterDatasets: ChartDataset[] = [ + { + data: [ + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: -2 }, + { x: 4, y: 4 }, + { x: 5, y: -3 }, + ], + label: 'Dataset 1', + pointRadius: 10, + backgroundColor: [ + 'red', + 'green', + 'blue', + 'purple', + 'yellow', + 'brown', + 'magenta', + 'cyan', + 'orange', + 'pink' + ], + } + ]; + + return scatterDatasets; + } + fetchChartData(): void { // Set flag to prevent recursive calls this.isFetchingData = true; @@ -139,7 +519,49 @@ export class ScatterChartComponent implements OnInit, OnChanges { filterParams = JSON.stringify(filterObj); } } - console.log('Base filter parameters:', filterParams); + + // Add common filters to filter parameters + const commonFilters = this.filterService.getFilterValues(); + console.log('Common filters from service:', commonFilters); + + if (Object.keys(commonFilters).length > 0) { + // Merge common filters with base filters + const mergedFilterObj = {}; + + // Add base filters first + if (filterParams) { + try { + const baseFilterObj = JSON.parse(filterParams); + Object.assign(mergedFilterObj, baseFilterObj); + } catch (e) { + console.warn('Failed to parse base filter parameters:', e); + } + } + + // Add common filters using the field name as the key, not the filter id + Object.keys(commonFilters).forEach(filterId => { + const filterValue = commonFilters[filterId]; + // Find the filter definition to get the field name + const filterDef = this.filterService.getFilters().find(f => f.id === filterId); + if (filterDef && filterDef.field) { + const fieldName = filterDef.field; + if (filterValue !== undefined && filterValue !== null && filterValue !== '') { + mergedFilterObj[fieldName] = filterValue; + } + } else { + // Fallback to using filterId as field name if no field is defined + if (filterValue !== undefined && filterValue !== null && filterValue !== '') { + mergedFilterObj[filterId] = filterValue; + } + } + }); + + if (Object.keys(mergedFilterObj).length > 0) { + filterParams = JSON.stringify(mergedFilterObj); + } + } + + console.log('Final filter parameters:', filterParams); // Log the URL that will be called const url = `chart/getdashjson/scatter?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`; @@ -287,24 +709,34 @@ export class ScatterChartComponent implements OnInit, OnChanges { } } - // Convert drilldownFilters to filter parameters for drilldown level - let drilldownFilterParams = ''; - if (this.drilldownFilters && this.drilldownFilters.length > 0) { - const filterObj = {}; - this.drilldownFilters.forEach(filter => { - if (filter.field && filter.value) { - filterObj[filter.field] = filter.value; + // Add common filters to drilldown filter parameters + const commonFilters = this.filterService.getFilterValues(); + if (Object.keys(commonFilters).length > 0) { + // Merge common filters with drilldown filters + const mergedFilterObj = {}; + + // Add drilldown filters first + if (filterParams) { + try { + const drilldownFilterObj = JSON.parse(filterParams); + Object.assign(mergedFilterObj, drilldownFilterObj); + } catch (e) { + console.warn('Failed to parse drilldown filter parameters:', e); + } + } + + // Add common filters + Object.keys(commonFilters).forEach(key => { + const value = commonFilters[key]; + if (value !== undefined && value !== null && value !== '') { + mergedFilterObj[key] = value; } }); - if (Object.keys(filterObj).length > 0) { - drilldownFilterParams = JSON.stringify(filterObj); + + 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 const url = `chart/getdashjson/scatter?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`; @@ -312,7 +744,7 @@ export class ScatterChartComponent implements OnInit, OnChanges { // Fetch data from the dashboard service with parameter field and value // Backend handles filtering, we just pass the parameter field and value - this.dashboardService.getChartData(actualApiUrl, 'scatter', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, finalFilterParams).subscribe( + this.dashboardService.getChartData(actualApiUrl, 'scatter', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, filterParams).subscribe( (data: any) => { console.log('Received drilldown data:', data); if (data === null) { @@ -325,7 +757,6 @@ export class ScatterChartComponent implements OnInit, OnChanges { // Handle the actual data structure returned by the API if (data && data.chartLabels && data.chartData) { // For scatter charts, we need to transform the data into scatter format - // Scatter charts expect data in the format: {x: number, y: number} this.noDataAvailable = data.chartLabels.length === 0; this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData); console.log('Updated scatter chart with drilldown data:', this.scatterChartData); @@ -349,33 +780,6 @@ export class ScatterChartComponent implements OnInit, OnChanges { ); } - // Transform chart data to scatter chart format - private transformToScatterData(labels: string[], datasets: any[]): ChartDataset[] { - // For scatter charts, we need to transform the data into scatter format - // Scatter charts expect data in the format: {x: number, y: number} - - // This is a simple transformation - in a real implementation, you might want to - // create a more sophisticated mapping based on your data structure - return datasets.map((dataset, index) => { - // Create scatter data points - const scatterData = labels.map((label, i) => { - // Use x-axis data as x coordinate, y-axis data as y coordinate - const xValue = dataset.data[i] || 0; - const yValue = i < datasets.length ? (datasets[i].data[index] || 0) : 0; - - return { x: xValue, y: yValue }; - }); - - return { - data: scatterData, - label: dataset.label || `Dataset ${index + 1}`, - backgroundColor: dataset.backgroundColor || `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.6)`, - borderColor: dataset.borderColor || 'rgba(0, 0, 0, 1)', - pointRadius: dataset.pointRadius || 5 - }; - }); - } - // Reset to original data (go back to base level) resetToOriginalData(): void { console.log('Resetting to original data'); @@ -436,16 +840,18 @@ export class ScatterChartComponent implements OnInit, OnChanges { // Get the index of the clicked element const clickedIndex = e.active[0].index; - // Get the label of the clicked element - // For scatter charts, we might not have labels in the same way as other charts - const clickedLabel = `Point ${clickedIndex}`; + // Get the dataset index + const datasetIndex = e.active[0].datasetIndex; - console.log('Clicked on scatter point:', { index: clickedIndex, label: clickedLabel }); + // Get the data point + const dataPoint = this.scatterChartData[datasetIndex].data[clickedIndex]; + + console.log('Clicked on scatter point:', { datasetIndex, clickedIndex, dataPoint }); // If we're not at the base level, store original data if (this.currentDrilldownLevel === 0) { // Store original data before entering drilldown mode - this.originalScatterChartData = [...this.scatterChartData]; + this.originalScatterChartData = JSON.parse(JSON.stringify(this.scatterChartData)); console.log('Stored original data for drilldown'); } @@ -487,9 +893,10 @@ export class ScatterChartComponent implements OnInit, OnChanges { // Add this click to the drilldown stack const stackEntry = { level: nextDrilldownLevel, + datasetIndex: datasetIndex, clickedIndex: clickedIndex, - clickedLabel: clickedLabel, - clickedValue: clickedLabel // Using label as value for now + dataPoint: dataPoint, + clickedValue: dataPoint // Using data point as value for now }; this.drilldownStack.push(stackEntry); @@ -513,6 +920,12 @@ export class ScatterChartComponent implements OnInit, OnChanges { } public chartHovered(e: any): void { - console.log(e); + console.log('Scatter chart hovered:', e); } -} + + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe()); + // Clean up document click handler + this.removeDocumentClickHandler(); + } +} \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component.html index c9e47d0..79b963b 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component.html +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component.html @@ -1,27 +1,310 @@ - - - - - - - - - - - - - - - - -
#Item
{{i + 1}}{{todo}} - - - -
- - - - - -
+
+ +
+ +
+

Base Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Drilldown Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+

Layer Filters

+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+ + +
+ +
+ + +
+ +
+ + +
+
+ {{ filter.field }} + + ({{ getSelectedOptionsCount(filter) }} selected) + + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + to + +
+
+ + +
+ + +
+
+
+
+ + +
+ +
+
+ + +
+
+

{{charttitle || 'To Do Chart'}}

+
+
+ + +
+
+ + +
+
+
+
+
+
+ +
+ + Drilldown Level: {{currentDrilldownLevel}} + + (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}}) + + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + +
#Item
{{i + 1}}{{todo}} + + + +
+ + + + + +
+
\ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component.scss index e69de29..4ae7af5 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component.scss +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component.scss @@ -0,0 +1,249 @@ +.filter-section { + margin-bottom: 20px; + padding: 15px; + border: 1px solid #ddd; + border-radius: 4px; + background-color: #f9f9f9; +} + +.filter-group { + margin-bottom: 15px; + + h4 { + margin-top: 0; + margin-bottom: 10px; + color: #333; + font-weight: 600; + } +} + +.filter-controls { + display: flex; + flex-wrap: wrap; + gap: 15px; +} + +.filter-item { + flex: 1 1 300px; + min-width: 250px; + padding: 10px; + background: white; + border: 1px solid #e0e0e0; + border-radius: 4px; +} + +.filter-label { + font-weight: 500; + margin-bottom: 8px; + color: #555; + font-size: 14px; +} + +.filter-input { + width: 100%; + + .filter-text-input, + .filter-select, + .filter-date { + width: 100%; + padding: 6px 12px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + } + + .filter-select { + height: 34px; + } +} + +.multiselect-container { + position: relative; +} + +.multiselect-display { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px; + border: 1px solid #ccc; + border-radius: 4px; + background: white; + cursor: pointer; + min-height: 34px; + + .multiselect-label { + flex: 1; + font-size: 14px; + } + + .multiselect-value { + color: #666; + font-size: 12px; + margin-right: 8px; + } + + .dropdown-icon { + flex-shrink: 0; + transition: transform 0.2s ease; + } + + &:hover { + border-color: #999; + } +} + +.multiselect-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 1000; + background: white; + border: 1px solid #ccc; + border-top: none; + border-radius: 0 0 4px 4px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + max-height: 200px; + overflow-y: auto; + + .checkbox-group { + padding: 8px; + + .checkbox-item { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + + .checkbox-label { + margin: 0; + font-size: 14px; + cursor: pointer; + } + } + } +} + +.date-range { + .date-input-group { + display: flex; + align-items: center; + gap: 8px; + } + + .date-separator { + margin: 0 5px; + color: #777; + } +} + +.toggle { + display: flex; + align-items: center; + gap: 8px; + + .toggle-label { + margin: 0; + font-size: 14px; + cursor: pointer; + } +} + +.filter-actions { + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #eee; + + .btn { + font-size: 13px; + } +} + +// New header row styling +.header-row { + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px solid #eee; + + .chart-title { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #333; + } +} + +// Responsive design +@media (max-width: 768px) { + .filter-controls { + flex-direction: column; + } + + .filter-item { + min-width: 100%; + } + + .header-row { + .chart-title { + font-size: 16px; + } + } +} + +// To Do Chart specific styles +.to-do-chart-container { + padding: 20px; +} + +.todo-table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; + + th, td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #ddd; + } + + th { + background-color: #f2f2f2; + font-weight: bold; + } + + tr:hover { + background-color: #f5f5f5; + } + + .c-col { + width: 50px; + } + + .todo-input { + width: 100%; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + } + + .add-button, .remove-button { + background: none; + border: none; + cursor: pointer; + padding: 5px; + border-radius: 3px; + + &:hover { + background-color: #e0e0e0; + } + } + + .add-button { + color: #28a745; + } + + .remove-button { + color: #dc3545; + } +} \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component.ts index ae84296..bb3ac86 100644 --- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component.ts +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/to-do-chart/to-do-chart.component.ts @@ -1,4 +1,6 @@ import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { FilterService } from '../../common-filter/filter.service'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-to-do-chart', @@ -21,15 +23,43 @@ export class ToDoChartComponent implements OnInit, OnChanges { @Input() datasource: string; @Input() fieldName: string; @Input() connection: number; // Add connection input + // Drilldown configuration inputs + @Input() drilldownEnabled: boolean = false; + @Input() drilldownApiUrl: string; + @Input() drilldownXAxis: string; + @Input() drilldownYAxis: string; + @Input() drilldownParameter: string; // Add drilldown parameter input + @Input() baseFilters: any[] = []; // Add base filters input + @Input() drilldownFilters: any[] = []; // Add drilldown filters input + // Multi-layer drilldown configuration inputs + @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations + + // 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 filterService: FilterService) { } + ngOnInit(): void { + // Subscribe to filter changes + this.subscriptions.push( + this.filterService.filterState$.subscribe(filters => { + // When filters change, refresh the chart data + // For To Do chart, this would trigger a refresh of the todo list + }) + ); } ngOnChanges(changes: SimpleChanges): void { console.log('ToDoChartComponent input changes:', changes); + // Initialize filter values if they haven't been initialized yet + if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) { + this.initializeFilterValues(); + this.filtersInitialized = true; + } + // Check if any of the key properties have changed const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; @@ -48,6 +78,14 @@ export class ToDoChartComponent implements OnInit, OnChanges { todo: string; todoList = ['todo 1']; + // Add properties for filter functionality + private openMultiselects: Map = new Map(); // Map of filterId -> context + private documentClickHandler: ((event: MouseEvent) => void) | null = null; + private filtersInitialized: boolean = false; + + // Subscriptions to unsubscribe on destroy + private subscriptions: Subscription[] = []; + fetchToDoData(): void { // If we have the necessary data, fetch to-do data from the service if (this.table) { @@ -73,4 +111,322 @@ export class ToDoChartComponent implements OnInit, OnChanges { this.todoList.splice(todoIx, 1); } } + + // Initialize filter values with proper default values based on type + private initializeFilterValues(): void { + console.log('Initializing filter values'); + + // Initialize base filters + if (this.baseFilters) { + this.baseFilters.forEach(filter => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + + // Initialize drilldown filters + if (this.drilldownFilters) { + this.drilldownFilters.forEach(filter => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + + // Initialize layer filters + if (this.drilldownLayers) { + this.drilldownLayers.forEach(layer => { + if (layer.filters) { + layer.filters.forEach((filter: any) => { + if (filter.value === undefined || filter.value === null) { + switch (filter.type) { + case 'multiselect': + filter.value = []; + break; + case 'date-range': + filter.value = { start: null, end: null }; + break; + case 'toggle': + filter.value = false; + break; + default: + filter.value = ''; + } + } + }); + } + }); + } + + console.log('Filter values initialized:', { + baseFilters: this.baseFilters, + drilldownFilters: this.drilldownFilters, + drilldownLayers: this.drilldownLayers + }); + } + + // Check if there are active filters + hasActiveFilters(): boolean { + return (this.baseFilters && this.baseFilters.length > 0) || + (this.drilldownFilters && this.drilldownFilters.length > 0) || + this.hasActiveLayerFilters(); + } + + // Check if there are active layer filters for current drilldown level + hasActiveLayerFilters(): boolean { + if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { + const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown + return layerIndex < this.drilldownLayers.length && + this.drilldownLayers[layerIndex].filters && + this.drilldownLayers[layerIndex].filters.length > 0; + } + return false; + } + + // Get active layer filters for current drilldown level + getActiveLayerFilters(): any[] { + if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) { + const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown + if (layerIndex < this.drilldownLayers.length && + this.drilldownLayers[layerIndex].filters) { + return this.drilldownLayers[layerIndex].filters; + } + } + return []; + } + + // Get filter options for dropdown/multiselect filters + getFilterOptions(filter: any): string[] { + if (filter.options) { + if (Array.isArray(filter.options)) { + return filter.options; + } else if (typeof filter.options === 'string') { + return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt); + } + } + return []; + } + + // Check if an option is selected for multiselect filters + isOptionSelected(filter: any, option: string): boolean { + if (!filter.value) { + return false; + } + + if (Array.isArray(filter.value)) { + return filter.value.includes(option); + } + + return filter.value === option; + } + + // Handle base filter changes + onBaseFilterChange(filter: any): void { + console.log('Base filter changed:', filter); + // Refresh data when filter changes + // For To Do chart, this would trigger a refresh of the todo list + } + + // Handle drilldown filter changes + onDrilldownFilterChange(filter: any): void { + console.log('Drilldown filter changed:', filter); + // Refresh data when filter changes + // For To Do chart, this would trigger a refresh of the todo list + } + + // Handle layer filter changes + onLayerFilterChange(filter: any): void { + console.log('Layer filter changed:', filter); + // Refresh data when filter changes + // For To Do chart, this would trigger a refresh of the todo list + } + + // Handle multiselect changes + onMultiSelectChange(filter: any, option: string, event: any): void { + const checked = event.target.checked; + + // Initialize filter.value as array if it's not already + if (!Array.isArray(filter.value)) { + filter.value = []; + } + + if (checked) { + // Add option to array if not already present + if (!filter.value.includes(option)) { + filter.value.push(option); + } + } else { + // Remove option from array + filter.value = filter.value.filter((item: string) => item !== option); + } + + // Refresh data when filter changes + // For To Do chart, this would trigger a refresh of the todo list + } + + // Handle date range changes + onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void { + filter.value = dateRange; + // Refresh data when filter changes + // For To Do chart, this would trigger a refresh of the todo list + } + + // Handle toggle changes + onToggleChange(filter: any, checked: boolean): void { + filter.value = checked; + // Refresh data when filter changes + // For To Do chart, this would trigger a refresh of the todo list + } + + // Toggle multiselect dropdown visibility + toggleMultiselect(filter: any, context: string): void { + const filterId = `${context}-${filter.field}`; + if (this.isMultiselectOpen(filter, context)) { + this.openMultiselects.delete(filterId); + } else { + // Close all other multiselects first + this.openMultiselects.clear(); + this.openMultiselects.set(filterId, context); + + // Add document click handler to close dropdown when clicking outside + this.addDocumentClickHandler(); + } + } + + // Add document click handler to close dropdowns when clicking outside + private addDocumentClickHandler(): void { + if (!this.documentClickHandler) { + this.documentClickHandler = (event: MouseEvent) => { + const target = event.target as HTMLElement; + // Check if click is outside any multiselect dropdown + if (!target.closest('.multiselect-container')) { + this.openMultiselects.clear(); + this.removeDocumentClickHandler(); + } + }; + + // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it + setTimeout(() => { + document.addEventListener('click', this.documentClickHandler!); + }, 0); + } + } + + // Remove document click handler + private removeDocumentClickHandler(): void { + if (this.documentClickHandler) { + document.removeEventListener('click', this.documentClickHandler); + this.documentClickHandler = null; + } + } + + // Check if multiselect dropdown is open + isMultiselectOpen(filter: any, context: string): boolean { + const filterId = `${context}-${filter.field}`; + return this.openMultiselects.has(filterId); + } + + // Get count of selected options for a multiselect filter + getSelectedOptionsCount(filter: any): number { + if (!filter.value) { + return 0; + } + + if (Array.isArray(filter.value)) { + return filter.value.length; + } + + return 0; + } + + // Clear all filters + clearAllFilters(): void { + // Clear base filters + if (this.baseFilters) { + this.baseFilters.forEach(filter => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + + // Clear drilldown filters + if (this.drilldownFilters) { + this.drilldownFilters.forEach(filter => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + + // Clear layer filters + if (this.drilldownLayers) { + this.drilldownLayers.forEach(layer => { + if (layer.filters) { + layer.filters.forEach((filter: any) => { + if (filter.type === 'multiselect') { + filter.value = []; + } else if (filter.type === 'date-range') { + filter.value = { start: null, end: null }; + } else if (filter.type === 'toggle') { + filter.value = false; + } else { + filter.value = ''; + } + }); + } + }); + } + + // Close all multiselect dropdowns + this.openMultiselects.clear(); + + // Refresh data + // For To Do chart, this would trigger a refresh of the todo list + } + + ngOnDestroy(): void { + // Unsubscribe from all subscriptions to prevent memory leaks + this.subscriptions.forEach(subscription => subscription.unsubscribe()); + + // Remove document click handler if it exists + this.removeDocumentClickHandler(); + } } \ No newline at end of file