From ffda17e6b15fb6a68c1b2397e1daba4243cf395f Mon Sep 17 00:00:00 2001 From: Gaurav Kumar Date: Fri, 31 Oct 2025 16:35:15 +0530 Subject: [PATCH] unified --- .../gadgets/unified-chart/index.ts | 1 + .../unified-chart.component.html | 365 +++++ .../unified-chart.component.scss | 262 ++++ .../unified-chart.component.spec.ts | 21 + .../unified-chart/unified-chart.component.ts | 1349 +++++++++++++++++ 5 files changed, 1998 insertions(+) create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/index.ts create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.html create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.scss create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.spec.ts create mode 100644 frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.ts diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/index.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/index.ts new file mode 100644 index 0000000..3552a7e --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/index.ts @@ -0,0 +1 @@ +export * from './unified-chart.component'; \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.html new file mode 100644 index 0000000..3888a09 --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.html @@ -0,0 +1,365 @@ +
+ +
+ + Level: {{ currentDrilldownLevel }} +
+ + +
+

{{ charttitle }}

+
+ + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+
Filters
+
+
+ +
+ + +
+ + +
+ + +
+ + +
+ +
+
+ Select {{ filter.field }} + + {{ getSelectedOptionsCount(filter) }} selected + +
+
+
+ + +
+
+
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+
+
+
+ + +
+
Drilldown Filters
+
+
+ +
+ + +
+ + +
+ + +
+ + +
+ +
+
+ Select {{ filter.field }} + + {{ getSelectedOptionsCount(filter) }} selected + +
+
+
+ + +
+
+
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+
+
+
+ + +
+
Layer Filters
+
+
+ +
+ + +
+ + +
+ + +
+ + +
+ +
+
+ Select {{ filter.field }} + + {{ getSelectedOptionsCount(filter) }} selected + +
+
+
+ + +
+
+
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+
+
+
+ + +
+ +
+
+ + +
+

No data available for the selected filters.

+ +
+ + +
+
+

Loading chart data...

