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 @@
+
+
+
0">
+
+ Level: {{ currentDrilldownLevel }}
+
+
+
+
+
{{ charttitle }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0">
+
Filters
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ 0">
+ {{ getSelectedOptionsCount(filter) }} selected
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0 && currentDrilldownLevel > 0">
+
Drilldown Filters
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ 0">
+ {{ getSelectedOptionsCount(filter) }} selected
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Layer Filters
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ 0">
+ {{ 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