+
\ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.scss new file mode 100644 index 0000000..c9e5088 --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.scss @@ -0,0 +1,262 @@ +.chart-container { + position: relative; + height: 100%; + width: 100%; + padding: 10px; +} + +.drilldown-back { + display: flex; + align-items: center; + margin-bottom: 10px; + + .drilldown-level { + margin-left: 10px; + font-size: 14px; + color: #666; + } +} + +.chart-title { + text-align: center; + margin-bottom: 15px; + + h4 { + margin: 0; + color: #333; + } +} + +.chart-wrapper { + position: relative; + height: calc(100% - 100px); + min-height: 300px; +} + +.filters-section { + margin-top: 20px; + padding: 15px; + border: 1px solid #e0e0e0; + border-radius: 4px; + background-color: #f9f9f9; + + h5 { + margin-top: 0; + margin-bottom: 10px; + color: #333; + } +} + +.filters-container { + display: flex; + flex-wrap: wrap; + gap: 15px; +} + +.filter-item { + flex: 1 1 200px; + min-width: 150px; + + label { + display: block; + margin-bottom: 5px; + font-weight: 500; + color: #555; + } + + .form-control { + width: 100%; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + } +} + +.filter-text, +.filter-dropdown, +.filter-date-range { + .form-control { + height: 36px; + } +} + +.date-range-inputs { + display: flex; + gap: 10px; + + .form-control { + flex: 1; + } +} + +.filter-multiselect { + position: relative; + + .multiselect-container { + position: relative; + } + + .multiselect-display { + width: 100%; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + background-color: white; + cursor: pointer; + min-height: 36px; + display: flex; + align-items: center; + } + + .multiselect-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background-color: white; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + z-index: 1000; + max-height: 200px; + overflow-y: auto; + } + + .multiselect-option { + padding: 8px 12px; + border-bottom: 1px solid #eee; + + &:last-child { + border-bottom: none; + } + + &:hover { + background-color: #f5f5f5; + } + + input[type="checkbox"] { + margin-right: 8px; + } + + label { + margin: 0; + font-weight: normal; + cursor: pointer; + } + } +} + +.filter-toggle { + display: flex; + align-items: center; + gap: 10px; + + .toggle-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; + } + + .toggle-switch input { + opacity: 0; + width: 0; + height: 0; + } + + .toggle-label { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; + border-radius: 24px; + } + + .toggle-slider { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; + border-radius: 50%; + } + + input:checked + .toggle-label { + background-color: #2196F3; + } + + input:checked + .toggle-label .toggle-slider { + transform: translateX(26px); + } +} + +.clear-filters { + margin-top: 15px; + text-align: center; +} + +.no-data-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 300px; + text-align: center; + color: #666; + + p { + margin-bottom: 15px; + font-size: 16px; + } +} + +.loading-indicator { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 300px; + + .spinner { + width: 40px; + height: 40px; + border: 4px solid #f3f3f3; + border-top: 4px solid #3498db; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 15px; + } + + p { + margin: 0; + color: #666; + } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +// Responsive adjustments +@media (max-width: 768px) { + .filters-container { + flex-direction: column; + } + + .filter-item { + min-width: 100%; + } + + .chart-wrapper { + min-height: 250px; + } +} \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.spec.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.spec.ts new file mode 100644 index 0000000..94a60e4 --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UnifiedChartComponent } from './unified-chart.component'; + +describe('UnifiedChartComponent', () => { + let component: UnifiedChartComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [UnifiedChartComponent] + }); + fixture = TestBed.createComponent(UnifiedChartComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.ts new file mode 100644 index 0000000..e3e9011 --- /dev/null +++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.ts @@ -0,0 +1,1349 @@ +import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, ViewChild } from '@angular/core'; +import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service'; +import { FilterService } from '../../common-filter/filter.service'; +import { Subscription } from 'rxjs'; +import { BaseChartDirective } from 'ng2-charts'; +import { ChartConfiguration, ChartDataset } from 'chart.js'; + +@Component({ + selector: 'app-unified-chart', + templateUrl: './unified-chart.component.html', + styleUrls: ['./unified-chart.component.scss'] +}) +export class UnifiedChartComponent implements OnInit, OnChanges, OnDestroy { + @Input() chartType: string; + @Input() xAxis: string; + @Input() yAxis: string | string[]; + @Input() table: string; + @Input() datastore: string; + @Input() charttitle: string; + @Input() chartlegend: boolean = true; + @Input() showlabel: boolean = true; + @Input() chartcolor: boolean; + @Input() slices: boolean; + @Input() donut: boolean; + @Input() charturl: string; + @Input() chartparameter: string; + @Input() datasource: string; + @Input() fieldName: string; + @Input() connection: number; + + // Drilldown configuration inputs + @Input() drilldownEnabled: boolean = false; + @Input() drilldownApiUrl: string; + @Input() drilldownXAxis: string; + @Input() drilldownYAxis: string; + @Input() drilldownParameter: string; + @Input() baseFilters: any[] = []; + @Input() drilldownFilters: any[] = []; + @Input() drilldownLayers: any[] = []; + + @ViewChild(BaseChartDirective) chart?: BaseChartDirective; + + // Chart data properties + chartLabels: string[] = []; + chartData: any[] = []; + chartOptions: any = {}; + chartPlugins = []; + chartLegend: boolean = true; + + // Bubble chart specific properties + bubbleChartData: ChartDataset[] = []; + + // No data state + noDataAvailable: boolean = false; + + // Loading state + isLoading: boolean = false; + + // Multi-layer drilldown state tracking + drilldownStack: any[] = []; + currentDrilldownLevel: number = 0; + originalChartData: any = {}; + + // Flag to prevent infinite loops + private isFetchingData: boolean = false; + + // Subscriptions to unsubscribe on destroy + private subscriptions: Subscription[] = []; + + // Filter properties + private openMultiselects: Map = new Map(); + 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 => { + this.fetchChartData(); + }) + ); + + this.initializeChartOptions(); + this.fetchChartData(); + } + + ngOnChanges(changes: SimpleChanges): void { + console.log('UnifiedChartComponent 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 chartTypeChanged = changes.chartType && !changes.chartType.firstChange; + const xAxisChanged = changes.xAxis && !changes.xAxis.firstChange; + const yAxisChanged = changes.yAxis && !changes.yAxis.firstChange; + const tableChanged = changes.table && !changes.table.firstChange; + const connectionChanged = changes.connection && !changes.connection.firstChange; + const baseFiltersChanged = changes.baseFilters && !changes.baseFilters.firstChange; + const drilldownFiltersChanged = changes.drilldownFilters && !changes.drilldownFilters.firstChange; + + // Drilldown configuration changes + const drilldownEnabledChanged = changes.drilldownEnabled && !changes.drilldownEnabled.firstChange; + const drilldownApiUrlChanged = changes.drilldownApiUrl && !changes.drilldownApiUrl.firstChange; + const drilldownXAxisChanged = changes.drilldownXAxis && !changes.drilldownXAxis.firstChange; + const drilldownYAxisChanged = changes.drilldownYAxis && !changes.drilldownYAxis.firstChange; + const drilldownLayersChanged = changes.drilldownLayers && !changes.drilldownLayers.firstChange; + + // Only fetch data if the actual chart configuration changed and we're not already fetching + if (!this.isFetchingData && (chartTypeChanged || xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged || drilldownFiltersChanged || + drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged || + drilldownLayersChanged)) { + console.log('Chart configuration changed, fetching new data'); + this.initializeChartOptions(); + this.fetchChartData(); + } + + // Update legend visibility if it changed + if (changes.chartlegend !== undefined) { + this.chartLegend = changes.chartlegend.currentValue; + this.chartOptions.plugins.legend.display = this.chartLegend; + console.log('Chart legend changed to:', this.chartLegend); + } + } + + ngOnDestroy(): void { + // Unsubscribe from all subscriptions + this.subscriptions.forEach(sub => sub.unsubscribe()); + + // Remove document click handler + if (this.documentClickHandler) { + document.removeEventListener('click', this.documentClickHandler); + } + } + + // 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 + }); + } + + // Initialize chart options based on chart type + private initializeChartOptions(): void { + switch (this.chartType) { + case 'bar': + this.initializeBarChartOptions(); + break; + case 'line': + this.initializeLineChartOptions(); + break; + case 'pie': + this.initializePieChartOptions(); + break; + case 'doughnut': + this.initializeDoughnutChartOptions(); + break; + case 'bubble': + this.initializeBubbleChartOptions(); + break; + case 'radar': + this.initializeRadarChartOptions(); + break; + case 'polar': + this.initializePolarChartOptions(); + break; + case 'scatter': + this.initializeScatterChartOptions(); + break; + default: + this.initializeDefaultChartOptions(); + } + } + + private initializeBarChartOptions(): void { + this.chartOptions = { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + ticks: { + autoSkip: false, + maxRotation: 45, + minRotation: 45, + padding: 15, + font: { + size: 12 + } + }, + grid: { + display: false + } + }, + y: { + beginAtZero: true, + ticks: { + font: { + size: 12 + } + } + } + }, + plugins: { + legend: { + display: true, + position: 'top', + labels: { + font: { + size: 12 + } + } + }, + tooltip: { + enabled: true + } + }, + layout: { + padding: { + bottom: 60, + left: 15, + right: 15, + top: 15 + } + } + }; + } + + private initializeLineChartOptions(): void { + this.chartOptions = { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + ticks: { + autoSkip: false, + maxRotation: 45, + minRotation: 45, + padding: 15, + font: { + size: 12 + } + }, + grid: { + display: false + } + }, + y: { + beginAtZero: true, + ticks: { + font: { + size: 12 + } + } + } + }, + plugins: { + legend: { + display: true, + position: 'top', + labels: { + font: { + size: 12 + } + } + }, + tooltip: { + enabled: true + } + }, + layout: { + padding: { + bottom: 60, + left: 15, + right: 15, + top: 15 + } + } + }; + } + + private initializePieChartOptions(): void { + this.chartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'top', + labels: { + font: { + size: 12 + } + } + }, + tooltip: { + enabled: true + } + } + }; + } + + private initializeDoughnutChartOptions(): void { + this.chartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'top', + labels: { + font: { + size: 12 + } + } + }, + tooltip: { + enabled: true + } + } + }; + } + + private initializeBubbleChartOptions(): void { + this.chartOptions = { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + beginAtZero: true, + title: { + display: true, + text: 'X Axis' + }, + ticks: { + autoSkip: false, + maxRotation: 45, + minRotation: 45 + } + }, + y: { + beginAtZero: true, + title: { + display: true, + text: 'Y Axis' + } + } + }, + plugins: { + legend: { + display: true, + position: 'top', + }, + tooltip: { + enabled: true, + mode: 'point', + intersect: false, + callbacks: { + label: function(context: any) { + const point: any = context.raw; + if (point && point.hasOwnProperty('y') && point.hasOwnProperty('r')) { + const yValue = parseFloat(point.y); + const rValue = parseFloat(point.r); + if (!isNaN(yValue) && !isNaN(rValue)) { + return `Value: ${yValue.toFixed(2)}, Size: ${rValue.toFixed(1)}`; + } + } + return context.dataset.label || ''; + } + } + } + }, + animation: { + duration: 800, + easing: 'easeInOutQuart' + }, + elements: { + point: { + hoverRadius: 12, + hoverBorderWidth: 3 + } + }, + layout: { + padding: { + left: 10, + right: 10, + top: 10, + bottom: 30 + } + } + }; + } + + private initializeRadarChartOptions(): void { + this.chartOptions = { + responsive: true, + maintainAspectRatio: false, + scales: { + r: { + angleLines: { + display: true + }, + suggestedMin: 0, + ticks: { + backdropColor: 'rgba(0, 0, 0, 0)' + } + } + }, + plugins: { + legend: { + display: true, + position: 'top' + } + } + }; + } + + private initializePolarChartOptions(): void { + this.chartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'top' + } + } + }; + } + + private initializeScatterChartOptions(): void { + this.chartOptions = { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + type: 'linear', + position: 'bottom' + } + }, + plugins: { + legend: { + display: true, + position: 'top' + } + } + }; + } + + private initializeDefaultChartOptions(): void { + this.chartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'top' + } + } + }; + } + + fetchChartData(): void { + // Set flag to prevent recursive calls + this.isFetchingData = true; + this.isLoading = true; + this.noDataAvailable = false; + + console.log('Starting fetchChartData for chart type:', this.chartType); + + // If we're in drilldown mode, fetch the appropriate drilldown data + if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) { + console.log('Fetching drilldown data'); + this.fetchDrilldownData(); + return; + } + + // If we have the necessary data, fetch chart data from the service + if (this.table && this.xAxis && this.yAxis) { + console.log('Fetching chart data for:', { + chartType: this.chartType, + table: this.table, + xAxis: this.xAxis, + yAxis: this.yAxis, + connection: this.connection + }); + + // Convert yAxis to string if it's an array + const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis; + + // Convert baseFilters to filter parameters + let filterParams = ''; + if (this.baseFilters && this.baseFilters.length > 0) { + const filterObj = {}; + this.baseFilters.forEach(filter => { + if (filter.field && filter.value) { + filterObj[filter.field] = filter.value; + } + }); + if (Object.keys(filterObj).length > 0) { + filterParams = JSON.stringify(filterObj); + } + } + + // 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); + + // Fetch data from the dashboard service + this.dashboardService.getChartData( + this.table, + this.chartType, + this.xAxis, + yAxisString, + this.connection, + '', + '', + filterParams + ).subscribe( + (data: any) => { + console.log('Received chart data:', data); + + if (data === null || data === undefined) { + console.warn('Chart API returned null/undefined data.'); + this.noDataAvailable = true; + } else if (data && data.chartLabels && data.chartData) { + // Handle the standard format + this.noDataAvailable = data.chartLabels.length === 0; + + if (this.chartType === 'bubble') { + // For bubble charts, transform the data + this.bubbleChartData = this.transformToBubbleData(data.chartLabels, data.chartData); + } else { + this.chartLabels = data.chartLabels; + this.chartData = data.chartData; + } + } else if (data && data.labels && data.datasets) { + // Handle the legacy format + this.noDataAvailable = data.labels.length === 0; + + if (this.chartType === 'bubble') { + // For bubble charts, use the datasets directly + this.bubbleChartData = data.datasets; + } else { + this.chartLabels = data.labels; + this.chartData = data.datasets; + } + } else { + console.warn('Chart received data does not have expected structure', data); + this.noDataAvailable = true; + } + + // Reset flags after fetching + this.isFetchingData = false; + this.isLoading = false; + + // Trigger chart update + setTimeout(() => { + if (this.chart) { + this.chart.update(); + } + }, 100); + }, + (error) => { + console.error('Error fetching chart data:', error); + this.noDataAvailable = true; + this.chartLabels = []; + this.chartData = []; + this.bubbleChartData = []; + + // Reset flags after fetching + this.isFetchingData = false; + this.isLoading = false; + } + ); + } else { + console.log('Missing required data for chart:', { + chartType: this.chartType, + table: this.table, + xAxis: this.xAxis, + yAxis: this.yAxis, + connection: this.connection + }); + this.noDataAvailable = true; + this.chartLabels = []; + this.chartData = []; + this.bubbleChartData = []; + + // Reset flags after fetching + this.isFetchingData = false; + this.isLoading = false; + } + } + + // Fetch drilldown data based on current drilldown level + fetchDrilldownData(): void { + console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel); + console.log('Drilldown stack:', this.drilldownStack); + + // Get the current drilldown configuration based on the current level + let drilldownConfig; + if (this.currentDrilldownLevel === 1) { + // Base drilldown level + drilldownConfig = { + apiUrl: this.drilldownApiUrl, + xAxis: this.drilldownXAxis, + yAxis: this.drilldownYAxis, + parameter: this.drilldownParameter + }; + } else { + // Multi-layer drilldown level + const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown + if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) { + drilldownConfig = this.drilldownLayers[layerIndex]; + } else { + console.warn('Invalid drilldown layer index:', layerIndex); + this.noDataAvailable = true; + this.chartLabels = []; + this.chartData = []; + this.bubbleChartData = []; + return; + } + } + + console.log('Drilldown config for level', this.currentDrilldownLevel, ':', drilldownConfig); + + // Check if we have valid drilldown configuration + if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) { + console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel); + this.noDataAvailable = true; + this.chartLabels = []; + this.chartData = []; + this.bubbleChartData = []; + return; + } + + // Get the parameter value from the drilldown stack + let parameterValue = ''; + if (this.drilldownStack.length > 0) { + const lastEntry = this.drilldownStack[this.drilldownStack.length - 1]; + parameterValue = lastEntry.clickedValue || ''; + console.log('Parameter value from last click:', parameterValue); + } + + // Get the parameter field from drilldown config + const parameterField = drilldownConfig.parameter || ''; + console.log('Parameter field:', parameterField); + + console.log('Fetching drilldown data for level:', this.currentDrilldownLevel, { + apiUrl: drilldownConfig.apiUrl, + xAxis: drilldownConfig.xAxis, + yAxis: drilldownConfig.yAxis, + parameterField: parameterField, + parameterValue: parameterValue, + connection: this.connection + }); + + // Build the actual API URL with parameter replacement + let actualApiUrl = drilldownConfig.apiUrl; + console.log('Original API URL:', actualApiUrl); + console.log('Parameter value to use:', parameterValue); + console.log('Parameter field:', parameterField); + + // Check if the URL contains angle brackets for parameter replacement + const hasAngleBrackets = /<[^>]+>/.test(actualApiUrl); + + if (hasAngleBrackets && parameterValue) { + // Replace angle brackets placeholder with actual value + console.log('Replacing angle brackets with parameter value'); + const encodedValue = encodeURIComponent(parameterValue); + actualApiUrl = actualApiUrl.replace(/<[^>]+>/g, encodedValue); + console.log('URL after angle bracket replacement:', actualApiUrl); + } + + // Convert drilldown layer filters to filter parameters (if applicable) + const filterObj = {}; + + // Add drilldown layer filters + if (drilldownConfig.filters && drilldownConfig.filters.length > 0) { + drilldownConfig.filters.forEach((filter: any) => { + if (filter.field && filter.value) { + filterObj[filter.field] = filter.value; + } + }); + } + + // Add drilldownFilters + if (this.drilldownFilters && this.drilldownFilters.length > 0) { + this.drilldownFilters.forEach(filter => { + if (filter.field && filter.value) { + filterObj[filter.field] = filter.value; + } + }); + } + + // Add common filters + const commonFilters = this.filterService.getFilterValues(); + Object.keys(commonFilters).forEach(filterId => { + const filterValue = commonFilters[filterId]; + + // Find the filter definition to get the field name + const filterDef = this.filterService.getFilters().find(f => f.id === filterId); + + if (filterDef && filterDef.field) { + const fieldName = filterDef.field; + if (filterValue !== undefined && filterValue !== null && filterValue !== '') { + filterObj[fieldName] = filterValue; + } + } + }); + + // Convert to JSON string for API call + let drilldownFilterParams = ''; + if (Object.keys(filterObj).length > 0) { + drilldownFilterParams = JSON.stringify(filterObj); + } + + console.log('Drilldown filter parameters:', drilldownFilterParams); + + // Fetch data from the dashboard service + this.dashboardService.getChartData( + actualApiUrl, + this.chartType, + drilldownConfig.xAxis, + drilldownConfig.yAxis, + this.connection, + parameterField, + parameterValue, + drilldownFilterParams + ).subscribe( + (data: any) => { + console.log('Received drilldown data:', data); + if (data === null) { + console.warn('Drilldown API returned null data.'); + this.noDataAvailable = true; + this.chartLabels = []; + this.chartData = []; + this.bubbleChartData = []; + return; + } + + // Handle the actual data structure returned by the API + if (data && data.chartLabels && data.chartData) { + // Backend has already filtered the data, just display it + this.noDataAvailable = data.chartLabels.length === 0; + + if (this.chartType === 'bubble') { + // For bubble charts, transform the data + this.bubbleChartData = this.transformToBubbleData(data.chartLabels, data.chartData); + } else { + this.chartLabels = data.chartLabels; + this.chartData = data.chartData; + } + + console.log('Updated chart with drilldown data:', { + labels: this.chartLabels, + data: this.chartData, + bubbleData: this.bubbleChartData + }); + } else if (data && data.labels && data.datasets) { + // Handle the legacy format + this.noDataAvailable = data.labels.length === 0; + + if (this.chartType === 'bubble') { + // For bubble charts, use the datasets directly + this.bubbleChartData = data.datasets; + } else { + this.chartLabels = data.labels; + this.chartData = data.datasets; + } + + console.log('Updated chart with drilldown legacy data format:', { + labels: this.chartLabels, + data: this.chartData, + bubbleData: this.bubbleChartData + }); + } else { + console.warn('Drilldown received data does not have expected structure', data); + this.noDataAvailable = true; + this.chartLabels = []; + this.chartData = []; + this.bubbleChartData = []; + } + + // Set loading state to false + this.isLoading = false; + + // Trigger chart update + setTimeout(() => { + if (this.chart) { + this.chart.update(); + } + }, 100); + }, + (error) => { + console.error('Error fetching drilldown data:', error); + this.noDataAvailable = true; + this.chartLabels = []; + this.chartData = []; + this.bubbleChartData = []; + this.isLoading = false; + } + ); + } + + // Transform data to bubble chart format + private transformToBubbleData(labels: any[], data: any[]): ChartDataset[] { + console.log('Transforming data to bubble format:', { labels, data }); + + // Handle null/undefined data + if (!labels || !data) { + console.log('Labels or data is null/undefined, returning empty dataset'); + return []; + } + + // 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')) { + console.log('Data is already in bubble format, returning as is'); + return data; + } + + // Transform the data properly for bubble chart + // Assuming labels are x-values and data[0].data are y-values + if (labels && data && data.length > 0 && data[0].data) { + console.log('Transforming regular data to bubble format'); + const yValues = data[0].data; + const label = data[0].label || 'Dataset 1'; + + // Handle case where yValues might not be an array + if (!Array.isArray(yValues)) { + console.log('yValues is not an array, returning empty dataset'); + return []; + } + + console.log('yValues type:', typeof yValues); + console.log('yValues length:', yValues.length); + console.log('First few yValues:', yValues.slice(0, 5)); + + // Find min and max values for scaling + let minValue = Infinity; + let maxValue = -Infinity; + const validYValues = []; + + // First pass: collect valid values and find min/max + for (let i = 0; i < yValues.length; i++) { + let y; + if (typeof yValues[i] === 'string') { + y = parseFloat(yValues[i]); + } else { + y = yValues[i]; + } + + if (!isNaN(y)) { + validYValues.push(y); + minValue = Math.min(minValue, y); + maxValue = Math.max(maxValue, y); + } + } + + console.log('Value range:', { minValue, maxValue }); + + // Adjust radius range based on number of data points + const dataPointCount = Math.min(labels.length, yValues.length); + let minRadius = 3; + let maxRadius = 30; + + // Adjust radius range based on data point count + if (dataPointCount > 50) { + minRadius = 2; + maxRadius = 20; + } else if (dataPointCount > 20) { + minRadius = 3; + maxRadius = 25; + } + + console.log('Radius range:', { minRadius, maxRadius, dataPointCount }); + + // Create bubble points from labels (x) and data (y) + const bubblePoints = []; + const bubbleColors = []; + const minLength = Math.min(labels.length, yValues.length); + + console.log('Processing data points:', { labels, yValues, minLength }); + + for (let i = 0; i < minLength; i++) { + // Convert y to number if it's a string + let y; + if (typeof yValues[i] === 'string') { + y = parseFloat(yValues[i]); + console.log(`Converted string yValue to number: ${yValues[i]} -> ${y}`); + } else { + y = yValues[i]; + } + + // Handle NaN values + if (isNaN(y)) { + console.log(`Skipping point ${i} due to NaN y value: ${yValues[i]}`); + continue; + } + + // Calculate radius based on the y-value with logarithmic scaling + const r = this.logScale(y, minValue, maxValue, minRadius, maxRadius); + + console.log(`Value: ${y}, Radius: ${r}`); + + // For x-value, we'll use the index position since labels are strings + const x = i; + + // Generate a unique color for this bubble + const backgroundColor = this.generateBubbleColor(i, y, minLength); + + // Store the color for the dataset + bubbleColors.push(backgroundColor); + + // Add the point + const point = { + x, + y, + r + }; + console.log(`Adding point ${i}:`, point); + bubblePoints.push(point); + } + + console.log('Generated bubble points:', bubblePoints); + console.log('Generated bubble points count:', bubblePoints.length); + console.log('Generated bubble colors count:', bubbleColors.length); + + // If we have no valid points, return empty array + if (bubblePoints.length === 0) { + console.log('No valid bubble points generated, returning empty dataset'); + return []; + } + + // Create a single dataset with all bubble points + const bubbleDatasets: ChartDataset[] = [ + { + data: bubblePoints, + label: label, + backgroundColor: bubbleColors, + borderColor: bubbleColors.map(color => this.replaceAlpha(color, 1)), + hoverBackgroundColor: bubbleColors.map(color => this.replaceAlpha(color, 0.9)), + hoverBorderColor: 'rgba(255, 255, 255, 1)', + borderWidth: 2, + pointHoverRadius: 10, + } + ]; + + console.log('Transformed bubble data:', bubbleDatasets); + return bubbleDatasets; + } + + console.log('Could not transform data, returning empty dataset'); + return []; + } + + // Helper function to calculate logarithmic scaling + private logScale(value: number, min: number, max: number, minRadius: number, maxRadius: number): number { + if (min === max) return (minRadius + maxRadius) / 2; + + // Normalize value to 0-1 range + const normalized = (value - min) / (max - min); + + // Apply logarithmic scaling (base 10) + // Add 1 to avoid log(0) and scale to 1-10 range + const logValue = Math.log10(normalized * 9 + 1); + + // Scale to desired radius range + return minRadius + (logValue / Math.log10(10)) * (maxRadius - minRadius); + } + + // Helper function to generate different colors for bubbles + private generateBubbleColor(index: number, value: number, total: number): string { + // Generate colors based on index or value + // Using HSL color model for better color distribution + const hue = (index * 137.508) % 360; // Golden angle approximation for good distribution + const saturation = 80 + (index % 20); // High saturation for vibrant colors + const lightness = 40 + (index % 30); // Vary lightness for contrast + + // Convert HSL to RGB + const h = hue / 360; + const s = saturation / 100; + const l = lightness / 100; + + const rgb = this.hslToRgb(h, s, l); + return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.7)`; + } + + // Helper function to convert HSL to RGB + private hslToRgb(h: number, s: number, l: number): { r: number, g: number, b: number } { + let r, g, b; + + if (s === 0) { + r = g = b = l; // achromatic + } else { + const hue2rgb = (p: number, q: number, t: number) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1/6) return p + (q - p) * 6 * t; + if (t < 1/2) return q; + if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + }; + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; + } + + // Helper function to replace alpha value in RGBA color string + private replaceAlpha(color: string, newAlpha: number): string { + // Match rgba(r, g, b, a) format and replace alpha value + return color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/, `rgba($1, $2, $3, ${newAlpha})`); + } + + // Reset to original data (go back to base level) + resetToOriginalData(): void { + console.log('Resetting to original data'); + console.log('Current stack before reset:', this.drilldownStack); + console.log('Current level before reset:', this.currentDrilldownLevel); + + this.currentDrilldownLevel = 0; + this.drilldownStack = []; + + if (this.originalChartData.labels && this.originalChartData.data) { + this.chartLabels = [...this.originalChartData.labels]; + this.chartData = [...this.originalChartData.data]; + console.log('Restored original data'); + } + + console.log('After reset - data:', { labels: this.chartLabels, data: this.chartData }); + + // Re-fetch original data + this.fetchChartData(); + } + + // Navigate back to previous drilldown level + navigateBack(): void { + console.log('Navigating back, current stack:', this.drilldownStack); + console.log('Current level:', this.currentDrilldownLevel); + + if (this.drilldownStack.length > 0) { + // Remove the last entry from the stack + const removedEntry = this.drilldownStack.pop(); + console.log('Removed entry from stack:', removedEntry); + + // Update the current drilldown level + this.currentDrilldownLevel = this.drilldownStack.length; + console.log('New level after pop:', this.currentDrilldownLevel); + console.log('Stack after pop:', this.drilldownStack); + + if (this.drilldownStack.length > 0) { + // Fetch data for the previous level + console.log('Fetching data for previous level'); + this.fetchDrilldownData(); + } else { + // Back to base level + console.log('Back to base level, resetting to original data'); + this.resetToOriginalData(); + } + } else { + // Already at base level, reset to original data + console.log('Already at base level, resetting to original data'); + this.resetToOriginalData(); + } + } + + // Chart click handler + public chartClicked(e: any): void { + console.log('Chart clicked:', e); + + // If drilldown is enabled and we have a valid click event + if (this.drilldownEnabled && e.active && e.active.length > 0) { + // Get the index of the clicked element + const clickedIndex = e.active[0].index; + + // Get the dataset index + const datasetIndex = e.active[0].datasetIndex; + + // Get the data point + let dataPoint; + if (this.chartType === 'bubble') { + dataPoint = this.bubbleChartData[datasetIndex].data[clickedIndex]; + } else { + dataPoint = this.chartData[datasetIndex].data[clickedIndex]; + } + + console.log('Clicked on chart element:', { 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.originalChartData = { + labels: [...this.chartLabels], + data: JSON.parse(JSON.stringify(this.chartData)), + bubbleData: JSON.parse(JSON.stringify(this.bubbleChartData)) + }; + console.log('Stored original data for drilldown'); + } + + // Determine the next drilldown level + const nextDrilldownLevel = this.currentDrilldownLevel + 1; + + console.log('Next drilldown level will be:', nextDrilldownLevel); + + // Check if there's a drilldown configuration for this level + let hasDrilldownConfig = false; + let drilldownConfig; + + if (nextDrilldownLevel === 1) { + // Base drilldown level + drilldownConfig = { + apiUrl: this.drilldownApiUrl, + xAxis: this.drilldownXAxis, + yAxis: this.drilldownYAxis, + parameter: this.drilldownParameter + }; + hasDrilldownConfig = !!this.drilldownApiUrl && !!this.drilldownXAxis && !!this.drilldownYAxis; + } else { + // Multi-layer drilldown level + const layerIndex = nextDrilldownLevel - 2; // -2 because level 1 is base drilldown + if (layerIndex < this.drilldownLayers.length) { + drilldownConfig = this.drilldownLayers[layerIndex]; + hasDrilldownConfig = drilldownConfig.enabled && + !!drilldownConfig.apiUrl && + !!drilldownConfig.xAxis && + !!drilldownConfig.yAxis; + } + } + + console.log('Drilldown config for next level:', drilldownConfig); + console.log('Has drilldown config:', hasDrilldownConfig); + + // If there's a drilldown configuration for the next level, proceed + if (hasDrilldownConfig) { + // Add this click to the drilldown stack + const stackEntry = { + level: nextDrilldownLevel, + datasetIndex: datasetIndex, + clickedIndex: clickedIndex, + dataPoint: dataPoint, + clickedValue: dataPoint // Using data point as value for now + }; + + this.drilldownStack.push(stackEntry); + + console.log('Added to drilldown stack:', stackEntry); + console.log('Current drilldown stack:', this.drilldownStack); + + // Update the current drilldown level + this.currentDrilldownLevel = nextDrilldownLevel; + + console.log('Entering drilldown level:', this.currentDrilldownLevel); + + // Fetch drilldown data for the new level + this.fetchDrilldownData(); + } else { + console.log('No drilldown configuration for level:', nextDrilldownLevel); + } + } else { + console.log('Drilldown not enabled or invalid click event'); + } + } + + public chartHovered(e: any): void { + console.log('Chart hovered:', e); + } + + // Method to check if chart data is valid + public isChartDataValid(): boolean { + if (this.chartType === 'bubble') { + console.log('Checking if bubble chart data is valid:', this.bubbleChartData); + if (!this.bubbleChartData || this.bubbleChartData.length === 0) { + console.log('Bubble chart data is null or empty'); + return false; + } + + // Check if any dataset has data + for (const dataset of this.bubbleChartData) { + console.log('Checking dataset:', dataset); + if (dataset.data && dataset.data.length > 0) { + console.log('Dataset has data, length:', dataset.data.length); + // For bubble charts, check if data points have x, y, r properties + for (const point of dataset.data) { + console.log('Checking point:', point); + if (typeof point === 'object' && point.hasOwnProperty('x') && point.hasOwnProperty('y') && point.hasOwnProperty('r')) { + // Valid bubble point + console.log('Found valid bubble point'); + return true; + } + } + } + } + + console.log('No valid bubble chart data found'); + return false; + } else { + console.log('Checking if chart data is valid:', { labels: this.chartLabels, data: this.chartData }); + if (!this.chartLabels || !this.chartData) { + console.log('Chart labels or data is null'); + return false; + } + + if (this.chartLabels.length === 0 || this.chartData.length === 0) { + console.log('Chart labels or data is empty'); + return false; + } + + // Check if any dataset has data + for (const dataset of this.chartData) { + if (dataset.data && dataset.data.length > 0) { + console.log('Found valid chart data'); + return true; + } + } + + console.log('No valid chart data found'); + return false; + } + } +} \ No newline at end of file