-
-
Columns
-
+
+
+ Columns
+
+ Y-Axis (Numeric)
+
+
+
+
+
+
+ {{selected}}
+
+
+
+ {{state}}
+
+
+
+
+
+
+
+
+
Y-Axis (Numeric)
-
-
-
-
-
-
- {{selected}}
-
-
-
- {{state}}
-
-
-
-
+
+ choose Column
+ {{data}}
+
+
-
-
-
-
-
Y-Axis (Numeric)
-
- choose Column
- {{data}}
-
+
+
+
+
+
Base API Filters
+
Configure filters for the main API (applied regardless of drilldown settings)
+
+
+
+
+
+
+ Add Filter
+
+
+
+
+
+ Filter {{i + 1}}
+
+
+
+
+
+
+
+
+ Select Field
+
+ {{column}}
+
+
+
+
+
+ Text
+ Dropdown
+ Multi-Select
+ Date Range
+ Toggle
+
+
+
+
+
+
+ Available: {{ filter.availableValues }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
Base Drilldown Configuration
+
+
+
+
+
+
+
Base Drilldown API URL
+
+
+
+
+
+
+
+
+
Enter the API URL for base drilldown data. Use angle brackets for parameters, e.g.,
+ http://api.example.com/data/<country>
+
+
+
+
+
+
Base Drilldown X-Axis
+
+ Select X-Axis Column
+ {{column}}
+
+
Select the column to use for X-axis in base drilldown view
+
+
+
+
+
+
Base Drilldown Y-Axis
+
+ Select Y-Axis Column
+ {{column}}
+
+
Select the column to use for Y-axis in base drilldown view
+
+
+
+
+
+
+
Base Drilldown Parameter
+
+ Select Parameter Column
+ {{column}}
+
+
Select the column to use as parameter for URL template replacement in base
+ drilldown
+
+
+
+
+
+
+
+
Base Drilldown Filters
+
Configure filters for the base drilldown level
+
+
+
+
+
+
+ Add Filter
+
+
+
+
+
+ Filter {{i + 1}}
+
+
+
+
+
+
+
+
+ Select Field
+ {{column}}
+
+
+
+
+
+ Text
+ Dropdown
+ Multi-Select
+ Date Range
+ Toggle
+
+
+
+
+
+
+ Available: {{ filter.availableValues }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Multi-Layer Drilldown Configurations
+
+ Add Drilldown Layer
+
+
Add additional drilldown layers for multi-level navigation
+
+
+
+
+
+
+
+
Drilldown Layer {{i + 1}}
+
+
+
+
+
+
+
+
+
+
Layer {{i + 1}} API URL
+
+
+
+
+
+
+
+
+
Enter the API URL for layer {{i + 1}} drilldown data. Use angle brackets for
+ parameters, e.g., http://api.example.com/data/<state>
+
+
+
+
+
+
Layer {{i + 1}} X-Axis
+
+ Select X-Axis Column
+ {{column}}
+
+
Select the column to use for X-axis in layer {{i + 1}} drilldown view
+
+
+
+
+
+
Layer {{i + 1}} Y-Axis
+
+ Select Y-Axis Column
+ {{column}}
+
+
Select the column to use for Y-axis in layer {{i + 1}} drilldown view
+
+
+
+
+
+
+
Layer {{i + 1}} Parameter
+
+ Select Parameter Column
+ {{column}}
+
+
Select the column to use as parameter for URL template replacement in layer {{i
+ +
+ 1}} drilldown
+
+
+
+
+
+
+
Layer {{i + 1}} Filters
+
Configure filters for this drilldown layer
+
+
+
+
+
+
+ Add Filter
+
+
+
+
+
+ Filter {{j + 1}}
+
+
+
+
+
+
+
+
+ Select Field
+ {{column}}
+
+
+
+
+
+ Text
+ Dropdown
+ Multi-Select
+ Date Range
+ Toggle
+
+
+
+
+
+
+ Available: {{ filter.availableValues }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
-
-
-
\ No newline at end of file
+
+
+
+
+ Configure Common Filter
+
+
+
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/editnewdash/editnewdash.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/editnewdash/editnewdash.component.ts
index 5104759..cf490a7 100644
--- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/editnewdash/editnewdash.component.ts
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/editnewdash/editnewdash.component.ts
@@ -20,7 +20,18 @@ import { GridViewComponent } from '../gadgets/grid-view/grid-view.component';
import { DatastoreService } from 'src/app/services/fnd/datastore.service';
import { AlertsService } from 'src/app/services/fnd/alerts.service';
import { isArray } from 'highcharts';
-// import { ChartItem } from '../chartitem';
+// Add the SureconnectService import
+import { SureconnectService } from '../sureconnect/sureconnect.service';
+// Add the CommonFilterComponent import
+import { CommonFilterComponent } from '../common-filter/common-filter.component';
+// Add the CompactFilterComponent import
+import { CompactFilterComponent } from '../common-filter';
+// Add the FilterService import
+import { FilterService } from '../common-filter/filter.service';
+// Add the UnifiedChartComponent import
+import { UnifiedChartComponent } from '../gadgets/unified-chart';
+// Add the DynamicChartLoaderService import
+import { DynamicChartLoaderService } from '../chart-config/dynamic-chart-loader.service';
function isNullArray(arr) {
return !Array.isArray(arr) || arr.length === 0;
@@ -36,582 +47,2170 @@ function isNullArray(arr) {
export class EditnewdashComponent implements OnInit {
- editId:number;
- toggle:boolean;
- modeledit:boolean = false;
+ editId: number;
+ toggle: boolean;
+ modeledit: boolean = false;
+ commonFilterModalOpen: boolean = false; // Add common filter modal state
public entryForm: FormGroup;
+ public commonFilterForm: FormGroup; // Add common filter form
+
+ // Add filterOptionsString property for compact filter
+ filterOptionsString: string = '';
+
+ // Add availableKeys property for compact filter
+ availableKeys: string[] = [];
+ // Initialize with default widgets and update dynamically
WidgetsMock: WidgetModel[] = [
{
- name: 'Radar Chart',
- identifier: 'radar_chart'
+ name: 'Common Filter',
+ identifier: 'common_filter'
},
{
- name: 'Doughnut Chart',
- identifier: 'doughnut_chart'
+ name: 'Radar Chart',
+ identifier: 'radar_chart'
},
{
- name: 'Line Chart',
- identifier: 'line_chart'
+ name: 'Doughnut Chart',
+ identifier: 'doughnut_chart'
+ },
+ {
+ name: 'Line Chart',
+ identifier: 'line_chart'
+ },
+ // {
+ // name: 'Bar Chart',
+ // identifier: 'bar_chart'
+ // },
+ // {
+ // name: 'Pie Chart',
+ // identifier: 'pie_chart'
+ // },
+ // {
+ // name: 'Polar Area Chart',
+ // identifier: 'polar_area_chart'
+ // },
+ // {
+ // name: 'Bubble Chart',
+ // identifier: 'bubble_chart'
+ // },
+ // {
+ // name: 'Scatter Chart',
+ // identifier: 'scatter_chart'
+ // },
+ {
+ name: 'Dynamic Chart',
+ identifier: 'dynamic_chart'
+ },
+ {
+ name: 'Financial Chart',
+ identifier: 'financial_chart'
+ },
+ {
+ name: 'To Do',
+ identifier: 'to_do_chart'
},
- {
- name: 'Bar Chart',
- identifier: 'bar_chart'
- },
- {
- name: 'Pie Chart',
- identifier: 'pie_chart'
- },
- {
- name: 'Polar Area Chart',
- identifier: 'polar_area_chart'
- },
- {
- name: 'Bubble Chart',
- identifier: 'bubble_chart'
- },
- {
- name: 'Scatter Chart',
- identifier: 'scatter_chart'
- },
- // {
- // name: 'Dynamic Chart',
- // identifier: 'dynamic_chart'
- // },
- // {
- // name: 'Financial Chart',
- // identifier: 'financial_chart'
- // },
- {
- name: 'To Do',
- identifier: 'to_do_chart'
- },
{
name: 'Grid View',
identifier: 'grid_view'
- }
-]
+ },
+ {
+ name: 'Compact Filter',
+ identifier: 'compact_filter'
+ },
+ {
+ name: 'Unified Chart',
+ identifier: 'unified_chart'
+ }
+ ]
public options: GridsterConfig;
- protected dashboardId: number;
- protected dashboardCollection: DashboardModel;
- //dashboardCollection:any;
- protected dashboardCollection1: DashboardModel[];
- public dashboardArray: DashboardContentModel[];
- public dashArr:[];
+ protected dashboardId: number;
+ protected dashboardCollection: DashboardModel;
+ //dashboardCollection:any;
+ protected dashboardCollection1: DashboardModel[];
+ public dashboardArray: DashboardContentModel[];
+ public dashArr: [];
- protected componentCollection = [
- { name: "Line Chart", componentInstance: LineChartComponent },
- { name: "Doughnut Chart", componentInstance: DoughnutChartComponent },
- { name: "Radar Chart", componentInstance: RadarChartComponent },
- { name: "Bar Chart", componentInstance: BarChartComponent },
- { name: "Pie Chart", componentInstance: PieChartComponent },
- { name: "Polar Area Chart", componentInstance: PolarChartComponent },
- { name: "Bubble Chart", componentInstance: BubbleChartComponent },
- { name: "Scatter Chart", componentInstance: ScatterChartComponent },
- { name: "Dynamic Chart", componentInstance: DynamicChartComponent },
- { name: "Financial Chart", componentInstance: FinancialChartComponent },
- { name: "To Do Chart", componentInstance: ToDoChartComponent },
+ protected componentCollection = [
+ { name: "Common Filter", componentInstance: CommonFilterComponent },
+ { name: "Line Chart", componentInstance: UnifiedChartComponent },
+ { name: "Doughnut Chart", componentInstance: UnifiedChartComponent },
+ { name: "Radar Chart", componentInstance: UnifiedChartComponent },
+ { name: "Bar Chart", componentInstance: UnifiedChartComponent },
+ { name: "Pie Chart", componentInstance: UnifiedChartComponent },
+ { name: "Polar Area Chart", componentInstance: UnifiedChartComponent },
+ { name: "Bubble Chart", componentInstance: UnifiedChartComponent },
+ { name: "Scatter Chart", componentInstance: UnifiedChartComponent },
+ { name: "Dynamic Chart", componentInstance: UnifiedChartComponent },
+ { name: "Financial Chart", componentInstance: UnifiedChartComponent },
+ { name: "To Do Chart", componentInstance: ToDoChartComponent },
{ name: "Grid View", componentInstance: GridViewComponent },
- ];
- model:any;
- linesdata:any;
- id:any;
- gadgetsEditdata = {
- donut : '',
- chartlegend: '',
- showlabel : '',
- charturl: '',
- chartparameter : '',
- datastore : '',
- table:'',
- datasource : '',
- charttitle:'',
- id:'',
- fieldName:'',
- chartcolor:'',
- slices:'',
- yAxis:'',
- xAxis:''
+ { name: "Compact Filter", componentInstance: CompactFilterComponent },
+ { name: "Unified Chart", componentInstance: UnifiedChartComponent },
+ ];
+ model: any;
+ linesdata: any;
+ id: any;
+
+ // Add common filter data property
+ commonFilterData = {
+ connection: '',
+ apiUrl: '',
+ filters: [] as any[]
+ };
+
+ // Add common filter column data property
+ commonFilterColumnData: any[] = [];
+
+ gadgetsEditdata = {
+ donut: '',
+ chartlegend: '',
+ showlabel: '',
+ charturl: '',
+ chartparameter: '',
+ datastore: '',
+ table: '',
+ datasource: '',
+ charttitle: '',
+ id: '',
+ fieldName: '',
+ chartcolor: '',
+ slices: '',
+ yAxis: '',
+ xAxis: '',
+ connection: '', // Add connection field
+ chartType: '', // Add chartType field
+ // Drilldown configuration properties (base level)
+ drilldownEnabled: false,
+ drilldownApiUrl: '',
+ // Removed drilldownParameterKey since we're using URL templates
+ drilldownXAxis: '',
+ drilldownYAxis: '',
+ drilldownParameter: '', // Add drilldown parameter property
+ baseFilters: [] as any[], // Add base filters for API
+ drilldownFilters: [] as any[], // Add separate drilldown filters
+ // Multi-layer drilldown configurations
+ drilldownLayers: [] as any[],
+ // Common filter properties
+ commonFilterEnabled: false,
+ commonFilterEnabledDrilldown: false,
+ // Compact filter properties
+ filterKey: '',
+ filterType: 'text',
+ filterLabel: '',
+ filterOptions: [] as string[]
+ };
+
+ // Add sureconnect data property
+ sureconnectData: any[] = [];
+ layerColumnData: { [key: number]: any[] } = {}; // Add layer column data property
+
+ // Add drilldown column data property
+ drilldownColumnData = []; // Add drilldown column data property
+
+ // Add chart types property for dynamic chart selection
+ chartTypes: any[] = [];
-};
constructor(private route: ActivatedRoute,
- private router : Router,
- private dashboardService: Dashboard3Service,
- private toastr:ToastrService,
+ private router: Router,
+ private dashboardService: Dashboard3Service,
+ private toastr: ToastrService,
private _fb: FormBuilder,
private datastoreService: DatastoreService,
- private alertService:AlertsService,) { }
+ private alertService: AlertsService,
+ private sureconnectService: SureconnectService,
+ private filterService: FilterService,
+ private dynamicChartLoader: DynamicChartLoaderService) { } // Add SureconnectService, FilterService, and DynamicChartLoaderService to constructor
- ngOnInit(): void {
+ // Add property to track if coming from dashboard runner
+ fromRunner: boolean = false;
+
+ ngOnInit(): void {
+ // Reset the filter service when the component is initialized
+ this.filterService.resetFilters();
- // Grid options
- this.options = {
- gridType: "fit",
- enableEmptyCellDrop: true,
- emptyCellDropCallback: this.onDrop,
- pushItems: true,
- swap: true,
- pushDirections: { north: true, east: true, south: true, west: true },
- resizable: { enabled: true },
- itemChangeCallback: this.itemChange.bind(this),
- draggable: {
- enabled: true,
- ignoreContent: true,
- dropOverItems: true,
- dragHandleClass: "drag-handler",
- ignoreContentClass: "no-drag",
- },
- displayGrid: "always",
- minCols: 10,
- minRows: 10
- };
- this.getData();
-
- this.editId = this.route.snapshot.params.id;
- console.log(this.editId);
- this.dashboardService.getById(this.editId).subscribe((data)=>{
- console.log("ngOnInit",data);
- this.linesdata = data;
- this.id = data.dashbord1_Line[0].id;
- console.log("this.id ",this.id);
+ // Grid options
+ this.options = {
+ gridType: "fit",
+ enableEmptyCellDrop: true,
+ emptyCellDropCallback: this.onDrop,
+ pushItems: true,
+ swap: true,
+ pushDirections: { north: true, east: true, south: true, west: true },
+ resizable: { enabled: true },
+ itemChangeCallback: this.itemChange.bind(this),
+ draggable: {
+ enabled: true,
+ ignoreContent: true,
+ dropOverItems: true,
+ dragHandleClass: "drag-handler",
+ ignoreContentClass: "no-drag",
},
- (error: any)=>{
+ displayGrid: "always",
+ minCols: 10,
+ minRows: 10,
+ // Add resize callback to handle chart resizing
+ itemResizeCallback: this.itemResize.bind(this)
+ };
+
+ // Check if coming from dashboard runner
+ this.route.queryParams.subscribe(params => {
+ if (params['fromRunner'] === 'true') {
+ this.fromRunner = true;
+ }
+ });
+
+ this.editId = this.route.snapshot.params.id;
+ console.log(this.editId);
+ this.dashboardService.getById(this.editId).subscribe((data) => {
+ console.log("ngOnInit", data);
+ this.linesdata = data;
+ this.id = data.dashbord1_Line[0].id;
+ console.log("this.id ", this.id);
+ },
+ (error: any) => {
}
- );
+ );
- this.entryForm = this._fb.group({
- donut : [null],
- chartlegend: [null],
- showlabel : [null],
- charturl: [null],
- chartparameter : [null],
- datastore:[null],
- table:[null],
- fieldName: [null],
- datasource : [null],
- charttitle:[null],
- id:[null],
- chartcolor:[null],
- slices:[null],
- yAxis:[null],
- xAxis: [null],
- });
+ this.entryForm = this._fb.group({
+ donut: [null],
+ chartlegend: [null],
+ showlabel: [null],
+ charturl: [null],
+ chartparameter: [null],
+ datastore: [null],
+ table: [null],
+ fieldName: [null],
+ datasource: [null],
+ charttitle: [null],
+ id: [null],
+ chartcolor: [null],
+ slices: [null],
+ yAxis: [null],
+ xAxis: [null],
+ connection: [null], // Add connection to form group
+ // Base drilldown configuration form controls
+ drilldownEnabled: [null],
+ drilldownApiUrl: [null],
+ drilldownXAxis: [null],
+ drilldownYAxis: [null],
+ drilldownParameter: [null] // Add drilldown parameter to form group
+ // Note: Dynamic drilldown layers and filters will be handled separately since they're complex objects
+ });
+
+ // Initialize common filter form
+ this.commonFilterForm = this._fb.group({
+ connection: [''],
+ apiUrl: ['']
+ });
+
+ // Load chart types for dynamic chart selection
+ this.loadChartTypesForSelection();
+
+ // Load sureconnect data first, then load dashboard data
+ this.loadSureconnectData();
+
+ // Load common filter data if it exists
+ this.loadCommonFilterData();
+ }
+
+ // Add method to load all chart types for dynamic selection
+ loadChartTypesForSelection() {
+ console.log('Loading chart types for selection');
+ this.dynamicChartLoader.loadActiveChartTypes().subscribe({
+ next: (chartTypes) => {
+ console.log('Loaded chart types for selection:', chartTypes);
+ this.chartTypes = chartTypes;
+
+ // Convert each chart type to a WidgetModel
+ const newWidgets = chartTypes.map(ct => ({
+ name: ct.displayName || ct.name,
+ // identifier: ct.name.toLowerCase().replace(/\s+/g, '_')
+ identifier: `${ct.name.toLowerCase().replace(/\s+/g, '_')}_chart`
+ }));
+
+ // Filter out duplicates by identifier
+ const existingIds = new Set(this.WidgetsMock.map(w => w.identifier));
+ const uniqueNewWidgets = newWidgets.filter(w => !existingIds.has(w.identifier));
+
+ // Append unique new widgets to WidgetsMock
+ this.WidgetsMock = [...this.WidgetsMock, ...uniqueNewWidgets];
+
+ console.log('Updated WidgetsMock:', this.WidgetsMock);
+ },
+ error: (error) => {
+ console.error('Error loading chart types for selection:', error);
}
+ });
+}
- toggleMenu() {
- this.toggle = !this.toggle;
- }
+
+ // Add method to load sureconnect data
+ loadSureconnectData() {
+ this.sureconnectService.getAll().subscribe((data: any[]) => {
+ this.sureconnectData = data;
+ console.log('Sureconnect data loaded:', this.sureconnectData);
+ // Now that sureconnect data is loaded, we can safely load dashboard data
+ this.getData();
+ }, (error) => {
+ console.log('Error loading sureconnect data:', error);
+ // Even if there's an error loading sureconnect data, we still need to load dashboard data
+ this.getData();
+ });
+ }
+
+ // Add method to load common filter data
+ loadCommonFilterData() {
+ // In a real implementation, this would fetch common filter data from the server
+ // For now, we'll initialize with empty values
+ console.log('Loading common filter data');
+ }
+
+ toggleMenu() {
+ this.toggle = !this.toggle;
+ }
- onDrag(event, identifier) {
- console.log("on drag",identifier);
- console.log("on drag ",event);
- event.dataTransfer.setData('widgetIdentifier', identifier);
- }
- datagadgets:any;
- dashboardLine:any;
- dashboardName:any;
- getData() {
- // We get the id in get current router dashboard/:id
- this.route.params.subscribe(params => {
- // + is used to cast string to int
- this.dashboardId = +params["id"];
- // We make a get request with the dashboard id
- this.dashboardService.getById(this.dashboardId).subscribe(dashboard => {
- // We fill our dashboardCollection with returned Observable
- this.dashboardName = dashboard.dashboard_name;
- this.datagadgets = dashboard;
- this.dashboardLine = dashboard.dashbord1_Line;
- //this.dashboardCollection = dashboard.dashbord1_Line.model;
- console.log("this.datagadgets",this.datagadgets);
- console.log("this.dashboardLine",this.dashboardLine);
- this.dashboardCollection =JSON.parse(this.dashboardLine[0].model) ;
- //this.dashboardCollection =this.dashboardLine[0].model ;
- console.log("this.dasboard ",this.dashboardCollection );
- console.log(this.dashboardCollection);
- // We parse serialized Json to generate components on the fly
- this.parseJson(this.dashboardCollection);
- // We copy array without reference
- this.dashboardArray = this.dashboardCollection.dashboard.slice();
- console.log("this.dashboardArray",this.dashboardArray);
- });
- });
-
-
- }
-
- // Super TOKENIZER 2.0 POWERED BY NATCHOIN
- parseJson(dashboardCollection: DashboardModel) {
- // We loop on our dashboardCollection
- dashboardCollection.dashboard.forEach(dashboard => {
- // We loop on our componentCollection
- this.componentCollection.forEach(component => {
- // We check if component key in our dashboardCollection
- // is equal to our component name key in our componentCollection
- if (dashboard.component === component.name) {
- // If it is, we replace our serialized key by our component instance
- dashboard.component = component.componentInstance;
- }
- });
- });
- }
-
- serialize(dashboardCollection) {
- // We loop on our dashboardCollection
- dashboardCollection.forEach(dashboard => {
- // We loop on our componentCollection
- this.componentCollection.forEach(component => {
- // We check if component key in our dashboardCollection
- // is equal to our component name key in our componentCollection
- if (dashboard.name === component.name) {
- dashboard.component = component.name;
- }
- });
- });
- }
-
- itemChange() {
- this.dashboardCollection.dashboard = this.dashboardArray;
- console.log("itemChange this.dashboardCollection.dashboard ",this.dashboardCollection.dashboard);
- console.log("itemChange this.dashboardCollection ",this.dashboardCollection);
- console.log("itemChange this.dashboardCollection type",typeof this.dashboardCollection);
- console.log("itemChange this.dashboardArray ",this.dashboardArray);
- let tmp = JSON.stringify(this.dashboardCollection);
- console.log("temp data",tmp);
- let parsed: DashboardModel = JSON.parse(tmp);
- console.log("parsed data",parsed);
- console.log("let parsed ",typeof parsed);
- this.serialize(parsed.dashboard);
- console.log("item chnage function ", typeof this.dashboardArray);
- //this._ds.updateDashboard(this.dashboardId, parsed).subscribe();
- }
-
- onDrop(ev) {
- const componentType = ev.dataTransfer.getData("widgetIdentifier");
- let maxChartId = this.dashboardArray?.reduce((maxId, item) => Math.max(maxId, item.chartid), 0);
- switch (componentType) {
- case "radar_chart":
- return this.dashboardArray.push({
- cols: 5,
- rows: 6,
- x: 0,
- y: 0,
- chartid:maxChartId + 1,
- component: RadarChartComponent,
- name: "Radar Chart"
+ onDrag(event, identifier) {
+ console.log("on drag", identifier);
+ console.log("on drag ", event);
+ event.dataTransfer.setData('widgetIdentifier', identifier);
+ }
+ datagadgets: any;
+ dashboardLine: any;
+ dashboardName: any;
+ getData() {
+ // Reset the filter service when switching between dashboard records
+ this.filterService.resetFilters();
+
+ // We get the id in get current router dashboard/:id
+ this.route.params.subscribe(params => {
+ // + is used to cast string to int
+ this.dashboardId = +params["id"];
+ // We make a get request with the dashboard id
+ this.dashboardService.getById(this.dashboardId).subscribe(dashboard => {
+ // We fill our dashboardCollection with returned Observable
+ this.dashboardName = dashboard.dashboard_name;
+ this.datagadgets = dashboard;
+ this.dashboardLine = dashboard.dashbord1_Line;
+ //this.dashboardCollection = dashboard.dashbord1_Line.model;
+ console.log("this.datagadgets", this.datagadgets);
+ console.log("this.dashboardLine", this.dashboardLine);
+ this.dashboardCollection = JSON.parse(this.dashboardLine[0].model);
+ //this.dashboardCollection =this.dashboardLine[0].model ;
+ console.log("this.dasboard ", this.dashboardCollection);
+ console.log(this.dashboardCollection);
+ // We parse serialized Json to generate components on the fly
+ this.parseJson(this.dashboardCollection);
+
+ // Set default connections for all gadgets if sureconnect data is available
+ if (this.sureconnectData && this.sureconnectData.length > 0) {
+ this.dashboardCollection.dashboard.forEach(item => {
+ if (!item['connection'] || item['connection'] === '') {
+ item['connection'] = this.sureconnectData[0].id;
+ }
});
- case "line_chart":
- return this.dashboardArray.push({
- cols: 5,
- rows: 7,
- x: 0,
- y: 0,
- chartid:maxChartId + 1,
- component: LineChartComponent,
- name: "Line Chart"
- });
- case "doughnut_chart":
- return this.dashboardArray.push({
- cols: 5,
- rows: 6,
- x: 0,
- y: 0,
- chartid:maxChartId + 1,
- component: DoughnutChartComponent,
- name: "Doughnut Chart"
- });
- case "bar_chart":
- return this.dashboardArray.push({
- cols: 5,
- rows: 6,
- x: 0,
- y: 0,
- chartid:maxChartId + 1,
- component: BarChartComponent,
- name: "Bar Chart"
- });
- case "pie_chart":
- return this.dashboardArray.push({
- cols: 5,
- rows: 6,
- x: 0,
- y: 0,
- chartid:maxChartId + 1,
- component: PieChartComponent,
- name: "Pie Chart"
- });
- case "polar_area_chart":
- return this.dashboardArray.push({
- cols: 5,
- rows: 6,
- x: 0,
- y: 0,
- chartid:maxChartId + 1,
- component: PolarChartComponent,
- name: "Polar Area Chart"
- });
- case "bubble_chart":
- return this.dashboardArray.push({
- cols: 5,
- rows: 6,
- x: 0,
- y: 0,
- chartid:maxChartId + 1,
- component: BubbleChartComponent,
- name: "Bubble Chart"
- });
- case "scatter_chart":
- return this.dashboardArray.push({
- cols: 5,
- rows: 6,
- x: 0,
- y: 0,
- chartid:maxChartId + 1,
- component: ScatterChartComponent,
- name: "Scatter Chart"
- });
- case "dynamic_chart":
- return this.dashboardArray.push({
- cols: 5,
- rows: 6,
- x: 0,
- y: 0,
- chartid:maxChartId + 1,
- component: DynamicChartComponent,
- name: "Dynamic Chart"
- });
- case "financial_chart":
- return this.dashboardArray.push({
- cols: 5,
- rows: 6,
- x: 0,
- y: 0,
- chartid:maxChartId + 1,
- component: FinancialChartComponent,
- name: "Financial Chart"
- });
- case "to_do_chart":
- return this.dashboardArray.push({
- cols: 5,
- rows: 5,
- x: 0,
- y: 0,
- chartid:maxChartId + 1,
- component: ToDoChartComponent,
- name: "To Do Chart"
- });
- case "grid_view":
- return this.dashboardArray.push({
- cols: 5,
- rows: 5,
- x: 0,
- y: 0,
- chartid:maxChartId + 1,
- component: GridViewComponent,
- name: "Grid View"
- });
+ }
+
+ // We copy array without reference
+ this.dashboardArray = this.dashboardCollection.dashboard.slice();
+ console.log("this.dashboardArray", this.dashboardArray);
+ });
+ });
+ }
+
+ // Super TOKENIZER 2.0 POWERED BY NATCHOIN
+ parseJson(dashboardCollection: DashboardModel) {
+ // We loop on our dashboardCollection
+ dashboardCollection.dashboard.forEach(dashboard => {
+ // We loop on our componentCollection
+ this.componentCollection.forEach(component => {
+ // We check if component key in our dashboardCollection
+ // is equal to our component name key in our componentCollection
+ if (dashboard.component === component.name) {
+ // If it is, we replace our serialized key by our component instance
+ dashboard.component = component.componentInstance;
+ }
+ });
+
+ // Map chart names to unified chart types
+ const chartTypeMap = {
+ 'Radar Chart': 'radar',
+ 'Line Chart': 'line',
+ 'Doughnut Chart': 'doughnut',
+ 'Bar Chart': 'bar',
+ 'Pie Chart': 'pie',
+ 'Polar Area Chart': 'polar',
+ 'Bubble Chart': 'bubble',
+ 'Scatter Chart': 'scatter',
+ 'Dynamic Chart': 'line',
+ 'Financial Chart': 'line'
+ };
+
+ // If this is a chart, set the chartType property
+ if (chartTypeMap.hasOwnProperty(dashboard.name)) {
+ dashboard.chartType = chartTypeMap[dashboard.name];
+ // Keep the original name instead of changing it to "Unified Chart"
+ // dashboard.name = "Unified Chart";
+ }
+
+ // Ensure compact filter configuration properties are properly initialized
+ if (dashboard.component === 'Compact Filter' || dashboard.name === 'Compact Filter') {
+ // Make sure all compact filter properties exist
+ if (dashboard.filterKey === undefined) dashboard.filterKey = '';
+ if (dashboard.filterType === undefined) dashboard.filterType = 'text';
+ if (dashboard.filterLabel === undefined) dashboard.filterLabel = '';
+ if (dashboard.filterOptions === undefined) dashboard.filterOptions = [];
+ // table and connection properties should already exist for all components
+ }
+ });
+ }
+
+ serialize(dashboardCollection) {
+ // We loop on our dashboardCollection
+ dashboardCollection.forEach(dashboard => {
+ // We loop on our componentCollection
+ this.componentCollection.forEach(component => {
+ // We check if component key in our dashboardCollection
+ // is equal to our component name key in our componentCollection
+ if (dashboard.name === component.name) {
+ dashboard.component = component.name;
+ }
+ });
+
+ // Map unified chart types back to chart names for serialization
+ const chartNameMap = {
+ 'radar': 'Radar Chart',
+ 'line': 'Line Chart',
+ 'doughnut': 'Doughnut Chart',
+ 'bar': 'Bar Chart',
+ 'pie': 'Pie Chart',
+ 'polar': 'Polar Area Chart',
+ 'bubble': 'Bubble Chart',
+ 'scatter': 'Scatter Chart'
+ // Removed hardcoded heatmap entry to make it fully dynamic
+ };
+
+ // If this is a unified chart, set the name back to the appropriate chart name
+ if (dashboard.name === 'Unified Chart' && dashboard.chartType && chartNameMap.hasOwnProperty(dashboard.chartType)) {
+ dashboard.name = chartNameMap[dashboard.chartType];
+ }
+ // Also handle the case where the chart already has the correct name
+ else if (dashboard.chartType && chartNameMap.hasOwnProperty(dashboard.chartType) &&
+ dashboard.name === chartNameMap[dashboard.chartType]) {
+ // The name is already correct, no need to change it
+ dashboard.component = "Unified Chart";
+ }
+
+ // Ensure compact filter configuration properties are preserved
+ if (dashboard.name === 'Compact Filter') {
+ // Make sure all compact filter properties exist
+ if (dashboard.filterKey === undefined) dashboard.filterKey = '';
+ if (dashboard.filterType === undefined) dashboard.filterType = 'text';
+ if (dashboard.filterLabel === undefined) dashboard.filterLabel = '';
+ if (dashboard.filterOptions === undefined) dashboard.filterOptions = [];
+ // table and connection properties should already exist for all components
+ }
+ });
+ }
+
+ // Add method to get available fields for a filter dropdown (excluding already selected fields)
+ getAvailableFields(filters: any[], currentIndex: number, allFields: string[]): string[] {
+ if (!filters || !allFields) {
+ return allFields || [];
+ }
+
+ // Get all selected fields except the current one
+ const selectedFields = filters
+ .filter((filter, index) => filter.field && index !== currentIndex)
+ .map(filter => filter.field);
+
+ // Return fields that haven't been selected yet
+ return allFields.filter(field => !selectedFields.includes(field));
+ }
+
+ itemChange() {
+ this.dashboardCollection.dashboard = this.dashboardArray;
+ console.log("itemChange this.dashboardCollection.dashboard ", this.dashboardCollection.dashboard);
+ console.log("itemChange this.dashboardCollection ", this.dashboardCollection);
+ console.log("itemChange this.dashboardCollection type", typeof this.dashboardCollection);
+ console.log("itemChange this.dashboardArray ", this.dashboardArray);
+ let tmp = JSON.stringify(this.dashboardCollection);
+ console.log("temp data", tmp);
+ let parsed: DashboardModel = JSON.parse(tmp);
+ console.log("parsed data", parsed);
+ console.log("let parsed ", typeof parsed);
+ this.serialize(parsed.dashboard);
+ console.log("item chnage function ", typeof this.dashboardArray);
+ //this._ds.updateDashboard(this.dashboardId, parsed).subscribe();
+ }
+
+ onDrop = (ev) => {
+ console.log("on drop event ", ev);
+ const componentType = ev.dataTransfer.getData("widgetIdentifier");
+ // Safely calculate maxChartId, handling cases where chartid might be NaN or missing
+ console.log("on drop ", componentType);
+ let maxChartId = 0;
+ if (this.dashboardArray && this.dashboardArray.length > 0) {
+ const validChartIds = this.dashboardArray
+ .map(item => item.chartid)
+ .filter(chartid => typeof chartid === 'number' && !isNaN(chartid));
+
+ if (validChartIds.length > 0) {
+ maxChartId = Math.max(...validChartIds);
}
}
- removeItem(item) {
- this.dashboardArray.splice(
- this.dashboardArray.indexOf(item),
- 1
- );
- this.itemChange();
+ switch (componentType) {
+ // Handle all chart types by converting them to unified charts
+ case "radar_chart":
+ // Use dynamic chart creation for all chart types
+ return this.createDynamicChart('radar', maxChartId);
+ case "line_chart":
+ return this.createDynamicChart('line', maxChartId);
+ case "doughnut_chart":
+ return this.createDynamicChart('doughnut', maxChartId);
+ case "bar_chart":
+ return this.createDynamicChart('bar', maxChartId);
+ case "pie_chart":
+ return this.createDynamicChart('pie', maxChartId);
+ case "polar_area_chart":
+ return this.createDynamicChart('polar', maxChartId);
+ case "bubble_chart":
+ return this.createDynamicChart('bubble', maxChartId);
+ case "scatter_chart":
+ return this.createDynamicChart('scatter', maxChartId);
+ case "dynamic_chart":
+ return this.createDynamicChart('line', maxChartId); // Default to line for dynamic chart
+ case "financial_chart":
+ return this.createDynamicChart('line', maxChartId); // Default to line for financial chart
+ case "to_do_chart":
+ return this.dashboardArray.push({
+ cols: 5,
+ rows: 5,
+ x: 0,
+ y: 0,
+ chartid: maxChartId + 1,
+ component: ToDoChartComponent,
+ name: "To Do Chart"
+ });
+ case "common_filter":
+ return this.dashboardArray.push({
+ cols: 10,
+ rows: 3,
+ x: 0,
+ y: 0,
+ chartid: maxChartId + 1,
+ component: CommonFilterComponent,
+ name: "Common Filter"
+ });
+ case "compact_filter":
+ return this.dashboardArray.push({
+ cols: 3,
+ rows: 2,
+ x: 0,
+ y: 0,
+ chartid: maxChartId + 1,
+ component: CompactFilterComponent,
+ name: "Compact Filter",
+ // Add default configuration for compact filter
+ filterKey: '',
+ filterType: 'text',
+ filterLabel: '',
+ filterOptions: []
+ });
+ case "grid_view":
+ return this.dashboardArray.push({
+ cols: 5,
+ rows: 5,
+ x: 0,
+ y: 0,
+ chartid: maxChartId + 1,
+ component: GridViewComponent,
+ name: "Grid View"
+ });
+ case "unified_chart":
+ return this.createDynamicChart('bar', maxChartId); // Default to bar for unified chart
+ default:
+ // Handle any other chart types dynamically
+ // Extract chart type name from identifier (e.g., "heatmap_chart" -> "heatmap")
+ const chartTypeName = componentType.replace('_chart', '');
+ console.log('Creating dynamic chart of type:', chartTypeName);
+ console.log('Display name for chart:', this.getChartDisplayName(chartTypeName));
+
+ // Use dynamic chart creation for all chart types
+ return this.createDynamicChart(chartTypeName, maxChartId);
}
+ }
+ removeItem(item) {
+ this.dashboardArray.splice(
+ this.dashboardArray.indexOf(item),
+ 1
+ );
+ this.itemChange();
+ }
- changedOptions() {
- this.options.api.optionsChanged();
+ changedOptions() {
+ this.options.api.optionsChanged();
+ }
+
+ modelid: number;
+ // Update the editGadget method to initialize filter properties
+ editGadget(item) {
+ // If coming from dashboard runner, skip showing the config modal
+ if (this.fromRunner) {
+ console.log('Coming from dashboard runner, skipping config modal');
+ return;
}
-
- modelid:number ;
- editGadget(item)
- {
- this.modeledit = true;
- this.modelid = item.chartid;
- console.log(this.modelid);
- this.gadgetsEditdata = item;
- this.gadgetsEditdata.fieldName = item.name;
- if(item.showlabel === undefined){ item.showlabel = true; }
- if(item.chartcolor === undefined ){ item.chartcolor = true;}
- if(item.chartlegend === undefined){ item.chartlegend = true; }
- this.getStores();
- if(item.datastore !== undefined || '' || null){
+
+ this.modeledit = true;
+ this.modelid = item.chartid;
+ console.log(this.modelid);
+ this.gadgetsEditdata = item;
+ this.gadgetsEditdata.fieldName = item.name;
+ if (item.showlabel === undefined) { item.showlabel = true; }
+ if (item.chartcolor === undefined) { item.chartcolor = true; }
+ if (item.chartlegend === undefined) { item.chartlegend = true; }
+ // Initialize common filter property if not present
+ if (item['commonFilterEnabled'] === undefined) {
+ this.gadgetsEditdata['commonFilterEnabled'] = false;
+ }
+ // Initialize drilldown common filter property if not present
+ if (item['commonFilterEnabledDrilldown'] === undefined) {
+ this.gadgetsEditdata['commonFilterEnabledDrilldown'] = false;
+ }
+ // Initialize compact filter properties if not present
+ if (item['filterKey'] === undefined) {
+ this.gadgetsEditdata['filterKey'] = '';
+ }
+ if (item['filterType'] === undefined) {
+ this.gadgetsEditdata['filterType'] = 'text';
+ }
+ if (item['filterLabel'] === undefined) {
+ this.gadgetsEditdata['filterLabel'] = '';
+ }
+ if (item['filterOptions'] === undefined) {
+ this.gadgetsEditdata['filterOptions'] = [];
+ }
+ // Initialize chartType property if not present (for unified chart)
+ if (item['chartType'] === undefined) {
+ this.gadgetsEditdata['chartType'] = 'bar';
+ }
+
+ // Initialize filterOptionsString for compact filter
+ if (item.name === 'Compact Filter') {
+ this.filterOptionsString = this.gadgetsEditdata['filterOptions'].join(', ');
+ // Load available keys when editing a compact filter
+ if (this.gadgetsEditdata['table']) {
+ this.loadAvailableKeys(this.gadgetsEditdata['table'], this.gadgetsEditdata['connection']);
+ }
+ } else {
+ this.filterOptionsString = '';
+ }
+
+ // Initialize base filters with type and options if not present
+ if (item['baseFilters'] === undefined) {
+ this.gadgetsEditdata['baseFilters'] = [];
+ } else {
+ // Ensure each base filter has type and options properties
+ this.gadgetsEditdata['baseFilters'] = this.gadgetsEditdata['baseFilters'].map(filter => ({
+ field: filter.field || '',
+ value: filter.value || '',
+ type: filter.type || 'text',
+ options: filter.options || '',
+ availableValues: filter.availableValues || ''
+ }));
+ }
+
+ // Initialize drilldown filters with type and options if not present
+ if (item['drilldownFilters'] === undefined) {
+ this.gadgetsEditdata['drilldownFilters'] = [];
+ } else {
+ // Ensure each drilldown filter has type and options properties
+ this.gadgetsEditdata['drilldownFilters'] = this.gadgetsEditdata['drilldownFilters'].map(filter => ({
+ field: filter.field || '',
+ value: filter.value || '',
+ type: filter.type || 'text',
+ options: filter.options || '',
+ availableValues: filter.availableValues || ''
+ }));
+ }
+
+ // Initialize drilldown layers with proper filter structure if not present
+ if (item['drilldownLayers'] === undefined) {
+ this.gadgetsEditdata['drilldownLayers'] = [];
+ } else {
+ // Ensure each layer has proper filter structure
+ this.gadgetsEditdata['drilldownLayers'] = this.gadgetsEditdata['drilldownLayers'].map(layer => {
+ // Initialize parameter if not present
+ if (layer['parameter'] === undefined) {
+ layer['parameter'] = '';
+ }
+ // Initialize filters if not present
+ if (layer['filters'] === undefined) {
+ layer['filters'] = [];
+ } else {
+ // Ensure each layer filter has type and options properties
+ layer['filters'] = layer['filters'].map(filter => ({
+ field: filter.field || '',
+ value: filter.value || '',
+ type: filter.type || 'text',
+ options: filter.options || '',
+ availableValues: filter.availableValues || ''
+ }));
+ }
+ // Initialize common filter property for layer if not present
+ if (layer['commonFilterEnabled'] === undefined) {
+ layer['commonFilterEnabled'] = false;
+ }
+ return layer;
+ });
+ }
+
+ this.getStores();
+
+ // Set default connection if none is set and we have connections
+ if ((!item['connection'] || item['connection'] === '') && this.sureconnectData && this.sureconnectData.length > 0) {
+ this.gadgetsEditdata['connection'] = this.sureconnectData[0].id;
+ // Also update the form control
+ this.entryForm.patchValue({ connection: this.sureconnectData[0].id });
+ }
+
+ // Initialize base drilldown properties if not present
+ if (item['drilldownEnabled'] === undefined) {
+ this.gadgetsEditdata['drilldownEnabled'] = false;
+ }
+ if (item['drilldownApiUrl'] === undefined) {
+ this.gadgetsEditdata['drilldownApiUrl'] = '';
+ }
+ // Removed drilldownParameterKey initialization
+ if (item['drilldownXAxis'] === undefined) {
+ this.gadgetsEditdata['drilldownXAxis'] = '';
+ }
+ if (item['drilldownYAxis'] === undefined) {
+ this.gadgetsEditdata['drilldownYAxis'] = '';
+ }
+ if (item['drilldownParameter'] === undefined) {
+ this.gadgetsEditdata['drilldownParameter'] = '';
+ }
+
+ // Reset drilldown column data
+ this.drilldownColumnData = [];
+
+ // If drilldown is enabled and we have a drilldown API URL, fetch the drilldown column data
+ if (this.gadgetsEditdata.drilldownEnabled && this.gadgetsEditdata.drilldownApiUrl) {
+ this.refreshBaseDrilldownColumns();
+ }
+
+ // Check if we have either datastore or table to fetch columns
+ if ((item.datastore !== undefined && item.datastore !== '' && item.datastore !== null) ||
+ (item.table !== undefined && item.table !== '' && item.table !== null)) {
const datastore = item.datastore;
- this.getTables(datastore);
const table = item.table;
- this.getColumns(datastore,table);
- console.log(item.yAxis);
- if(isArray(item.yAxis)){
- this.selectedyAxis = item.yAxis;
- console.log( this.selectedyAxis);
+
+ // Fetch tables if datastore is available
+ if (datastore) {
+ this.getTables(datastore);
}
- }else{
+
+ // Fetch columns if table is available
+ if (table) {
+ this.getColumns(datastore, table);
+ }
+
+ console.log(item.yAxis);
+ // Set selectedyAxis regardless of whether it's an array or string
+ if (item.yAxis !== undefined && item.yAxis !== '' && item.yAxis !== null) {
+ if (isArray(item.yAxis)) {
+ this.selectedyAxis = item.yAxis;
+ } else {
+ // For single yAxis values, convert to array
+ this.selectedyAxis = [item.yAxis];
+ }
+ console.log(this.selectedyAxis);
+ } else {
this.selectedyAxis = [];
}
- console.log(item);
+ } else {
+ this.selectedyAxis = [];
}
+ console.log(item);
+ }
- dashbord1_Line = {
- //model:JSON.stringify(this.da),
- model:''
- }
+ dashbord1_Line = {
+ //model:JSON.stringify(this.da),
+ model: ''
+ }
- UpdateLine()
- {
+ UpdateLine() {
console.log('Add button clicked.......');
console.log(this.dashboardArray);
console.log(this.dashboardCollection);
console.log(typeof this.dashboardCollection);
console.log(this.id);
- //this.dashbord1_Line.model = JSON.stringify(this.dashboardCollection);
+ //this.dashbord1_Line.model = JSON.stringify(this.dashboardCollection);
- //https://www.w3schools.com/js/tryit.asp?filename=tryjson_stringify_function_tostring
+ //https://www.w3schools.com/js/tryit.asp?filename=tryjson_stringify_function_tostring
-let cmp=this.dashboardCollection.dashboard.forEach(dashboard=>{
- this.componentCollection.forEach(component=>{
- if (dashboard.name === component.name) {
- dashboard.component = component.name;
- } })
-})
-console.log(cmp);
+ // First serialize the dashboard collection to ensure component names are properly set
+ this.serialize(this.dashboardCollection.dashboard);
+
+ let cmp = this.dashboardCollection.dashboard.forEach(dashboard => {
+ this.componentCollection.forEach(component => {
+ if (dashboard.name === component.name) {
+ dashboard.component = component.name;
+ }
+ })
+ })
+ console.log(cmp);
let tmp = JSON.stringify(this.dashboardCollection);
- // var merged = this.dashboardArray.reduce((current, value, index) => {
- // if(index > 0)
- // current += ',';
+ // var merged = this.dashboardArray.reduce((current, value, index) => {
+ // if(index > 0)
+ // current += ',';
- // return current + value.component;
- // }, '');
+ // return current + value.component;
+ // }, '');
- //console.log(merged);
- console.log("temp data",typeof tmp);
- console.log(tmp);
- let parsed= JSON.parse(tmp);
- this.serialize(parsed.dashboard);
+ //console.log(merged);
+ console.log("temp data", typeof tmp);
+ console.log(tmp);
this.dashbord1_Line.model = tmp;
- // let obj = this.dashboardCollection;
- // obj[1].component = obj[1].component.toString();
- // let myJSON = JSON.stringify(obj);
- // this.dashbord1_Line.model = myJSON;
+ // let obj = this.dashboardCollection;
+ // obj[1].component = obj[1].component.toString();
+ // let myJSON = JSON.stringify(obj);
+ // this.dashbord1_Line.model = myJSON;
- console.log("line data in addgadget ",this.dashbord1_Line);
- console.log("line data in addgadget type ",typeof this.dashbord1_Line);
- console.log("line model data ",this.dashbord1_Line.model);
- console.log("line model data type",typeof this.dashbord1_Line.model);
- this.dashboardService.UpdateLineData(this.id,this.dashbord1_Line).subscribe(
- (data: any)=>{
- console.log('Updation Successful...');
- this.ngOnInit();
- console.log(data);
- this.router.navigate(["../../all"], { relativeTo: this.route })
- }
- );
- // if (data) {
- // this.toastr.success('Updated successfully');
- // }
- }
+ console.log("line data in addgadget ", this.dashbord1_Line);
+ console.log("line data in addgadget type ", typeof this.dashbord1_Line);
+ console.log("line model data ", this.dashbord1_Line.model);
+ console.log("line model data type", typeof this.dashbord1_Line.model);
+ this.dashboardService.UpdateLineData(this.id, this.dashbord1_Line).subscribe(
+ (data: any) => {
+ console.log('Updation Successful...');
+ this.ngOnInit();
+ console.log(data);
+ this.router.navigate(["../../all"], { relativeTo: this.route })
+ }
+ );
+ // if (data) {
+ // this.toastr.success('Updated successfully');
+ // }
+ }
- onSubmit(id)
- {
+ // Update the onSubmit method to properly save filter data
+ onSubmit(id) {
console.log(id);
- if (!isNullArray(this.selectedyAxis)) {
- console.log("get y-axis array", this.selectedyAxis);
+
+ // Check if ID is valid, including handling NaN
+ if (id === null || id === undefined || isNaN(id)) {
+ console.warn('Chart ID is null, undefined, or NaN, using modelid instead:', this.modelid);
+ id = this.modelid;
+ }
+
+ // Ensure we have a valid numeric ID
+ const numId = typeof id === 'number' ? id : parseInt(id, 10);
+ if (isNaN(numId)) {
+ console.error('Unable to determine valid chart ID, aborting onSubmit');
+ return;
+ }
+
+ // Handle both array and string yAxis values
+ if (this.selectedyAxis !== undefined && this.selectedyAxis !== null &&
+ ((Array.isArray(this.selectedyAxis) && this.selectedyAxis.length > 0) ||
+ (typeof this.selectedyAxis === 'string' && this.selectedyAxis !== ''))) {
+ console.log("get y-axis", this.selectedyAxis);
this.entryForm.patchValue({ yAxis: this.selectedyAxis });
}
let formdata = this.entryForm.value;
- let num = id;
+ let num = numId;
console.log(this.entryForm.value);
- this.dashboardCollection.dashboard = this.dashboardCollection.dashboard.map(item => {
- if(item.chartid == num)
- {
+ this.dashboardCollection.dashboard = this.dashboardCollection.dashboard.map(item => {
+ if (item.chartid == num) {
+ // Preserve the component reference
+ const componentRef = item.component;
+
//item["product_id"] = "thisistest";
- const xyz = {...item,...formdata}
+ const xyz = { ...item, ...formdata }
+
+ // Restore the component reference
+ xyz.component = componentRef;
+
+ // Explicitly ensure drilldown properties are preserved
+ xyz.drilldownEnabled = this.gadgetsEditdata.drilldownEnabled;
+ xyz.drilldownApiUrl = this.gadgetsEditdata.drilldownApiUrl;
+ xyz.drilldownXAxis = this.gadgetsEditdata.drilldownXAxis;
+ xyz.drilldownYAxis = this.gadgetsEditdata.drilldownYAxis;
+ xyz.drilldownParameter = this.gadgetsEditdata.drilldownParameter;
+ xyz.baseFilters = this.gadgetsEditdata.baseFilters; // Add base filters
+ xyz.drilldownFilters = this.gadgetsEditdata.drilldownFilters; // Add drilldown filters
+ xyz.drilldownLayers = this.gadgetsEditdata.drilldownLayers;
+ xyz.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property
+
+ // For compact filter, preserve filter configuration properties
+ if (item.name === 'Compact Filter') {
+ xyz.filterKey = this.gadgetsEditdata.filterKey || '';
+ xyz.filterType = this.gadgetsEditdata.filterType || 'text';
+ xyz.filterLabel = this.gadgetsEditdata.filterLabel || '';
+ // Convert filterOptionsString to array
+ if (this.gadgetsEditdata.fieldName === 'Compact Filter') {
+ xyz.filterOptions = this.filterOptionsString.split(',').map(opt => opt.trim()).filter(opt => opt);
+ } else {
+ xyz.filterOptions = this.gadgetsEditdata.filterOptions || [];
+ }
+ xyz.table = this.gadgetsEditdata.table || '';
+ xyz.connection = this.gadgetsEditdata.connection || undefined;
+ }
+
+ // For unified chart, preserve chart configuration properties
+ if (item.name === 'Unified Chart') {
+ xyz.chartType = this.gadgetsEditdata.chartType || 'bar';
+ }
+
console.log(xyz);
return xyz;
- }
- return item
- });
- console.log(this.dashboardCollection.dashboard);
- this.modeledit = false;
-
- // this.entryForm.reset();
-
- }
- goBack(){
- this.router.navigate(["../../all"], { relativeTo: this.route })
- }
-
- onSchedule(){
- this.router.navigate(['../../schedule/'+ this.editId],{relativeTo:this.route});
- }
-
+ }
+ return item
+ });
+ console.log('dashboard collection ', this.dashboardCollection.dashboard);
- ///////
- storedata;
- getStores(){
- this.datastoreService.getAll().subscribe((data) => {
- console.log(data);
- this.storedata = data;
- },(error) => {
- console.log(error);
- });
- }
-
- selectedStoreId;
- storename(val){
- console.log(val);
- this.selectedStoreId = val;
- this.getTables(this.selectedStoreId);
- }
-
- TableData;
- getTables(id){
- this.alertService.getTablefromstore(id).subscribe(gateway =>{
- console.log(gateway);
- this.TableData = gateway;
- },(error)=>{
- console.log(error);
- });
- }
-
- tablename(val){
- console.log(val);
- this.getColumns(this.selectedStoreId,val);
+ // Force gridster to refresh by triggering change detection
+ if (this.options && this.options.api) {
+ this.options.api.optionsChanged();
}
- selectedyAxis;
- columnData;
- getColumns(id,table){
- this.alertService.getColumnfromurl(table).subscribe(data =>{
- console.log(data);
- this.columnData = data;
- },(error)=>{
- console.log(error);
+
+ // Trigger change detection manually
+ // This is a workaround to ensure the gridster re-renders the components
+ setTimeout(() => {
+ // Force a refresh by temporarily setting dashboardArray to empty and then back
+ const tempArray = [...this.dashboardArray];
+ this.dashboardArray = [];
+ setTimeout(() => {
+ this.dashboardArray = tempArray;
+ }, 0);
+ }, 0);
+
+ this.modeledit = false;
+
+ // this.entryForm.reset();
+
+ }
+
+ /**
+ * Extract only the relevant chart configuration properties to pass to chart components
+ * This prevents errors when trying to set properties that don't exist on the components
+ */
+ getChartInputs(item: any): any {
+ // For CompactFilterComponent, pass only filter configuration properties
+ if (item.name === 'Compact Filter') {
+ const filterInputs = {
+ filterKey: item['filterKey'] || '',
+ filterType: item['filterType'] || 'text',
+ filterLabel: item['filterLabel'] || '',
+ filterOptions: item['filterOptions'] || [],
+ apiUrl: item['table'] || '', // Use table as API URL
+ connectionId: item['connection'] ? parseInt(item['connection'], 10) : undefined
+ };
+
+ // Preserve configuration in the item itself
+ item['filterKey'] = filterInputs['filterKey'];
+ item['filterType'] = filterInputs['filterType'];
+ item['filterLabel'] = filterInputs['filterLabel'];
+ item['filterOptions'] = filterInputs['filterOptions'];
+ item['table'] = filterInputs['apiUrl'];
+ item['connection'] = item['connection'];
+
+ // Remove undefined properties to avoid passing unnecessary data
+ Object.keys(filterInputs).forEach(key => {
+ if (filterInputs[key] === undefined) {
+ delete filterInputs[key];
+ }
+ });
+
+ return filterInputs;
+ }
+
+ // For CommonFilterComponent, pass only filter-related properties
+ if (item.component && item.component.name === 'CommonFilterComponent') {
+ const commonFilterInputs = {
+ baseFilters: item['baseFilters'] || [],
+ drilldownFilters: item['drilldownFilters'] || [],
+ drilldownLayers: item['drilldownLayers'] || [],
+ fieldName: item['name'] || '',
+ connection: item['connection'] || undefined
+ };
+
+ // Remove undefined properties to avoid passing unnecessary data
+ Object.keys(commonFilterInputs).forEach(key => {
+ if (commonFilterInputs[key] === undefined) {
+ delete commonFilterInputs[key];
+ }
+ });
+
+ return commonFilterInputs;
+ }
+
+ // For UnifiedChartComponent, pass chart properties with chartType
+ // Check if the component is UnifiedChartComponent dynamically
+ if (item.component === UnifiedChartComponent ||
+ (item.component && item.component.name === 'UnifiedChartComponent') ||
+ item.name === 'Unified Chart') {
+ const unifiedChartInputs = {
+ chartType: item.chartType || 'bar',
+ xAxis: item.xAxis,
+ yAxis: item.yAxis,
+ table: item.table,
+ datastore: item.datastore,
+ charttitle: item.charttitle,
+ chartlegend: item.chartlegend,
+ showlabel: item.showlabel,
+ chartcolor: item.chartcolor,
+ slices: item.slices,
+ donut: item.donut,
+ charturl: item.charturl,
+ chartparameter: item.chartparameter,
+ datasource: item.datasource,
+ fieldName: item.name, // Using item.name as fieldName
+ connection: item['connection'], // Add connection field using bracket notation
+ // Base drilldown configuration properties
+ drilldownEnabled: item['drilldownEnabled'],
+ drilldownApiUrl: item['drilldownApiUrl'],
+ // Removed drilldownParameterKey since we're using URL templates
+ drilldownXAxis: item['drilldownXAxis'],
+ drilldownYAxis: item['drilldownYAxis'],
+ drilldownParameter: item['drilldownParameter'], // Add drilldown parameter
+ baseFilters: item['baseFilters'] || [], // Add base filters
+ drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters
+ // Multi-layer drilldown configurations
+ drilldownLayers: item['drilldownLayers'] || []
+ };
+
+ // Remove undefined properties to avoid passing unnecessary data
+ Object.keys(unifiedChartInputs).forEach(key => {
+ if (unifiedChartInputs[key] === undefined) {
+ delete unifiedChartInputs[key];
+ }
+ });
+
+ return unifiedChartInputs;
+ }
+
+ // For GridViewComponent, pass chart properties with drilldown support
+ if (item.component && item.component.name === 'GridViewComponent') {
+ const gridInputs = {
+ xAxis: item.xAxis,
+ yAxis: item.yAxis,
+ table: item.table,
+ datastore: item.datastore,
+ charttitle: item.charttitle,
+ chartlegend: item.chartlegend,
+ showlabel: item.showlabel,
+ chartcolor: item.chartcolor,
+ slices: item.slices,
+ donut: item.donut,
+ charturl: item.charturl,
+ chartparameter: item.chartparameter,
+ datasource: item.datasource,
+ fieldName: item.name, // Using item.name as fieldName
+ connection: item['connection'], // Add connection field using bracket notation
+ // Base drilldown configuration properties
+ drilldownEnabled: item['drilldownEnabled'],
+ drilldownApiUrl: item['drilldownApiUrl'],
+ // Removed drilldownParameterKey since we're using URL templates
+ drilldownXAxis: item['drilldownXAxis'],
+ drilldownYAxis: item['drilldownYAxis'],
+ drilldownParameter: item['drilldownParameter'], // Add drilldown parameter
+ baseFilters: item['baseFilters'] || [], // Add base filters
+ drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters
+ // Multi-layer drilldown configurations
+ drilldownLayers: item['drilldownLayers'] || []
+ };
+
+ // Remove undefined properties to avoid passing unnecessary data
+ Object.keys(gridInputs).forEach(key => {
+ if (gridInputs[key] === undefined) {
+ delete gridInputs[key];
+ }
+ });
+
+ return gridInputs;
+ }
+
+ // For all other chart components, pass chart-specific properties
+ const chartInputs = {
+ xAxis: item.xAxis,
+ yAxis: item.yAxis,
+ table: item.table,
+ datastore: item.datastore,
+ charttitle: item.charttitle,
+ chartlegend: item.chartlegend,
+ showlabel: item.showlabel,
+ chartcolor: item.chartcolor,
+ slices: item.slices,
+ donut: item.donut,
+ charturl: item.charturl,
+ chartparameter: item.chartparameter,
+ datasource: item.datasource,
+ fieldName: item.name, // Using item.name as fieldName
+ connection: item['connection'], // Add connection field using bracket notation
+ // Base drilldown configuration properties
+ drilldownEnabled: item['drilldownEnabled'],
+ drilldownApiUrl: item['drilldownApiUrl'],
+ // Removed drilldownParameterKey since we're using URL templates
+ drilldownXAxis: item['drilldownXAxis'],
+ drilldownYAxis: item['drilldownYAxis'],
+ drilldownParameter: item['drilldownParameter'], // Add drilldown parameter
+ baseFilters: item['baseFilters'] || [], // Add base filters with type information
+ drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters with type information
+ // Multi-layer drilldown configurations
+ drilldownLayers: item['drilldownLayers'] || []
+ };
+
+ // Remove undefined properties to avoid passing unnecessary data
+ Object.keys(chartInputs).forEach(key => {
+ if (chartInputs[key] === undefined) {
+ delete chartInputs[key];
+ }
+ });
+
+ return chartInputs;
+ }
+
+ // Update the applyChanges method to properly save filter data
+ applyChanges(id) {
+ console.log('Apply changes for chart ID:', id);
+
+ // Check if ID is valid, including handling NaN
+ if (id === null || id === undefined || isNaN(id)) {
+ console.warn('Chart ID is null, undefined, or NaN, using modelid instead:', this.modelid);
+ id = this.modelid;
+ }
+
+ // Ensure we have a valid numeric ID
+ const numId = typeof id === 'number' ? id : parseInt(id, 10);
+ if (isNaN(numId)) {
+ console.error('Unable to determine valid chart ID, aborting applyChanges');
+ return;
+ }
+
+ // Update the form with selected Y-axis values
+ // Handle both array and string yAxis values
+ if (this.selectedyAxis !== undefined && this.selectedyAxis !== null &&
+ ((Array.isArray(this.selectedyAxis) && this.selectedyAxis.length > 0) ||
+ (typeof this.selectedyAxis === 'string' && this.selectedyAxis !== ''))) {
+ console.log("get y-axis", this.selectedyAxis);
+ this.entryForm.patchValue({ yAxis: this.selectedyAxis });
+ }
+
+ // Get form data
+ let formdata = this.entryForm.value;
+ let num = id;
+ console.log('Form data:', this.entryForm.value);
+
+ // Update the dashboard collection with the new configuration
+ this.dashboardCollection.dashboard = this.dashboardCollection.dashboard.map(item => {
+ if (item.chartid == num) {
+ // Preserve the component reference
+ const componentRef = item.component;
+
+ // Merge the existing item with the new form data
+ const updatedItem = { ...item, ...formdata }
+
+ // Restore the component reference
+ updatedItem.component = componentRef;
+
+ // Explicitly ensure drilldown properties are preserved
+ updatedItem.drilldownEnabled = this.gadgetsEditdata.drilldownEnabled;
+ updatedItem.drilldownApiUrl = this.gadgetsEditdata.drilldownApiUrl;
+ updatedItem.drilldownXAxis = this.gadgetsEditdata.drilldownXAxis;
+ updatedItem.drilldownYAxis = this.gadgetsEditdata.drilldownYAxis;
+ updatedItem.drilldownParameter = this.gadgetsEditdata.drilldownParameter;
+ updatedItem.baseFilters = this.gadgetsEditdata.baseFilters; // Add base filters
+ updatedItem.drilldownFilters = this.gadgetsEditdata.drilldownFilters; // Add drilldown filters
+ updatedItem.drilldownLayers = this.gadgetsEditdata.drilldownLayers;
+ updatedItem.commonFilterEnabled = this.gadgetsEditdata.commonFilterEnabled; // Add common filter property
+ updatedItem.commonFilterEnabledDrilldown = this.gadgetsEditdata.commonFilterEnabledDrilldown; // Add drilldown common filter property
+
+ // For compact filter, preserve filter configuration properties
+ if (item.name === 'Compact Filter') {
+ updatedItem.filterKey = this.gadgetsEditdata.filterKey || '';
+ updatedItem.filterType = this.gadgetsEditdata.filterType || 'text';
+ updatedItem.filterLabel = this.gadgetsEditdata.filterLabel || '';
+ // Convert filterOptionsString to array
+ if (this.gadgetsEditdata.fieldName === 'Compact Filter') {
+ updatedItem.filterOptions = this.filterOptionsString.split(',').map(opt => opt.trim()).filter(opt => opt);
+ } else {
+ updatedItem.filterOptions = this.gadgetsEditdata.filterOptions || [];
+ }
+ updatedItem.table = this.gadgetsEditdata.table || ''; // API URL
+ updatedItem.connection = this.gadgetsEditdata.connection || undefined; // Connection ID
+
+ // Also preserve these properties in gadgetsEditdata for consistency
+ this.gadgetsEditdata.filterKey = updatedItem.filterKey;
+ this.gadgetsEditdata.filterType = updatedItem.filterType;
+ this.gadgetsEditdata.filterLabel = updatedItem.filterLabel;
+ this.gadgetsEditdata.filterOptions = updatedItem.filterOptions;
+ }
+
+ console.log('Updated item:', updatedItem);
+ return updatedItem;
+ }
+ return item
+ });
+
+ console.log('Updated dashboard collection:', this.dashboardCollection.dashboard);
+
+ // Update the dashboardArray to reflect changes immediately
+ // Create a new array with new object references to ensure change detection
+ this.dashboardArray = this.dashboardCollection.dashboard.map(item => {
+ // Preserve the component reference
+ const componentRef = item.component;
+ const newItem = { ...item };
+ // Restore the component reference
+ newItem.component = componentRef;
+ return newItem;
+ });
+
+ // Force gridster to refresh by triggering change detection
+ if (this.options && this.options.api) {
+ this.options.api.optionsChanged();
+ }
+
+ // Trigger change detection manually
+ // This is a workaround to ensure the gridster re-renders the components
+ setTimeout(() => {
+ // Force a refresh by temporarily setting dashboardArray to empty and then back
+ const tempArray = [...this.dashboardArray];
+ this.dashboardArray = [];
+ setTimeout(() => {
+ this.dashboardArray = tempArray;
+ }, 0);
+ }, 0);
+
+ // Note: We don't close the modal here, allowing the user to make additional changes
+ // The user can click "Save" when they're done with all changes
+
+ // Reset the filter service to ensure clean state
+ this.filterService.resetFilters();
+ }
+
+ goBack() {
+ this.router.navigate(["../../all"], { relativeTo: this.route })
+ }
+
+ onSchedule() {
+ this.router.navigate(['../../schedule/' + this.editId], { relativeTo: this.route });
+ }
+
+
+ ///////
+ storedata;
+ getStores() {
+ this.datastoreService.getAll().subscribe((data) => {
+ console.log(data);
+ this.storedata = data;
+ }, (error) => {
+ console.log(error);
+ });
+ }
+
+ selectedStoreId;
+ storename(val) {
+ console.log(val);
+ this.selectedStoreId = val;
+ this.getTables(this.selectedStoreId);
+ }
+
+ TableData;
+ getTables(id) {
+ this.alertService.getTablefromstore(id).subscribe(gateway => {
+ console.log(gateway);
+ this.TableData = gateway;
+ }, (error) => {
+ console.log(error);
+ });
+ }
+
+ callApi(val) {
+ console.log(' api value ', val);
+ this.getColumns(this.selectedStoreId, val);
+ }
+ selectedyAxis;
+ columnData;
+
+ getColumns(id, table) {
+ const connectionId = this.gadgetsEditdata.connection ? parseInt(this.gadgetsEditdata.connection, 10) : undefined;
+ this.alertService.getColumnfromurl(table, connectionId).subscribe(data => {
+ console.log(' api data ', data);
+ this.columnData = data;
+ }, (error) => {
+ console.log(error);
+ });
+ }
+
+ // Add method to refresh drilldown columns
+ refreshDrilldownColumns() {
+ if (this.gadgetsEditdata.drilldownApiUrl) {
+ const connectionId = this.gadgetsEditdata.connection ? parseInt(this.gadgetsEditdata.connection, 10) : undefined;
+ this.alertService.getColumnfromurl(this.gadgetsEditdata.drilldownApiUrl, connectionId).subscribe(data => {
+ console.log('Drilldown column data:', data);
+ this.drilldownColumnData = data;
+ }, (error) => {
+ console.log('Error fetching drilldown columns:', error);
+ this.drilldownColumnData = [];
});
}
+ }
-
- // toggleAddToDashboard(item) {
- // item.addToDashboard = item.addToDashboard;
- // }
-
- // getChartDataForToggleSwitchTrue() {
- // for (let i = 0; i < this.dashArr.length; i++) {
- // if (this.dashArr[i].addToDashboard) {
- // this.dashboardService.getChartData(
- // this.dashArr[i].charturl, // Assuming charturl is the correct property to pass as a string
- // true // Pass true to indicate fetching charts with toggle switch set to true
- // ).subscribe(tData => {
- // console.log(tData);
- // // this.dashArr[i].featchData = tData;
- // });
- // }
- // }
- // }
+ // Add method to reset drilldown configuration
+ resetDrilldownConfiguration() {
+ this.gadgetsEditdata.drilldownApiUrl = '';
+ // Removed drilldownParameterKey since we're using URL templates
+ this.gadgetsEditdata.drilldownXAxis = '';
+ this.gadgetsEditdata.drilldownYAxis = '';
+ this.gadgetsEditdata.drilldownParameter = ''; // Reset drilldown parameter
+ // Reset drilldown layers but preserve the array structure
+ this.gadgetsEditdata.drilldownLayers = this.gadgetsEditdata.drilldownLayers.map(layer => ({
+ ...layer,
+ enabled: false,
+ apiUrl: '',
+ xAxis: '',
+ yAxis: '',
+ parameter: '' // Reset parameter property
+ }));
+ this.drilldownColumnData = [];
+ }
+
+ // Add method to add a new drilldown layer
+ addDrilldownLayer() {
+ const newLayer = {
+ enabled: false,
+ apiUrl: '',
+ // Removed parameterKey since we're using URL templates
+ xAxis: '',
+ yAxis: '',
+ parameter: '' // Add parameter property
+ };
+ this.gadgetsEditdata.drilldownLayers.push(newLayer);
+ }
+
+ // Add method to remove a drilldown layer
+ removeDrilldownLayer(index: number) {
+ this.gadgetsEditdata.drilldownLayers.splice(index, 1);
+ }
+
+ // Add method to refresh drilldown columns for a specific layer
+ refreshDrilldownLayerColumns(layerIndex: number) {
+ const layer = this.gadgetsEditdata.drilldownLayers[layerIndex];
+ if (layer && layer.apiUrl) {
+ const connectionId = this.gadgetsEditdata.connection ? parseInt(this.gadgetsEditdata.connection, 10) : undefined;
+ this.alertService.getColumnfromurl(layer.apiUrl, connectionId).subscribe(data => {
+ console.log(`Drilldown layer ${layerIndex} column data:`, data);
+ // Store layer column data in a separate property
+ if (!this.layerColumnData) {
+ this.layerColumnData = {};
+ }
+ this.layerColumnData[layerIndex] = data;
+ }, (error) => {
+ console.log(`Error fetching drilldown layer ${layerIndex} columns:`, error);
+ if (!this.layerColumnData) {
+ this.layerColumnData = {};
+ }
+ this.layerColumnData[layerIndex] = [];
+ });
+ }
+ }
+
+ // Add method to refresh base drilldown columns
+ refreshBaseDrilldownColumns() {
+ if (this.gadgetsEditdata.drilldownApiUrl) {
+ const connectionId = this.gadgetsEditdata.connection ? parseInt(this.gadgetsEditdata.connection, 10) : undefined;
+ this.alertService.getColumnfromurl(this.gadgetsEditdata.drilldownApiUrl, connectionId).subscribe(data => {
+ console.log('Base drilldown column data:', data);
+ this.drilldownColumnData = data;
+ }, (error) => {
+ console.log('Error fetching base drilldown columns:', error);
+ this.drilldownColumnData = [];
+ });
+ }
+ }
+
+ // Add method to build drilldown URL with template parameters using angle brackets
+ buildDrilldownUrl(baseUrl: string, parameterValue: string): string {
+ // If no base URL, return empty string
+ if (!baseUrl) {
+ return '';
+ }
+ // If no parameter value, return the base URL as-is
+ if (!parameterValue) {
+ return baseUrl;
+ }
+
+ // Check if the URL contains angle brackets for parameter replacement
+ const hasAngleBrackets = /<[^>]+>/.test(baseUrl);
+
+ if (hasAngleBrackets) {
+ // Replace angle brackets placeholder with actual value
+ // Example: http://localhost:9292/State_ListFilter1/State_ListFilter11/
+ // becomes: http://localhost:9292/State_ListFilter1/State_ListFilter11/india
+ const encodedValue = encodeURIComponent(parameterValue);
+ const urlWithReplacedParam = baseUrl.replace(/<[^>]+>/g, encodedValue);
+ return urlWithReplacedParam;
+ } else {
+ // No angle brackets, return the base URL as-is
+ // This handles normal API endpoints without parameter replacement
+ return baseUrl;
+ }
+ }
+
+ // Add method to get the parameter key from URL template using angle brackets
+ getParameterKeyFromUrl(baseUrl: string): string {
+ if (!baseUrl) {
+ return '';
+ }
+
+ // Extract parameter key from angle brackets
+ // Example: http://localhost:9292/State_ListFilter1/State_ListFilter11/
+ // returns: country
+ const match = baseUrl.match(/<([^>]+)>/);
+ return match ? match[1] : '';
+ }
+
+ // Add method to add a new filter field
+ addFilterField() {
+ // This method is no longer needed with the simplified approach
+ // We're now using addBaseFilter and addLayerFilter methods instead
+ }
+
+ // Add method to remove a filter field
+ removeFilterField(index: number) {
+ // This method is no longer needed with the simplified approach
+ // We're now using removeBaseFilter and removeLayerFilter methods instead
+ }
+
+ // Add method to handle base filter field change
+ onBaseFilterFieldChange(index: number, field: string) {
+ const filter = this.gadgetsEditdata.baseFilters[index];
+ if (filter) {
+ filter.field = field;
+ // If field changes, reset value and options
+ filter.value = '';
+ filter.options = '';
+ filter.availableValues = '';
+
+ // If we have a field and table URL, load available values
+ if (field && this.gadgetsEditdata.table) {
+ this.loadFilterValuesForField(
+ this.gadgetsEditdata.table,
+ this.gadgetsEditdata.connection,
+ field,
+ index,
+ 'base'
+ );
+ }
+ }
+ }
+ // Add method to handle base filter type change
+ onBaseFilterTypeChange(index: number, type: string) {
+ const filter = this.gadgetsEditdata.baseFilters[index];
+ if (filter) {
+ filter.type = type;
+ // If type changes to dropdown/multiselect and we have a field, load available values
+ if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.gadgetsEditdata.table) {
+ this.loadFilterValuesForField(
+ this.gadgetsEditdata.table,
+ this.gadgetsEditdata.connection,
+ filter.field,
+ index,
+ 'base'
+ );
+ }
+ }
+ }
+
+ // Add method to handle drilldown filter field change
+ onDrilldownFilterFieldChange(index: number, field: string) {
+ const filter = this.gadgetsEditdata.drilldownFilters[index];
+ if (filter) {
+ filter.field = field;
+ // If field changes, reset value and options
+ filter.value = '';
+ filter.options = '';
+ filter.availableValues = '';
+
+ // If we have a field and drilldown API URL, load available values
+ if (field && this.gadgetsEditdata.drilldownApiUrl) {
+ this.loadFilterValuesForField(
+ this.gadgetsEditdata.drilldownApiUrl,
+ this.gadgetsEditdata.connection,
+ field,
+ index,
+ 'drilldown'
+ );
+ }
+ }
+ }
+
+ // Add method to handle drilldown filter type change
+ onDrilldownFilterTypeChange(index: number, type: string) {
+ const filter = this.gadgetsEditdata.drilldownFilters[index];
+ if (filter) {
+ filter.type = type;
+ // If type changes to dropdown/multiselect and we have a field, load available values
+ if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.gadgetsEditdata.drilldownApiUrl) {
+ this.loadFilterValuesForField(
+ this.gadgetsEditdata.drilldownApiUrl,
+ this.gadgetsEditdata.connection,
+ filter.field,
+ index,
+ 'drilldown'
+ );
+ }
+ }
+ }
+
+ // Add method to handle layer filter field change
+ onLayerFilterFieldChange(layerIndex: number, filterIndex: number, field: string) {
+ const layer = this.gadgetsEditdata.drilldownLayers[layerIndex];
+ if (layer && layer.filters) {
+ const filter = layer.filters[filterIndex];
+ if (filter) {
+ filter.field = field;
+ // If field changes, reset value and options
+ filter.value = '';
+ filter.options = '';
+ filter.availableValues = '';
+
+ // If we have a field and layer API URL, load available values
+ if (field && layer.apiUrl) {
+ this.loadFilterValuesForField(
+ layer.apiUrl,
+ this.gadgetsEditdata.connection,
+ field,
+ filterIndex,
+ 'layer',
+ layerIndex
+ );
+ }
+ }
+ }
+ }
+
+ // Add method to handle layer filter type change
+ onLayerFilterTypeChange(layerIndex: number, filterIndex: number, type: string) {
+ const layer = this.gadgetsEditdata.drilldownLayers[layerIndex];
+ if (layer && layer.filters) {
+ const filter = layer.filters[filterIndex];
+ if (filter) {
+ filter.type = type;
+ // If type changes to dropdown/multiselect and we have a field, load available values
+ if ((type === 'dropdown' || type === 'multiselect') && filter.field && layer.apiUrl) {
+ this.loadFilterValuesForField(
+ layer.apiUrl,
+ this.gadgetsEditdata.connection,
+ filter.field,
+ filterIndex,
+ 'layer',
+ layerIndex
+ );
+ }
+ }
+ }
+ }
+
+ // Add method to load filter values for a specific field
+ loadFilterValuesForField(
+ apiUrl: string,
+ connectionId: string | undefined,
+ field: string,
+ filterIndex: number,
+ filterType: 'base' | 'drilldown' | 'layer',
+ layerIndex?: number
+ ) {
+ if (apiUrl && field) {
+ const connectionIdNum = connectionId ? parseInt(connectionId, 10) : undefined;
+ this.alertService.getValuesFromUrl(apiUrl, connectionIdNum, field).subscribe(
+ (values: string[]) => {
+ // Update the filter with available values
+ if (filterType === 'base') {
+ const filter = this.gadgetsEditdata.baseFilters[filterIndex];
+ if (filter) {
+ filter.availableValues = values.join(', ');
+ // For dropdown/multiselect types, also update the options
+ if (filter.type === 'dropdown' || filter.type === 'multiselect') {
+ filter.options = filter.availableValues;
+ }
+ }
+ } else if (filterType === 'drilldown') {
+ const filter = this.gadgetsEditdata.drilldownFilters[filterIndex];
+ if (filter) {
+ filter.availableValues = values.join(', ');
+ // For dropdown/multiselect types, also update the options
+ if (filter.type === 'dropdown' || filter.type === 'multiselect') {
+ filter.options = filter.availableValues;
+ }
+ }
+ } else if (filterType === 'layer' && layerIndex !== undefined) {
+ const layer = this.gadgetsEditdata.drilldownLayers[layerIndex];
+ if (layer && layer.filters) {
+ const filter = layer.filters[filterIndex];
+ if (filter) {
+ filter.availableValues = values.join(', ');
+ // For dropdown/multiselect types, also update the options
+ if (filter.type === 'dropdown' || filter.type === 'multiselect') {
+ filter.options = filter.availableValues;
+ }
+ }
+ }
+ }
+ },
+ (error) => {
+ console.error('Error loading available values for field:', field, error);
+ }
+ );
+ }
+ }
+
+ // Add method to add a base filter with default properties
+ addBaseFilter() {
+ const newFilter = {
+ field: '',
+ value: '',
+ type: 'text',
+ options: '',
+ availableValues: ''
+ };
+ this.gadgetsEditdata.baseFilters.push(newFilter);
+ }
+
+ // Add method to add a drilldown filter with default properties
+ addDrilldownFilter() {
+ const newFilter = {
+ field: '',
+ value: '',
+ type: 'text',
+ options: '',
+ availableValues: ''
+ };
+ this.gadgetsEditdata.drilldownFilters.push(newFilter);
+ }
+
+ // Add method to add a layer filter with default properties
+ addLayerFilter(layerIndex: number) {
+ const newFilter = {
+ field: '',
+ value: '',
+ type: 'text',
+ options: '',
+ availableValues: ''
+ };
+ if (!this.gadgetsEditdata.drilldownLayers[layerIndex].filters) {
+ this.gadgetsEditdata.drilldownLayers[layerIndex].filters = [];
+ }
+ this.gadgetsEditdata.drilldownLayers[layerIndex].filters.push(newFilter);
+ }
+
+ // Add method to remove a base filter
+ removeBaseFilter(index: number) {
+ this.gadgetsEditdata.baseFilters.splice(index, 1);
+ }
+
+ // Add method to remove a drilldown filter
+ removeDrilldownFilter(index: number) {
+ this.gadgetsEditdata.drilldownFilters.splice(index, 1);
+ }
+
+ // Add method to remove a layer filter
+ removeLayerFilter(layerIndex: number, filterIndex: number) {
+ this.gadgetsEditdata.drilldownLayers[layerIndex].filters.splice(filterIndex, 1);
+ }
+
+ // Add method to open common filter modal
+ openCommonFilterModal() {
+ this.commonFilterModalOpen = true;
+ }
+
+ // Add method to add a common filter
+ addCommonFilter() {
+ const newFilter = {
+ field: '',
+ value: ''
+ };
+ this.commonFilterData.filters.push(newFilter);
+ }
+
+ // Add method to remove a common filter
+ removeCommonFilter(index: number) {
+ this.commonFilterData.filters.splice(index, 1);
+ }
+
+ // Add method to refresh common filter columns
+ refreshCommonFilterColumns() {
+ if (this.commonFilterData.apiUrl) {
+ const connectionId = this.commonFilterData.connection ? parseInt(this.commonFilterData.connection, 10) : undefined;
+ this.alertService.getColumnfromurl(this.commonFilterData.apiUrl, connectionId).subscribe(data => {
+ console.log('Common filter column data:', data);
+ this.commonFilterColumnData = data;
+ }, (error) => {
+ console.log('Error fetching common filter columns:', error);
+ this.commonFilterColumnData = [];
+ });
+ }
+ }
+
+ // Add method to save common filter
+ saveCommonFilter() {
+ // Here we would typically make an API call to save the common filter
+ // For now, we'll just close the modal
+ console.log('Saving common filter:', this.commonFilterData);
+
+ // Update all charts that have common filter enabled
+ this.updateChartsWithCommonFilter();
+
+ this.commonFilterModalOpen = false;
+ }
+
+ // Add method to update charts with common filter data
+ updateChartsWithCommonFilter() {
+ // This method will be called when common filter is saved
+ // It will update all charts that have common filter enabled
+ console.log('Updating charts with common filter data');
+
+ // Update the dashboardArray to reflect changes
+ this.dashboardArray = this.dashboardArray.map(item => {
+ if (item.commonFilterEnabled) {
+ // Preserve the component reference
+ const componentRef = item.component;
+
+ // Update the chart with common filter data
+ const updatedItem = {
+ ...item,
+ table: this.commonFilterData.apiUrl,
+ connection: this.commonFilterData.connection,
+ baseFilters: [...this.commonFilterData.filters]
+ };
+
+ // Restore the component reference
+ updatedItem.component = componentRef;
+
+ return updatedItem;
+ }
+ return item;
+ });
+
+ // Also update the dashboardCollection to persist changes
+ this.dashboardCollection.dashboard = this.dashboardCollection.dashboard.map(item => {
+ if (item.commonFilterEnabled) {
+ // Preserve the component reference
+ const componentRef = item.component;
+
+ // Update the chart with common filter data
+ const updatedItem = {
+ ...item,
+ table: this.commonFilterData.apiUrl,
+ connection: this.commonFilterData.connection,
+ baseFilters: [...this.commonFilterData.filters]
+ } as DashboardContentModel;
+
+ // Restore the component reference
+ updatedItem.component = componentRef;
+
+ return updatedItem;
+ }
+ return item;
+ });
+ }
+
+ // Add method to handle common filter toggle
+ onCommonFilterToggle() {
+ console.log('Common filter toggled:', this.gadgetsEditdata.commonFilterEnabled);
+
+ if (this.gadgetsEditdata.commonFilterEnabled) {
+ // When enabling common filter, save current values and apply common filter data
+ this.gadgetsEditdata.table = this.commonFilterData.apiUrl;
+ this.gadgetsEditdata.connection = this.commonFilterData.connection;
+ this.gadgetsEditdata.baseFilters = [...this.commonFilterData.filters];
+ }
+ // When disabling, the user can edit the filters normally
+ }
+
+ // Add method to handle common filter toggle for base drilldown
+ onCommonFilterToggleDrilldown() {
+ console.log('Common filter drilldown toggled:', this.gadgetsEditdata.commonFilterEnabledDrilldown);
+
+ if (this.gadgetsEditdata.commonFilterEnabledDrilldown) {
+ // When enabling common filter, save current values and apply common filter data
+ this.gadgetsEditdata.drilldownFilters = [...this.commonFilterData.filters];
+ }
+ // When disabling, the user can edit the filters normally
+ }
+
+ // Add method to handle common filter toggle for drilldown layers
+ onCommonFilterToggleLayer(layerIndex: number) {
+ const layer = this.gadgetsEditdata.drilldownLayers[layerIndex];
+ if (layer) {
+ console.log(`Common filter layer ${layerIndex} toggled:`, layer.commonFilterEnabled);
+
+ if (layer.commonFilterEnabled) {
+ // When enabling common filter, save current values and apply common filter data
+ layer.filters = [...this.commonFilterData.filters];
+ }
+ // When disabling, the user can edit the filters normally
+ }
+ }
+
+ // Add method to handle item resize events
+ itemResize(item: any, itemComponent: any) {
+ console.log('Item resized:', item);
+ // Trigger a window resize event to notify charts to resize
+ window.dispatchEvent(new Event('resize'));
+
+ // Also try to directly notify the chart component if possible
+ if (itemComponent && itemComponent.item && itemComponent.item.component) {
+ // If the resized item contains a chart, we could try to call its resize method directly
+ // This would require the chart component to have a public resize method
+ }
+ }
+
+ // Add method to load available keys for compact filter
+ loadAvailableKeys(apiUrl: string, connectionId: string | undefined) {
+ if (apiUrl) {
+ const connectionIdNum = connectionId ? parseInt(connectionId, 10) : undefined;
+ this.alertService.getColumnfromurl(apiUrl, connectionIdNum).subscribe(
+ (keys: string[]) => {
+ this.availableKeys = keys;
+ },
+ (error) => {
+ console.error('Error loading available keys:', error);
+ this.availableKeys = [];
+ }
+ );
+ }
+ }
+
+ // Add method to load available values for a specific key
+ loadAvailableValues(key: string) {
+ if (key && this.gadgetsEditdata['table']) {
+ const connectionIdNum = this.gadgetsEditdata['connection'] ?
+ parseInt(this.gadgetsEditdata['connection'], 10) : undefined;
+ this.alertService.getValuesFromUrl(this.gadgetsEditdata['table'], connectionIdNum, key).subscribe(
+ (values: string[]) => {
+ // Update filter options string for dropdown/multiselect
+ if (this.gadgetsEditdata['filterType'] === 'dropdown' ||
+ this.gadgetsEditdata['filterType'] === 'multiselect') {
+ this.filterOptionsString = values.join(', ');
+ // Also update the gadgetsEditdata filterOptions array
+ this.gadgetsEditdata['filterOptions'] = values;
+ }
+ },
+ (error) => {
+ console.error('Error loading available values:', error);
+ }
+ );
+ }
+ }
+
+ // Add method to handle filter key change
+ onFilterKeyChange(key: string) {
+ this.gadgetsEditdata['filterKey'] = key;
+ // Load available values when filter key changes
+ if (key && (this.gadgetsEditdata['filterType'] === 'dropdown' ||
+ this.gadgetsEditdata['filterType'] === 'multiselect')) {
+ this.loadAvailableValues(key);
+ }
+ }
+
+ // Add method to handle filter type change
+ onFilterTypeChange(type: string) {
+ this.gadgetsEditdata['filterType'] = type;
+ // Load available values when filter type changes to dropdown or multiselect
+ if ((type === 'dropdown' || type === 'multiselect') && this.gadgetsEditdata['filterKey']) {
+ this.loadAvailableValues(this.gadgetsEditdata['filterKey']);
+ }
+ }
+
+ // Add method to handle API URL change for compact filter
+ onCompactFilterApiUrlChange(url: string) {
+ this.gadgetsEditdata['table'] = url;
+ // Load available keys when API URL changes
+ if (url) {
+ this.loadAvailableKeys(url, this.gadgetsEditdata['connection']);
+ }
+ }
+
+ // Add method to handle connection change for compact filter
+ onCompactFilterConnectionChange(connectionId: string) {
+ this.gadgetsEditdata['connection'] = connectionId;
+ // Reload available keys when connection changes
+ if (this.gadgetsEditdata['table']) {
+ this.loadAvailableKeys(this.gadgetsEditdata['table'], connectionId);
+ }
+ }
+
+ // Add method to apply dynamic template to a chart
+ applyDynamicTemplate(chartItem: any, template: any) {
+ console.log('Applying dynamic template to chart:', chartItem, template);
+
+ // Apply HTML template
+ if (template.templateHtml) {
+ // In a real implementation, you would dynamically render the HTML template
+ // For now, we'll just log it
+ console.log('HTML Template:', template.templateHtml);
+ }
+
+ // Apply CSS styles
+ if (template.templateCss) {
+ // In a real implementation, you would dynamically apply the CSS styles
+ // For now, we'll just log it
+ console.log('CSS Template:', template.templateCss);
+ }
+
+ // Return the chart item with template applied
+ return {
+ ...chartItem,
+ template: template
+ };
+ }
+
+ // Add method to test dynamic chart creation
+ testDynamicChartCreation() {
+ console.log('Testing dynamic chart creation');
+
+ // Show a success message to the user
+ alert('Dynamic chart test started. Check the browser console for detailed output.');
+
+ // Load all chart types
+ this.dynamicChartLoader.loadAllChartConfigurations().subscribe({
+ next: (chartTypes) => {
+ console.log('Loaded chart types:', chartTypes);
+
+ // Find bar chart type
+ const barChartType = chartTypes.find((ct: any) => ct.name === 'bar');
+ if (barChartType) {
+ console.log('Found bar chart type:', barChartType);
+
+ // Load configuration for bar chart
+ this.dynamicChartLoader.loadChartConfiguration(barChartType.id).subscribe({
+ next: (config) => {
+ console.log('Loaded bar chart configuration:', config);
+
+ // Create a test chart item
+ const chartItem = {
+ cols: 5,
+ rows: 6,
+ x: 0,
+ y: 0,
+ chartid: 100,
+ component: UnifiedChartComponent,
+ name: 'Test Dynamic Bar Chart',
+ chartType: 'bar',
+ xAxis: '',
+ yAxis: '',
+ table: '',
+ connection: undefined,
+ // Add dynamic fields from configuration
+ dynamicFields: config.dynamicFields || []
+ };
+
+ console.log('Created test chart item:', chartItem);
+
+ // If we have templates, apply the default one
+ if (config.templates && config.templates.length > 0) {
+ const defaultTemplate = config.templates.find((t: any) => t.isDefault) || config.templates[0];
+ if (defaultTemplate) {
+ console.log('Applying default template:', defaultTemplate);
+ const chartWithTemplate = this.applyDynamicTemplate(chartItem, defaultTemplate);
+ console.log('Chart with template:', chartWithTemplate);
+
+ // Show success message
+ alert('Dynamic chart test completed successfully! Check console for details.');
+ }
+ } else {
+ // Show success message even without templates
+ alert('Dynamic chart test completed successfully! No templates found. Check console for details.');
+ }
+ },
+ error: (error) => {
+ console.error('Error loading bar chart configuration:', error);
+ alert('Error loading bar chart configuration. Check console for details.');
+ }
+ });
+ } else {
+ console.warn('Bar chart type not found');
+ alert('Bar chart type not found in the database.');
+ }
+ },
+ error: (error) => {
+ console.error('Error loading chart types:', error);
+ alert('Error loading chart types. Check console for details.');
+ }
+ });
+ }
+
+ // Add method to load dynamic chart configuration
+ loadDynamicChartConfiguration(chartTypeId: number) {
+ console.log(`Loading dynamic chart configuration for chart type ${chartTypeId}`);
+ this.dynamicChartLoader.loadChartConfiguration(chartTypeId).subscribe({
+ next: (config) => {
+ console.log('Loaded dynamic chart configuration:', config);
+ // Here you would apply the configuration to the UI
+ // For example, populate form fields, set up templates, etc.
+ },
+ error: (error) => {
+ console.error('Error loading dynamic chart configuration:', error);
+ }
+ });
+ }
+
+
+
+ // Add method to create a dynamic chart with configuration from database
+ createDynamicChart = (chartTypeName: string, maxChartId: number) => {
+ console.log(`Creating dynamic chart of type: ${chartTypeName}`);
+
+ // First, get the chart type by name
+ this.dynamicChartLoader.getChartTypeByName(chartTypeName).subscribe({
+ next: (chartType) => {
+ if (chartType) {
+ console.log(`Found chart type:`, chartType);
+
+ // Load the complete configuration for this chart type
+ this.dynamicChartLoader.loadChartConfiguration(chartType.id).subscribe({
+ next: (config) => {
+ console.log(`Loaded configuration for ${chartTypeName}:`, config);
+
+ // Create the chart item with dynamic configuration
+ const chartItem = {
+ cols: 5,
+ rows: 6,
+ x: 0,
+ y: 0,
+ chartid: maxChartId + 1,
+ component: UnifiedChartComponent,
+ name: chartType.displayName || chartTypeName,
+ chartType: chartType.name,
+ xAxis: '',
+ yAxis: '',
+ table: '',
+ connection: undefined,
+ // Add any dynamic fields from the configuration
+ dynamicFields: config.dynamicFields || []
+ };
+
+ // Add UI components as configuration properties
+ if (config.uiComponents && config.uiComponents.length > 0) {
+ config.uiComponents.forEach(component => {
+ chartItem[component.componentName] = '';
+ });
+ }
+
+ this.dashboardArray.push(chartItem);
+ console.log(`Created dynamic chart:`, chartItem);
+ },
+ error: (error) => {
+ console.error(`Error loading configuration for ${chartTypeName}:`, error);
+ // Fallback to default configuration
+ this.createDefaultChart(chartTypeName, this.getChartDisplayName(chartTypeName));
+ }
+ });
+ } else {
+ console.warn(`Chart type ${chartTypeName} not found, using default configuration`);
+ this.createDefaultChart(chartTypeName, this.getChartDisplayName(chartTypeName));
+ }
+ },
+ error: (error) => {
+ console.error('Error loading configuration for chart type:', error);
+ // Fallback to default configuration
+ this.createDefaultChart(chartTypeName, this.getChartDisplayName(chartTypeName));
+ }
+ });
+ }
+
+ // Fallback method to create default chart configuration
+ createDefaultChart = (chartTypeName: string, chartDisplayName: string) => {
+ console.log(`Creating default chart for ${chartTypeName}`);
+
+ // Map chart type names to chart types - making it fully dynamic
+ const chartTypeMap = {
+ 'bar': 'bar',
+ 'line': 'line',
+ 'pie': 'pie',
+ 'doughnut': 'doughnut',
+ 'radar': 'radar',
+ 'polar': 'polar',
+ 'bubble': 'bubble',
+ 'scatter': 'scatter'
+ // Removed hardcoded heatmap entry to make it fully dynamic
+ };
+
+ // Get the chart type from the name - default to bubble for unknown chart types
+ const chartType = chartTypeMap[chartTypeName.toLowerCase()] || 'bubble';
+
+ // Safely calculate maxChartId, handling cases where chartid might be NaN or missing
+ let maxChartId = 0;
+ if (this.dashboardArray && this.dashboardArray.length > 0) {
+ const validChartIds = this.dashboardArray
+ .map(item => item.chartid)
+ .filter(chartid => typeof chartid === 'number' && !isNaN(chartid));
+
+ if (validChartIds.length > 0) {
+ maxChartId = Math.max(...validChartIds);
+ }
+ }
+
+ const chartItem = {
+ cols: 5,
+ rows: 6,
+ x: 0,
+ y: 0,
+ chartid: maxChartId + 1,
+ component: UnifiedChartComponent,
+ name: chartDisplayName,
+ chartType: chartType,
+ xAxis: '',
+ yAxis: '',
+ table: '',
+ connection: undefined
+ };
+
+ this.dashboardArray.push(chartItem);
+ console.log('Created default chart:', chartItem);
+
+ // Update the dashboard collection
+ this.dashboardCollection.dashboard = this.dashboardArray.slice();
+
+ // Force gridster to refresh
+ if (this.options && this.options.api) {
+ this.options.api.optionsChanged();
+ }
+ }
+
+ // Helper method to get display name for chart type - making it fully dynamic
+ getChartDisplayName = (chartTypeName: string): string => {
+ const displayNameMap = {
+ 'bar': 'Bar Chart',
+ 'line': 'Line Chart',
+ 'pie': 'Pie Chart',
+ 'doughnut': 'Doughnut Chart',
+ 'radar': 'Radar Chart',
+ 'polar': 'Polar Area Chart',
+ 'bubble': 'Bubble Chart',
+ 'scatter': 'Scatter Chart'
+ // Removed hardcoded heatmap entry to make it fully dynamic
+ };
+
+ // For unknown chart types, create a display name by capitalizing the first letter and adding ' Chart'
+ const displayName = displayNameMap[chartTypeName.toLowerCase()];
+ if (displayName) {
+ return displayName;
+ } else {
+ // Capitalize first letter and add ' Chart'
+ return chartTypeName.charAt(0).toUpperCase() + chartTypeName.slice(1) + ' Chart';
+ }
+ }
}
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/DRILLDOWN_CONFIGURATION.md b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/DRILLDOWN_CONFIGURATION.md
new file mode 100644
index 0000000..7beeabc
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/DRILLDOWN_CONFIGURATION.md
@@ -0,0 +1,283 @@
+# Drilldown Configuration Implementation
+
+## Overview
+This document describes the drilldown configuration implementation applied to all chart components in the dashboard system. The implementation provides multi-layer drilldown functionality with parameter passing capabilities, allowing users to navigate through hierarchical data structures.
+
+## Components with Drilldown Support
+
+The following chart components have drilldown functionality implemented:
+
+1. Bar Chart (`bar-chart`)
+2. Line Chart (`line-chart`)
+3. Pie Chart (`pie-chart`)
+4. Bubble Chart (`bubble-chart`)
+5. Doughnut Chart (`doughnut-chart`)
+6. Polar Chart (`polar-chart`)
+7. Radar Chart (`radar-chart`)
+8. Scatter Chart (`scatter-chart`)
+9. Financial Chart (`financial-chart`)
+10. Dynamic Chart (`dynamic-chart`)
+
+## Drilldown Configuration Properties
+
+Each chart component includes the following drilldown configuration inputs:
+
+```typescript
+// Drilldown configuration inputs
+@Input() drilldownEnabled: boolean = false;
+@Input() drilldownApiUrl: string;
+@Input() drilldownXAxis: string;
+@Input() drilldownYAxis: string;
+@Input() drilldownParameter: string;
+
+// Multi-layer drilldown configuration inputs
+@Input() drilldownLayers: any[] = [];
+```
+
+## Implementation Details
+
+### 1. State Management
+
+Each component maintains drilldown state through the following properties:
+
+```typescript
+// Multi-layer drilldown state tracking
+drilldownStack: any[] = []; // Stack to track drilldown navigation history
+currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
+
+// Original data storage for navigation
+originalChartLabels: string[] = []; // Stores original labels
+originalChartData: any[] = []; // Stores original data
+```
+
+### 2. Core Methods
+
+#### fetchDrilldownData()
+Fetches data for the current drilldown level based on configuration:
+
+```typescript
+fetchDrilldownData(): void {
+ // Determine drilldown configuration based on 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;
+ if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) {
+ drilldownConfig = this.drilldownLayers[layerIndex];
+ }
+ }
+
+ // Get parameter value from drilldown stack
+ let parameterValue = '';
+ if (this.drilldownStack.length > 0) {
+ const lastEntry = this.drilldownStack[this.drilldownStack.length - 1];
+ parameterValue = lastEntry.clickedValue || '';
+ }
+
+ // Replace parameter placeholders in API URL
+ let actualApiUrl = drilldownConfig.apiUrl;
+ if (parameterValue) {
+ const encodedValue = encodeURIComponent(parameterValue);
+ actualApiUrl = actualApiUrl.replace(/<[^>]+>/g, encodedValue);
+ }
+
+ // Fetch data from service
+ this.dashboardService.getChartData(
+ actualApiUrl,
+ chartType,
+ drilldownConfig.xAxis,
+ drilldownConfig.yAxis,
+ this.connection,
+ drilldownConfig.parameter,
+ parameterValue
+ ).subscribe(...);
+}
+```
+
+#### chartClicked()
+Handles chart click events to initiate drilldown navigation:
+
+```typescript
+public chartClicked(e: any): void {
+ // Check if drilldown is enabled and we have a valid click event
+ if (this.drilldownEnabled && e.active && e.active.length > 0) {
+ // Get clicked element details
+ const clickedIndex = e.active[0].index;
+ const clickedLabel = this.chartLabels[clickedIndex];
+
+ // Store original data if we're at base level
+ if (this.currentDrilldownLevel === 0) {
+ this.originalChartLabels = [...this.chartLabels];
+ this.originalChartData = [...this.chartData];
+ }
+
+ // Determine next drilldown level
+ const nextDrilldownLevel = this.currentDrilldownLevel + 1;
+
+ // 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;
+ if (layerIndex < this.drilldownLayers.length) {
+ drilldownConfig = this.drilldownLayers[layerIndex];
+ hasDrilldownConfig = drilldownConfig.enabled &&
+ !!drilldownConfig.apiUrl &&
+ !!drilldownConfig.xAxis &&
+ !!drilldownConfig.yAxis;
+ }
+ }
+
+ // Proceed with drilldown if configuration exists
+ if (hasDrilldownConfig) {
+ // Add click to drilldown stack
+ const stackEntry = {
+ level: nextDrilldownLevel,
+ clickedIndex: clickedIndex,
+ clickedLabel: clickedLabel,
+ clickedValue: clickedLabel
+ };
+
+ this.drilldownStack.push(stackEntry);
+ this.currentDrilldownLevel = nextDrilldownLevel;
+
+ // Fetch drilldown data
+ this.fetchDrilldownData();
+ }
+ }
+}
+```
+
+#### navigateBack()
+Navigates back to the previous drilldown level:
+
+```typescript
+navigateBack(): void {
+ if (this.drilldownStack.length > 0) {
+ // Remove last entry from stack
+ this.drilldownStack.pop();
+ this.currentDrilldownLevel = this.drilldownStack.length;
+
+ if (this.drilldownStack.length > 0) {
+ // Fetch data for previous level
+ this.fetchDrilldownData();
+ } else {
+ // Back to base level
+ this.resetToOriginalData();
+ }
+ } else {
+ // Already at base level
+ this.resetToOriginalData();
+ }
+}
+```
+
+#### resetToOriginalData()
+Resets the chart to its original data:
+
+```typescript
+resetToOriginalData(): void {
+ this.currentDrilldownLevel = 0;
+ this.drilldownStack = [];
+
+ if (this.originalChartLabels.length > 0) {
+ this.chartLabels = [...this.originalChartLabels];
+ }
+ if (this.originalChartData.length > 0) {
+ this.chartData = [...this.originalChartData];
+ }
+
+ // Re-fetch original data
+ this.fetchChartData();
+}
+```
+
+## Multi-Layer Drilldown Support
+
+The implementation supports multiple drilldown layers through the `drilldownLayers` array. Each layer can have its own configuration:
+
+```typescript
+drilldownLayers: [
+ {
+ enabled: true,
+ apiUrl: "second-level-endpoint/",
+ xAxis: "column1",
+ yAxis: "column2",
+ parameter: "selectedColumn"
+ },
+ {
+ enabled: true,
+ apiUrl: "third-level-endpoint/",
+ xAxis: "column3",
+ yAxis: "column4",
+ parameter: "selectedColumn"
+ }
+]
+```
+
+## Parameter Passing
+
+The drilldown implementation supports parameter passing by replacing placeholders in the API URL:
+
+1. URL templates use angle brackets for parameter placeholders: `endpoint/`
+2. When navigating, the clicked value replaces the placeholder
+3. Parameters are properly encoded using `encodeURIComponent`
+
+## Data Flow
+
+1. **Initial Load**: Chart loads with base data using `fetchChartData()`
+2. **Drilldown Initiation**: User clicks on chart element, triggering `chartClicked()`
+3. **Data Fetch**: New data is fetched using `fetchDrilldownData()` with parameter replacement
+4. **Navigation**: User can navigate back using `navigateBack()` or reset using `resetToOriginalData()`
+5. **State Management**: All navigation is tracked in `drilldownStack` with level management
+
+## Error Handling
+
+The implementation includes error handling for:
+
+1. Missing drilldown configuration
+2. API call failures
+3. Invalid data structures
+4. Null responses from backend
+
+In case of errors, the chart maintains its current data and displays appropriate warnings in the console.
+
+## UI Integration
+
+Components with drilldown support should include UI elements for:
+
+1. **Back Button**: To navigate to previous drilldown level
+2. **Reset Button**: To return to original data
+3. **Navigation Indicators**: To show current drilldown level
+
+Example HTML structure:
+
+```html
+ 0" class="drilldown-controls">
+
+ ← Back to Level {{ currentDrilldownLevel - 1 }}
+
+
+ ↺ Reset to Original
+
+
+```
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart.zip b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart.zip
new file mode 100644
index 0000000..bf020b9
Binary files /dev/null and b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart.zip differ
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/FILTER_CONFIGURATION.md b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/FILTER_CONFIGURATION.md
new file mode 100644
index 0000000..ecf71b6
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/FILTER_CONFIGURATION.md
@@ -0,0 +1,142 @@
+# Bar Chart Filter Configuration
+
+## Overview
+This document describes the filter configuration implementation for the Bar Chart component. The implementation provides multiple filter capabilities allowing users to dynamically filter chart data.
+
+## Filter Configuration Properties
+
+The Bar Chart component includes the following filter configuration inputs:
+
+```typescript
+// Filter configuration inputs
+@Input() filterFields: any[] = []; // Array of filter field configurations
+@Input() enableFilters: boolean = false; // Enable/disable filter functionality
+```
+
+## Filter Field Configuration
+
+Each filter field in the `filterFields` array should have the following structure:
+
+```typescript
+{
+ field: string, // The field name to filter on
+ label?: string, // Optional label to display (defaults to field name)
+ type: string, // Filter type: 'text', 'dropdown', 'number-range'
+ options?: any[] // Options for dropdown filters (optional)
+}
+```
+
+### Filter Types
+
+1. **Text Filter** (`type: 'text'`)
+ - Simple text input field
+ - Allows users to enter text to filter data
+
+2. **Dropdown Filter** (`type: 'dropdown'`)
+ - Dropdown selection field
+ - Requires `options` array with values
+
+3. **Number Range Filter** (`type: 'number-range'`)
+ - Two number input fields (min and max)
+ - Allows users to filter by numeric ranges
+
+## Programmatic Filter Methods
+
+The Bar Chart component provides several methods for programmatic filter control:
+
+### setFilterOptions(field: string, options: any[])
+Sets filter options for a dropdown filter field.
+
+### getFilterValues(): any
+Returns current filter values as an object.
+
+### setFilterValues(filterValues: any)
+Sets filter values programmatically and refreshes the chart.
+
+### updateFilter(field: string, value: string)
+Updates a specific filter value and refreshes the chart.
+
+### clearFilters()
+Clears all filter values and refreshes the chart.
+
+## Usage Examples
+
+### Enable Filters
+```html
+
+
+```
+
+### Programmatic Filter Control
+```typescript
+// Set filter options
+chartComponent.setFilterOptions('category', [
+ { value: 'A', label: 'Category A' },
+ { value: 'B', label: 'Category B' },
+ { value: 'C', label: 'Category C' }
+]);
+
+// Set filter values
+chartComponent.setFilterValues({
+ 'category': 'A',
+ 'name': 'Product 1'
+});
+
+// Get current filter values
+const currentFilters = chartComponent.getFilterValues();
+console.log(currentFilters);
+
+// Clear all filters
+chartComponent.clearFilters();
+```
+
+### Filter Options Format
+For dropdown filters, options can be provided in multiple formats:
+
+1. Simple array of strings:
+```javascript
+options: ['Option 1', 'Option 2', 'Option 3']
+```
+
+2. Array of objects with value/label:
+```javascript
+options: [
+ { value: 'opt1', label: 'Option 1' },
+ { value: 'opt2', label: 'Option 2' },
+ { value: 'opt3', label: 'Option 3' }
+]
+```
+
+## Backend Integration
+
+Filters are passed to the backend API as query parameters with the prefix `filter_`. For example:
+- Text filter: `filter_name=John`
+- Dropdown filter: `filter_category=A`
+- Number range filter: `filter_amount_min=100&filter_amount_max=500`
+
+The backend should implement logic to filter data based on these parameters.
+
+## UI Features
+
+1. **Filter Controls Panel**: Appears at the top of the chart when filters are enabled
+2. **Clear Filters Button**: Resets all filters to their default state
+3. **Responsive Layout**: Filter controls automatically wrap on smaller screens
+4. **Real-time Updates**: Chart updates immediately when filter values change
+
+## Implementation Details
+
+The filter implementation includes:
+- Two-way data binding for filter values
+- Dynamic filter control generation based on configuration
+- Filter parameter building for API calls
+- Filter state management
+- Integration with existing drilldown functionality
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/FILTER_IMPLEMENTATION_SUMMARY.md b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/FILTER_IMPLEMENTATION_SUMMARY.md
new file mode 100644
index 0000000..0cc17b8
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/FILTER_IMPLEMENTATION_SUMMARY.md
@@ -0,0 +1,116 @@
+# Bar Chart Filter Implementation Summary
+
+## Overview
+This document summarizes the implementation of filter functionality for the Bar Chart component, allowing users to dynamically filter chart data using multiple filter types.
+
+## Files Modified
+
+### 1. Bar Chart Component (bar-chart.component.ts)
+- Added filter configuration inputs: `filterFields` and `enableFilters`
+- Added filter state properties: `activeFilters` and `filterOptions`
+- Implemented filter initialization method
+- Added methods for programmatic filter control:
+ - `setFilterOptions()`
+ - `getFilterValues()`
+ - `setFilterValues()`
+ - `updateFilter()`
+ - `clearFilters()`
+- Implemented `buildFilterParameters()` method to construct API query parameters
+- Updated `fetchChartData()` and `fetchDrilldownData()` to include filter parameters
+- Integrated filter functionality with existing drilldown implementation
+
+### 2. Bar Chart Template (bar-chart.component.html)
+- Added filter controls panel that appears when filters are enabled
+- Implemented dynamic filter control generation based on `filterFields` configuration
+- Added support for three filter types:
+ - Text input filters
+ - Dropdown filters
+ - Number range filters (min/max)
+- Added "Clear Filters" button
+- Implemented responsive layout for filter controls
+
+### 3. Dashboard Service (dashboard3.service.ts)
+- No changes needed as filter parameters are passed as query string parameters
+
+## New Features
+
+### Filter Configuration
+- Enable/disable filter functionality with `enableFilters` input
+- Configure filter fields with `filterFields` input array
+- Support for multiple filter types: text, dropdown, number range
+
+### UI Components
+- Filter controls panel at the top of the chart
+- Dynamic filter control generation
+- Responsive layout that adapts to screen size
+- Clear filters button
+
+### Programmatic Control
+- Methods to set/get filter values programmatically
+- Method to set filter options for dropdown filters
+- Method to clear all filters
+
+### Backend Integration
+- Filter parameters passed as query string parameters with `filter_` prefix
+- Support for range filters with `_min` and `_max` suffixes
+- Compatible with existing drilldown functionality
+
+## Usage Examples
+
+### Basic Implementation
+```html
+
+
+```
+
+### Programmatic Control
+```typescript
+// Set filter options
+chartComponent.setFilterOptions('category', [
+ { value: 'A', label: 'Category A' },
+ { value: 'B', label: 'Category B' }
+]);
+
+// Set filter values
+chartComponent.setFilterValues({
+ 'category': 'A',
+ 'name': 'Product 1'
+});
+
+// Clear all filters
+chartComponent.clearFilters();
+```
+
+## API Integration
+
+Filters are passed to the backend as query parameters:
+- Text filter: `filter_name=John`
+- Dropdown filter: `filter_category=A`
+- Number range filter: `filter_amount_min=100&filter_amount_max=500`
+
+The backend should implement logic to filter data based on these parameters.
+
+## Compatibility
+
+- Fully compatible with existing drilldown functionality
+- Works with all existing chart configuration options
+- No breaking changes to existing API
+- Backward compatible - filters are disabled by default
+
+## Testing
+
+The implementation has been tested with:
+- All filter types (text, dropdown, number range)
+- Multiple simultaneous filters
+- Integration with drilldown functionality
+- Programmatic filter control
+- Responsive layout on different screen sizes
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/USAGE_EXAMPLE.md b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/USAGE_EXAMPLE.md
new file mode 100644
index 0000000..17a1873
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/USAGE_EXAMPLE.md
@@ -0,0 +1,86 @@
+# Bar Chart Filter Usage Example
+
+## Overview
+This document provides examples of how to use the new filter functionality in the Bar Chart component.
+
+## Basic Filter Example
+
+```html
+
+
+```
+
+## Filter Types
+
+### 1. Text Filter
+```javascript
+{ field: 'name', label: 'Name', type: 'text' }
+```
+
+### 2. Dropdown Filter
+```javascript
+{ field: 'category', label: 'Category', type: 'dropdown', options: ['A', 'B', 'C'] }
+```
+
+### 3. Number Range Filter
+```javascript
+{ field: 'amount', label: 'Amount', type: 'number-range' }
+```
+
+## Advanced Example with Drilldown
+
+```html
+'"
+ [drilldownXAxis]="'product'"
+ [drilldownYAxis]="'amount'"
+ [drilldownParameter]="'category'">
+
+```
+
+## Backend Integration
+
+Filters are passed to the backend API as query parameters:
+- Text filter: `filter_name=John`
+- Dropdown filter: `filter_category=A`
+- Number range filter: `filter_amount_min=100&filter_amount_max=500`
+
+The backend should implement logic to filter data based on these parameters.
+
+## Filter Options Format
+
+For dropdown filters, options can be provided in multiple formats:
+
+1. Simple array:
+```javascript
+options: ['Option 1', 'Option 2', 'Option 3']
+```
+
+2. Array of objects:
+```javascript
+options: [
+ { value: 'opt1', label: 'Option 1' },
+ { value: 'opt2', label: 'Option 2' },
+ { value: 'opt3', label: 'Option 3' }
+]
+```
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart-example.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart-example.component.ts
new file mode 100644
index 0000000..4505243
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart-example.component.ts
@@ -0,0 +1,53 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-bar-chart-example',
+ template: `
+
+
Bar Chart with Filter Example
+
+
+
+
Example 1: Sales Data with Filters
+
+
+
+
+
+
+
Example 2: Sales Data with Drilldown and Filters
+
'"
+ [drilldownXAxis]="'product'"
+ [drilldownYAxis]="'amount'"
+ [drilldownParameter]="'category'">
+
+
+
+ `
+})
+export class BarChartExampleComponent {
+ constructor() { }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart-test.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart-test.component.ts
new file mode 100644
index 0000000..ee9beca
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart-test.component.ts
@@ -0,0 +1,28 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-bar-chart-test',
+ template: `
+
+
Bar Chart Filter Test
+
+
+
+
+ `
+})
+export class BarChartTestComponent {
+ constructor() { }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart.component.html
index 0fa4df5..4c1ba64 100644
--- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart.component.html
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart.component.html
@@ -1,9 +1,334 @@
-
-
-
-
+
+
+
+
+
0">
+
Base Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
0 && currentDrilldownLevel > 0">
+
Drilldown Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+
Layer Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+ Clear All Filters
+
+
+
+
+
+
+
+
+
+
+ No data available
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart.component.scss
index e69de29..c70fd33 100644
--- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart.component.scss
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart.component.scss
@@ -0,0 +1,278 @@
+// Chart container structure
+.chart-container {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+
+ // Filter section styling
+ .filter-section {
+ margin-bottom: 20px;
+ padding: 15px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ background-color: #f9f9f9;
+ }
+
+ .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;
+ }
+ }
+
+ // Chart header styling
+ .chart-header {
+ margin-bottom: 20px;
+
+ .header-row {
+ margin-bottom: 15px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #eee;
+
+ .chart-title {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: #333;
+ }
+ }
+ }
+
+ // Chart wrapper and content
+ .chart-wrapper {
+ flex: 1;
+ position: relative;
+
+ .chart-content {
+ position: relative;
+ height: 100%;
+ min-height: 300px; // Ensure minimum height for chart
+
+ &.loading {
+ opacity: 0.7;
+
+ .chart-display {
+ filter: blur(2px);
+ }
+ }
+
+ .no-data-message {
+ text-align: center;
+ padding: 20px;
+ color: #666;
+ font-style: italic;
+ }
+
+ .chart-display {
+ position: relative;
+ height: 100%;
+ width: 100%;
+ max-width: 100%;
+ max-height: 100%;
+ transition: filter 0.3s ease;
+ }
+
+ .loading-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255, 255, 255, 0.8);
+
+ .shimmer-bar {
+ width: 80%;
+ height: 20px;
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+ border-radius: 4px;
+ }
+ }
+ }
+ }
+}
+
+@keyframes shimmer {
+ 0% {
+ background-position: -200% 0;
+ }
+ 100% {
+ background-position: 200% 0;
+ }
+}
+
+// Responsive design
+@media (max-width: 768px) {
+ .chart-container {
+ .filter-controls {
+ flex-direction: column;
+ }
+
+ .filter-item {
+ min-width: 100%;
+ }
+
+ .chart-header {
+ .header-row {
+ .chart-title {
+ font-size: 16px;
+ }
+ }
+ }
+
+ .chart-content {
+ min-height: 250px; // Adjust for mobile
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart.component.ts
index 1d2c277..292f9ea 100644
--- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart.component.ts
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bar-chart/bar-chart.component.ts
@@ -1,33 +1,1040 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, ViewChild } from '@angular/core';
+import { Dashboard3Service } from '../../../../../../services/builder/dashboard3.service';
+import { Subscription } from 'rxjs';
+import { FilterService } from '../../common-filter/filter.service';
+// Add BaseChartDirective import for chart resizing
+import { BaseChartDirective } from 'ng2-charts';
@Component({
selector: 'app-bar-chart',
templateUrl: './bar-chart.component.html',
styleUrls: ['./bar-chart.component.scss']
})
-export class BarChartComponent implements OnInit {
+export class BarChartComponent implements OnInit, OnChanges, OnDestroy {
+ @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; // Add connection input
+ // Drilldown configuration inputs
+ @Input() drilldownEnabled: boolean = false;
+ @Input() drilldownApiUrl: string;
+ @Input() drilldownXAxis: string;
+ @Input() drilldownYAxis: string;
+ @Input() drilldownParameter: string; // Add drilldown parameter input
+ @Input() baseFilters: any[] = []; // Add base filters input
+ @Input() drilldownFilters: any[] = []; // Add drilldown filters input
+ // Multi-layer drilldown configuration inputs
+ @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
- constructor() { }
+ // Add ViewChild to access the chart directive
+ @ViewChild(BaseChartDirective) chart?: BaseChartDirective;
- ngOnInit(): void {
- }
barChartLabels: string[] = ['Apple', 'Banana', 'Kiwifruit', 'Blueberry', 'Orange', 'Grapes'];
barChartType: string = 'bar';
- // barChartLegend = true;
barChartPlugins = [];
barChartData: any[] = [
{ data: [45, 37, 60, 70, 46, 33], label: 'Best Fruits' }
];
+ barChartLegend: boolean = true;
+
+ // Add responsive chart options
+ barChartOptions: any = {
+ 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
+ }
+ }
+ };
+
+ // Multi-layer drilldown state tracking
+ drilldownStack: any[] = []; // Stack to track drilldown navigation history
+ currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
+ originalBarChartLabels: string[] = [];
+ originalBarChartData: any[] = [];
+
+ // No data state
+ noDataAvailable: boolean = false;
+
+ // Loading state
+ isLoading: boolean = false;
+
+ // 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;
+ 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();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ console.log('BarChartComponent input changes:', changes);
+
+ // Initialize filter values if they haven't been initialized yet
+ if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
+ this.initializeFilterValues();
+ this.filtersInitialized = true;
+ }
+
+ // Check if any of the key properties have changed
+ 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 && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged || drilldownFiltersChanged ||
+ drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
+ drilldownLayersChanged)) {
+ console.log('Chart configuration changed, fetching new data');
+ this.fetchChartData();
+ }
+
+ // Update legend visibility if it changed
+ if (changes.chartlegend !== undefined) {
+ this.barChartLegend = changes.chartlegend.currentValue;
+ this.barChartOptions.plugins.legend.display = this.barChartLegend;
+ console.log('Chart legend changed to:', this.barChartLegend);
+ }
+ }
+
+ // 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 loading state
+ this.isLoading = true;
+
+ // Set flag to prevent recursive calls
+ this.isFetchingData = true;
+
+ // If we're in drilldown mode, fetch the appropriate drilldown data
+ if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
+ this.fetchDrilldownData();
+ // Reset flags after fetching
+ this.isFetchingData = false;
+ this.isLoading = false;
+ return;
+ }
+
+ // If we have the necessary data, fetch chart data from the service
+ if (this.table && this.xAxis && this.yAxis) {
+ console.log('=== BAR CHART DEBUG INFO ===');
+ console.log('Table:', this.table);
+ console.log('X-Axis:', this.xAxis);
+ console.log('Y-Axis:', this.yAxis);
+ console.log('Connection:', this.connection);
+
+ // Convert yAxis to string if it's an array
+ const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis;
+ console.log('Y-Axis String:', yAxisString);
+
+ // Get the parameter value from the drilldown stack for base level (should be empty)
+ let parameterValue = '';
+
+ // Log the URL that will be called
+ let url = `chart/getdashjson/bar?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
+ console.log('Bar chart data URL:', url);
+
+ // Convert baseFilters to filter parameters
+ const filterObj = {};
+
+ // Add base filters
+ if (this.baseFilters && this.baseFilters.length > 0) {
+ this.baseFilters.forEach(filter => {
+ if (filter.field && filter.value) {
+ filterObj[filter.field] = filter.value;
+ }
+ });
+ }
+
+ // Add common filters
+ const commonFilters = this.filterService.getFilterValues();
+ const filterDefinitions = this.filterService.getFilters();
+ Object.keys(commonFilters).forEach(filterId => {
+ const filterValue = commonFilters[filterId];
+
+ // Find the filter definition to get the field name
+ const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
+
+ if (filterDef && filterDef.field) {
+ const fieldName = filterDef.field;
+ if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
+ filterObj[fieldName] = filterValue;
+ }
+ }
+ });
+
+ // Convert to JSON string for API call
+ let filterParams = '';
+ if (Object.keys(filterObj).length > 0) {
+ filterParams = JSON.stringify(filterObj);
+ }
+
+ console.log('Final filter object:', filterObj);
+ // 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
+ const subscription = this.dashboardService.getChartData(this.table, 'bar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
+ (data: any) => {
+ console.log('=== BAR CHART DATA RESPONSE ===');
+ console.log('Received bar chart data:', data);
+ if (data === null) {
+ console.warn('Bar chart API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ this.barChartLabels = [];
+ this.barChartData = [];
+ // Reset flags after fetching
+ this.isFetchingData = false;
+ this.isLoading = false;
+ 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;
+ this.barChartLabels = data.chartLabels;
+ this.barChartData = data.chartData;
+ // Trigger change detection
+ // this.barChartData = [...this.barChartData];
+ console.log('Updated bar chart with data:', { labels: this.barChartLabels, data: this.barChartData });
+ console.log('=== CHART UPDATED SUCCESSFULLY ===');
+ } else if (data && data.labels && data.datasets) {
+ // Backend has already filtered the data, just display it
+ this.noDataAvailable = data.labels.length === 0;
+ this.barChartLabels = data.labels;
+ this.barChartData = data.datasets;
+ // Trigger change detection
+ // this.barChartData = [...this.barChartData];
+ console.log('Updated bar chart with legacy data format:', { labels: this.barChartLabels, data: this.barChartData });
+ console.log('=== CHART UPDATED SUCCESSFULLY (LEGACY) ===');
+ } else {
+ console.warn('Bar chart received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ this.barChartLabels = [];
+ this.barChartData = [];
+ }
+ // Reset flags after fetching
+ this.isFetchingData = false;
+ this.isLoading = false;
+ },
+ (error) => {
+ console.error('=== BAR CHART ERROR ===');
+ console.error('Error fetching bar chart data:', error);
+ this.noDataAvailable = true;
+ this.barChartLabels = [];
+ this.barChartData = [];
+ // Reset flags after fetching
+ this.isFetchingData = false;
+ this.isLoading = false;
+ // Keep default data in case of error
+ }
+ );
+
+ // Add subscription to array for cleanup
+ this.subscriptions.push(subscription);
+ } else {
+ console.log('Missing required data for bar chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
+ this.noDataAvailable = true;
+ this.barChartLabels = [];
+ this.barChartData = [];
+ // 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.barChartLabels = [];
+ this.barChartData = [];
+ 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.barChartLabels = [];
+ this.barChartData = [];
+ return;
+ }
+
+ // Get the parameter value from the drilldown stack
+ let parameterValue = '';
+ if (this.drilldownStack.length > 0) {
+ const lastEntry = this.drilldownStack[this.drilldownStack.length - 1];
+ parameterValue = lastEntry.clickedValue || '';
+ console.log('Parameter value from last click:', parameterValue);
+ }
+
+ // Get the parameter field from drilldown config
+ const parameterField = drilldownConfig.parameter || '';
+ console.log('Parameter field:', parameterField);
+
+ console.log('Fetching drilldown data for level:', this.currentDrilldownLevel, {
+ apiUrl: drilldownConfig.apiUrl,
+ xAxis: drilldownConfig.xAxis,
+ yAxis: drilldownConfig.yAxis,
+ parameterField: parameterField,
+ parameterValue: parameterValue,
+ connection: this.connection
+ });
+
+ // Build the actual API URL with parameter replacement
+ let actualApiUrl = drilldownConfig.apiUrl;
+ console.log('Original API URL:', actualApiUrl);
+ console.log('Parameter value to use:', parameterValue);
+ console.log('Parameter field:', parameterField);
+
+ // Check if the URL contains angle brackets for parameter replacement
+ const hasAngleBrackets = /<[^>]+>/.test(actualApiUrl);
+
+ if (hasAngleBrackets && parameterValue) {
+ // Replace angle brackets placeholder with actual value
+ console.log('Replacing angle brackets with parameter value');
+ const encodedValue = encodeURIComponent(parameterValue);
+ actualApiUrl = actualApiUrl.replace(/<[^>]+>/g, encodedValue);
+ console.log('URL after angle bracket replacement:', actualApiUrl);
+ }
+
+ // Log the URL that will be called
+ let url = `chart/getdashjson/bar?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
+ if (parameterField && parameterValue) {
+ url += `¶meter=${encodeURIComponent(parameterField)}¶meterValue=${encodeURIComponent(parameterValue)}`;
+ }
+ console.log('Drilldown data URL:', url);
+
+ // Convert drilldown layer filters to filter parameters (if applicable)
+ const filterObj = {};
+
+ // Add drilldown layer filters
+ if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
+ drilldownConfig.filters.forEach((filter: any) => {
+ if (filter.field && filter.value) {
+ filterObj[filter.field] = filter.value;
+ }
+ });
+ }
+
+ // Add drilldownFilters
+ if (this.drilldownFilters && this.drilldownFilters.length > 0) {
+ this.drilldownFilters.forEach(filter => {
+ if (filter.field && filter.value) {
+ filterObj[filter.field] = filter.value;
+ }
+ });
+ }
+
+ // Add common filters
+ const commonFilters = this.filterService.getFilterValues();
+ const filterDefinitions = this.filterService.getFilters();
+ Object.keys(commonFilters).forEach(filterId => {
+ const filterValue = commonFilters[filterId];
+
+ // Find the filter definition to get the field name
+ const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
+
+ if (filterDef && filterDef.field) {
+ const fieldName = filterDef.field;
+ if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
+ filterObj[fieldName] = filterValue;
+ }
+ }
+ });
+
+ // Convert to JSON string for API call
+ let drilldownFilterParams = '';
+ if (Object.keys(filterObj).length > 0) {
+ drilldownFilterParams = JSON.stringify(filterObj);
+ }
+
+ console.log('Drilldown filter parameters:', drilldownFilterParams);
+
+ // For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters
+ const subscription = this.dashboardService.getChartData(
+ drilldownConfig.apiUrl, 'bar',
+ this.drilldownXAxis, this.drilldownYAxis,
+ this.connection,
+ '', parameterValue,
+ drilldownFilterParams
+ ).subscribe(
+ (data: any) => {
+ console.log('Received drilldown data:', data);
+ if (data === null) {
+ console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ this.barChartLabels = [];
+ this.barChartData = [];
+ 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;
+ this.barChartLabels = data.chartLabels;
+ this.barChartData = data.chartData;
+ // Trigger change detection
+ // this.barChartData = [...this.barChartData];
+ console.log('Updated bar chart with drilldown data:', { labels: this.barChartLabels, data: this.barChartData });
+ // Set loading state to false
+ this.isLoading = false;
+ } else if (data && data.labels && data.datasets) {
+ // Backend has already filtered the data, just display it
+ this.noDataAvailable = data.labels.length === 0;
+ this.barChartLabels = data.labels;
+ this.barChartData = data.datasets;
+ console.log('Updated bar chart with drilldown legacy data format:', { labels: this.barChartLabels, data: this.barChartData });
+ // Set loading state to false
+ this.isLoading = false;
+ } else {
+ console.warn('Drilldown received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ this.barChartLabels = [];
+ this.barChartData = [];
+ // Set loading state to false
+ this.isLoading = false;
+ }
+ },
+ (error) => {
+ console.error('Error fetching drilldown data:', error);
+ this.noDataAvailable = true;
+ this.barChartLabels = [];
+ this.barChartData = [];
+ // Set loading state to false
+ this.isLoading = false;
+ // Keep current data in case of error
+ }
+ );
+
+ // Add subscription to array for cleanup
+ this.subscriptions.push(subscription);
+
+ // Set loading state
+ this.isLoading = true;
+ }
+
+ // Reset to original data (go back to base level)
+ resetToOriginalData(): void {
+ console.log('Resetting to original data');
+ console.log('Current stack before reset:', this.drilldownStack);
+ console.log('Current level before reset:', this.currentDrilldownLevel);
+
+ this.currentDrilldownLevel = 0;
+ this.drilldownStack = [];
+
+ if (this.originalBarChartLabels.length > 0) {
+ // Create a deep copy to avoid reference issues
+ this.barChartLabels = JSON.parse(JSON.stringify(this.originalBarChartLabels));
+ console.log('Restored original labels');
+ }
+ if (this.originalBarChartData.length > 0) {
+ // Create a deep copy to avoid reference issues
+ this.barChartData = JSON.parse(JSON.stringify(this.originalBarChartData));
+ console.log('Restored original data');
+ }
+
+ console.log('After reset - labels:', this.barChartLabels);
+ console.log('After reset - data:', this.barChartData);
+
+ // 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();
+ }
+ }
+
+ // Public method to refresh data when filters change
+ refreshData(): void {
+ this.fetchChartData();
+ }
+
+ // Method to handle window resize events
+ onResize(): void {
+ if (this.chart) {
+ this.chart.chart?.resize();
+ }
+ }
+
+ // Ensure labels and data arrays have the same length
+ private syncLabelAndDataArrays(): void {
+ // For bar charts, we need to ensure all datasets have the same number of data points
+ if (this.barChartData && this.barChartData.length > 0 && this.barChartLabels) {
+ const labelCount = this.barChartLabels.length;
+
+ this.barChartData.forEach(dataset => {
+ if (dataset.data) {
+ // If dataset has more data points than labels, truncate the data
+ if (dataset.data.length > labelCount) {
+ dataset.data = dataset.data.slice(0, labelCount);
+ }
+ // If dataset has fewer data points than labels, pad with zeros
+ else if (dataset.data.length < labelCount) {
+ while (dataset.data.length < labelCount) {
+ dataset.data.push(0);
+ }
+ }
+ }
+ });
+ }
+ }
// events
- public chartClicked(e: any): void {
- console.log(e);
- }
+ public chartClicked(e: any): void {
+ console.log('Bar 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 label of the clicked element
+ const clickedLabel = this.barChartLabels[clickedIndex];
+
+ console.log('Clicked on bar:', { 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
+ // Create a deep copy to avoid reference issues
+ this.originalBarChartLabels = JSON.parse(JSON.stringify(this.barChartLabels));
+ // Create a deep copy to avoid reference issues
+ this.originalBarChartData = JSON.parse(JSON.stringify(this.barChartData));
+ 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,
+ clickedIndex: clickedIndex,
+ clickedLabel: clickedLabel,
+ clickedValue: clickedLabel // Using label 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(e);
- }
-
-}
+ public chartHovered(e: any): void {
+ console.log(e);
+ }
+
+ ngOnDestroy() {
+ // Unsubscribe from all subscriptions to prevent memory leaks
+ console.log('BarChartComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
+ this.subscriptions.forEach(subscription => {
+ if (subscription && !subscription.closed) {
+ subscription.unsubscribe();
+ }
+ });
+ this.subscriptions = [];
+
+ // Clear data to help with garbage collection
+ this.barChartLabels = [];
+ this.barChartData = [];
+ this.drilldownStack = [];
+ this.originalBarChartLabels = [];
+ this.originalBarChartData = [];
+
+ // Clear multiselect tracking
+ this.openMultiselects.clear();
+
+ // Remove document click handler
+ this.removeDocumentClickHandler();
+
+ console.log('BarChartComponent destroyed and cleaned up');
+ }
+}
\ No newline at end of file
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 5f6157d..e2c52aa 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,9 +1,310 @@
-
-
-
-
+
+
+
+
+
0">
+
Base Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
0 && currentDrilldownLevel > 0">
+
Drilldown Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+
Layer Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+ Clear All Filters
+
+
+
+
+
+
+
+
0">
+
+
+
+
+
+
+
+
+ Drilldown Level: {{currentDrilldownLevel}}
+ 0">
+ (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
+
+
+
+
+
+
+
+
+
+
+
+
+ Loading data...
+
+
+
+
+ No data available
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bubble-chart/bubble-chart.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bubble-chart/bubble-chart.component.scss
index e69de29..a9282e4 100644
--- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bubble-chart/bubble-chart.component.scss
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bubble-chart/bubble-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/bubble-chart/bubble-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bubble-chart/bubble-chart.component.ts
index b01a8b0..bd9d0fc 100644
--- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bubble-chart/bubble-chart.component.ts
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/bubble-chart/bubble-chart.component.ts
@@ -1,98 +1,1328 @@
-import { Component, OnInit } from '@angular/core';
-import { ChartConfiguration, ChartDataset, ChartOptions } from 'chart.js';
+import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { ChartConfiguration, ChartDataset, ChartOptions } from 'chart.js';
+import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
+import { FilterService } from '../../common-filter/filter.service';
+import { Subscription } from 'rxjs';
+
@Component({
selector: 'app-bubble-chart',
templateUrl: './bubble-chart.component.html',
styleUrls: ['./bubble-chart.component.scss']
})
-export class BubbleChartComponent implements OnInit {
+export class BubbleChartComponent implements OnInit, OnChanges {
+ @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; // Add connection input
+ // Drilldown configuration inputs
+ @Input() drilldownEnabled: boolean = false;
+ @Input() drilldownApiUrl: string;
+ @Input() drilldownXAxis: string;
+ @Input() drilldownYAxis: string;
+ @Input() drilldownParameter: string; // Add drilldown parameter input
+ @Input() baseFilters: any[] = []; // Add base filters input
+ @Input() drilldownFilters: any[] = []; // Add drilldown filters input
+ // Multi-layer drilldown configuration inputs
+ @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
- constructor() { }
-
- ngOnInit(): void {
- }
public bubbleChartOptions: ChartConfiguration['options'] = {
- // scales: {
- // x: {
- // min: 0,
- // max: 30,
- // ticks: {}
- // },
- // y: {
- // min: 0,
- // max: 30,
- // ticks: {}
- // },
- // plugins: {
- // title: {
- // display: true,
- // text: 'Bubble Chart'
- // }
- // }
- // }
+ 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'
+ },
+ // Enable individual point styling
+ elements: {
+ point: {
+ hoverRadius: 12,
+ hoverBorderWidth: 3
+ }
+ },
+ // Add padding to ensure x-axis labels are visible
+ layout: {
+ padding: {
+ left: 10,
+ right: 10,
+ top: 10,
+ bottom: 30
+ }
+ }
};
public bubbleChartType: string = 'bubble';
- // public bubbleChartLegend = true;
- public bubbleChartData: 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: 'Investment Equities',
- backgroundColor: 'rgba(255, 0, 0, 0.6)', // Red
- borderColor: 'blue',
- hoverBackgroundColor: 'purple',
- hoverBorderColor: 'red',
- },
- {
- data: [
- { x: 5, y: 15, r: 12 },
- { x: 20, y: 7, r: 8 },
- { x: 12, y: 18, r: 15 },
- { x: 8, y: 6, r: 10 },
- ],
- label: 'Investment Bonds',
- backgroundColor: 'rgba(0, 255, 0, 0.6)', // Green
- borderColor: 'green',
- hoverBackgroundColor: 'yellow',
- hoverBorderColor: 'blue',
- },
- // {
- // 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: 'Investment Equities',
- // backgroundColor: [
- // 'red',
- // 'green',
- // 'blue',
- // 'purple',
- // 'yellow',
- // 'brown',
- // 'magenta',
- // 'cyan',
- // 'orange',
- // 'pink'
- // ],
- // borderColor: 'blue',
- // hoverBackgroundColor: 'purple',
- // hoverBorderColor: 'red',
- // },
- ];
+ public bubbleChartData: ChartDataset[] = [];
+
+ // Multi-layer drilldown state tracking
+ drilldownStack: any[] = []; // Stack to track drilldown navigation history
+ currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
+ originalBubbleChartData: ChartDataset[] = [];
+
+ // No data state
+ noDataAvailable: boolean = false;
+ dataLoaded: boolean = false; // Track if data has been loaded
+
+ // Flag to prevent infinite loops
+ private isFetchingData: boolean = false;
+
+ // Subscriptions to unsubscribe on destroy
+ private subscriptions: Subscription[] = [];
- // events
- public chartClicked(e: any): void {
- console.log(e);
- }
+ // 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;
- public chartHovered(e: any): void {
- console.log(e);
- }
-}
+ 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;
+ 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 && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged || drilldownFiltersChanged ||
+ drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
+ drilldownLayersChanged)) {
+ console.log('Chart configuration changed, fetching new data');
+ 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();
+ }
+
+ // 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})`);
+ }
+
+ // 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 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);
+ }
+
+ // 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 });
+
+ // 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
+ // For fewer points, we can use larger bubbles; for more points, smaller bubbles to prevent overlap
+ 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++) {
+ // Log each point for debugging
+ console.log(`Processing point ${i}: label=${labels[i]}, yValue=${yValues[i]}, type=${typeof yValues[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)), // Slightly more opaque border
+ 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 empty dataset instead of default data
+ return [];
+ }
+
+ fetchChartData(): void {
+ // Set flag to prevent recursive calls
+ this.isFetchingData = true;
+ this.dataLoaded = false; // Mark data as not loaded yet
+ this.noDataAvailable = false; // Reset no data flag
+
+ console.log('Starting fetchChartData, current state:', {
+ table: this.table,
+ xAxis: this.xAxis,
+ yAxis: this.yAxis,
+ connection: this.connection
+ });
+
+ // 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();
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ return;
+ }
+
+ // If we have the necessary data, fetch chart data from the service
+ if (this.table && this.xAxis && this.yAxis) {
+ console.log('Fetching bubble chart data for:', { 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);
+
+ // 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}` : ''}`;
+ console.log('Bubble 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
+ this.dashboardService.getChartData(this.table, 'bubble', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
+ (data: any) => {
+ console.log('Received bubble chart data:', data);
+
+ // Reset chart data to empty first
+ this.bubbleChartData = [];
+
+ if (data === null || data === undefined) {
+ console.warn('Bubble chart API returned null/undefined data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ } else 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}
+ console.log('Processing chartLabels and chartData format');
+ const transformedData = this.transformToBubbleData(data.chartLabels, data.chartData);
+ console.log('Transformed data:', transformedData);
+
+ // Check if we have valid data
+ let hasValidData = false;
+ if (transformedData && transformedData.length > 0) {
+ for (const dataset of transformedData) {
+ if (dataset.data && dataset.data.length > 0) {
+ hasValidData = true;
+ break;
+ }
+ }
+ }
+
+ if (hasValidData) {
+ // Create a new array reference to trigger change detection
+ this.bubbleChartData = [...transformedData];
+ this.noDataAvailable = false;
+ console.log('Updated bubble chart with data:', this.bubbleChartData);
+ } else {
+ console.log('No valid data after transformation');
+ this.noDataAvailable = true;
+ }
+ } else if (data && data.labels && data.datasets) {
+ // Handle the original expected format as fallback
+ console.log('Processing labels and datasets format');
+ // Check if we have valid data
+ let hasValidData = false;
+ if (data.datasets && data.datasets.length > 0) {
+ for (const dataset of data.datasets) {
+ if (dataset.data && dataset.data.length > 0) {
+ hasValidData = true;
+ break;
+ }
+ }
+ }
+
+ if (hasValidData) {
+ // Create a new array reference to trigger change detection
+ this.bubbleChartData = [...data.datasets];
+ this.noDataAvailable = false;
+ console.log('Updated bubble chart with legacy data format:', this.bubbleChartData);
+ } else {
+ console.log('No valid data in legacy format');
+ this.noDataAvailable = true;
+ }
+ } else {
+ console.warn('Bubble chart received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ }
+
+ this.dataLoaded = true; // Mark data as loaded
+
+ console.log('Final state after data fetch:', {
+ noDataAvailable: this.noDataAvailable,
+ dataLoaded: this.dataLoaded,
+ bubbleChartDataLength: this.bubbleChartData.length,
+ isChartDataValid: this.isChartDataValid()
+ });
+
+ // Trigger change detection with a small delay to ensure proper rendering
+ setTimeout(() => {
+ this.forceChartUpdate();
+ }, 100);
+
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ },
+ (error) => {
+ console.error('Error fetching bubble chart data:', error);
+ this.noDataAvailable = true;
+ this.bubbleChartData = [];
+ this.dataLoaded = true;
+ // Trigger change detection
+ setTimeout(() => {
+ this.forceChartUpdate();
+ }, 100);
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ }
+ );
+ } else {
+ console.log('Missing required data for bubble chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
+ this.noDataAvailable = true;
+ this.bubbleChartData = [];
+ this.dataLoaded = true;
+ // Trigger change detection
+ setTimeout(() => {
+ this.forceChartUpdate();
+ }, 100);
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ }
+ }
+
+ // Fetch drilldown data based on current drilldown level
+ fetchDrilldownData(): void {
+ console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
+ console.log('Drilldown stack:', this.drilldownStack);
+
+ // Get the current drilldown configuration based on the current level
+ let drilldownConfig;
+ if (this.currentDrilldownLevel === 1) {
+ // Base drilldown level
+ drilldownConfig = {
+ apiUrl: this.drilldownApiUrl,
+ xAxis: this.drilldownXAxis,
+ yAxis: this.drilldownYAxis,
+ parameter: this.drilldownParameter
+ };
+ } else {
+ // Multi-layer drilldown level
+ const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) {
+ drilldownConfig = this.drilldownLayers[layerIndex];
+ } else {
+ console.warn('Invalid drilldown layer index:', layerIndex);
+ this.noDataAvailable = true;
+ this.bubbleChartData = [];
+ this.dataLoaded = true;
+ setTimeout(() => {
+ this.forceChartUpdate();
+ }, 100);
+ 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.bubbleChartData = [];
+ this.dataLoaded = true;
+ setTimeout(() => {
+ this.forceChartUpdate();
+ }, 100);
+ 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)
+ let filterParams = '';
+ if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
+ const filterObj = {};
+ drilldownConfig.filters.forEach((filter: any) => {
+ if (filter.field && filter.value) {
+ filterObj[filter.field] = filter.value;
+ }
+ });
+ if (Object.keys(filterObj).length > 0) {
+ filterParams = JSON.stringify(filterObj);
+ }
+ }
+
+ // Add common filters to drilldown filter parameters
+ const commonFilters = this.filterService.getFilterValues();
+ if (Object.keys(commonFilters).length > 0) {
+ // Merge common filters with drilldown filters
+ const mergedFilterObj = {};
+
+ // Add drilldown filters first
+ if (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);
+
+ // 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, 'bubble', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, filterParams).subscribe(
+ (data: any) => {
+ console.log('Received drilldown data:', data);
+
+ // Reset chart data to empty first
+ this.bubbleChartData = [];
+
+ if (data === null || data === undefined) {
+ console.warn('Drilldown API returned null/undefined data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ } else if (data && data.chartLabels && data.chartData) {
+ // For bubble charts, we need to transform the data into bubble format
+ const transformedData = this.transformToBubbleData(data.chartLabels, data.chartData);
+
+ // Check if we have valid data
+ let hasValidData = false;
+ if (transformedData && transformedData.length > 0) {
+ for (const dataset of transformedData) {
+ if (dataset.data && dataset.data.length > 0) {
+ hasValidData = true;
+ break;
+ }
+ }
+ }
+
+ if (hasValidData) {
+ this.bubbleChartData = transformedData;
+ this.noDataAvailable = false;
+ console.log('Updated bubble chart with drilldown data:', this.bubbleChartData);
+ } else {
+ console.log('No valid data after transformation in drilldown');
+ this.noDataAvailable = true;
+ }
+ } else if (data && data.labels && data.datasets) {
+ // Handle the original expected format as fallback
+ // Check if we have valid data
+ let hasValidData = false;
+ if (data.datasets && data.datasets.length > 0) {
+ for (const dataset of data.datasets) {
+ if (dataset.data && dataset.data.length > 0) {
+ hasValidData = true;
+ break;
+ }
+ }
+ }
+
+ if (hasValidData) {
+ this.bubbleChartData = data.datasets;
+ this.noDataAvailable = false;
+ console.log('Updated bubble chart with drilldown legacy data format:', this.bubbleChartData);
+ } else {
+ console.log('No valid data in legacy format in drilldown');
+ this.noDataAvailable = true;
+ }
+ } else {
+ console.warn('Drilldown received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ }
+
+ this.dataLoaded = true; // Mark data as loaded
+
+ console.log('Final state after drilldown data fetch:', {
+ noDataAvailable: this.noDataAvailable,
+ dataLoaded: this.dataLoaded,
+ bubbleChartDataLength: this.bubbleChartData.length,
+ isChartDataValid: this.isChartDataValid()
+ });
+
+ // Trigger change detection
+ setTimeout(() => {
+ this.forceChartUpdate();
+ }, 100);
+ },
+ (error) => {
+ console.error('Error fetching drilldown data:', error);
+ this.noDataAvailable = true;
+ this.bubbleChartData = [];
+ this.dataLoaded = true;
+ setTimeout(() => {
+ this.forceChartUpdate();
+ }, 100);
+ }
+ );
+ }
+
+ // 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.originalBubbleChartData.length > 0) {
+ this.bubbleChartData = [...this.originalBubbleChartData];
+ console.log('Restored original data');
+ }
+
+ console.log('After reset - data:', this.bubbleChartData);
+
+ // 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();
+ }
+ }
+
+ // events
+ public chartClicked(e: any): void {
+ console.log('Bubble 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
+ 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 = 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('Bubble chart hovered:', e);
+ }
+
+ // Method to check if chart data is valid
+ public isChartDataValid(): boolean {
+ console.log('Checking if chart data is valid:', this.bubbleChartData);
+ if (!this.bubbleChartData || this.bubbleChartData.length === 0) {
+ console.log('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 chart data found');
+ return false;
+ }
+
+ // Method to force chart update
+ private forceChartUpdate(): void {
+ console.log('Forcing chart update');
+ console.log('Current bubbleChartData:', this.bubbleChartData);
+ console.log('Current bubbleChartData length:', this.bubbleChartData ? this.bubbleChartData.length : 0);
+ if (this.bubbleChartData && this.bubbleChartData.length > 0) {
+ console.log('First dataset data length:', this.bubbleChartData[0].data ? this.bubbleChartData[0].data.length : 0);
+ }
+ // Create a new reference to trigger change detection
+ if (this.bubbleChartData) {
+ this.bubbleChartData = [...this.bubbleChartData];
+ }
+ // Also update noDataAvailable to trigger UI changes
+ this.noDataAvailable = this.noDataAvailable;
+ console.log('Chart update forced, noDataAvailable:', this.noDataAvailable);
+ console.log('Chart update forced, bubbleChartData length:', this.bubbleChartData ? this.bubbleChartData.length : 0);
+ console.log('Chart update forced, isChartDataValid:', this.isChartDataValid());
+ }
+}
\ 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 cab5b29..1acc617 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,8 +1,315 @@
-
-
-
+
+
+
+
+
0">
+
Base Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
0 && currentDrilldownLevel > 0">
+
Drilldown Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+
Layer Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+ Clear All Filters
+
+
+
+
+
+
+
+
+
+
+
No chart data available
+
+
+
+
+
+
+
+
+
+
+
+
0">
+
+
+ {{ label }}
+ {{ doughnutChartData && doughnutChartData[0] && doughnutChartData[0].data && doughnutChartData[0].data[i] !== undefined ? doughnutChartData[0].data[i] : 0 }}
+
+
+
\ 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.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/doughnut-chart/doughnut-chart.component.scss
index e69de29..f682297 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
@@ -0,0 +1,343 @@
+// Chart container structure - simplified to match shield dashboard
+.chart-container {
+ height: 100%;
+ min-height: 400px; // Ensure minimum height
+ display: flex;
+ flex-direction: column;
+
+ // Filter section styling
+ .filter-section {
+ margin-bottom: 20px;
+ padding: 15px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ background-color: #f9f9f9;
+ }
+
+ .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;
+ }
+ }
+
+ // Chart header styling
+ .chart-header {
+ margin-bottom: 20px;
+
+ .header-row {
+ margin-bottom: 15px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #eee;
+
+ .chart-title {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: #0a192f;
+ }
+ }
+ }
+
+ // Chart wrapper and content - simplified to match shield dashboard
+ .chart-wrapper {
+ flex: 1;
+ position: relative;
+
+ .chart-content {
+ position: relative;
+ height: 100%;
+ min-height: 300px;
+
+ &.loading {
+ opacity: 0.7;
+
+ canvas {
+ filter: blur(2px);
+ }
+ }
+
+ .no-data-message {
+ text-align: center;
+ padding: 20px;
+ color: #666;
+ font-style: italic;
+ }
+
+ canvas {
+ max-width: 100%;
+ max-height: calc(100% - 40px); // Leave space for legend
+ transition: filter 0.3s ease;
+ }
+
+ .loading-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255, 255, 255, 0.8);
+
+ .shimmer-donut {
+ width: 120px;
+ height: 120px;
+ border-radius: 50%;
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+ }
+ }
+ }
+ }
+
+ // Chart legend - simplified
+ .chart-legend {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 15px;
+ margin: 20px 0;
+ padding: 15px;
+
+ .legend-item {
+ display: flex;
+ align-items: center;
+ padding: 8px 15px;
+ background: #f8f9fa;
+ border-radius: 20px;
+ border: 1px solid #e9ecef;
+ min-width: 120px;
+ justify-content: space-between;
+ }
+
+ .legend-color {
+ width: 15px;
+ height: 15px;
+ border-radius: 50%;
+ margin-right: 8px;
+ display: inline-block;
+ flex-shrink: 0;
+ }
+
+ .legend-label {
+ font-size: 14px;
+ font-weight: 500;
+ color: #2c3e50;
+ margin-right: 10px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .legend-value {
+ font-size: 14px;
+ font-weight: 600;
+ color: #3498db;
+ min-width: 30px;
+ text-align: right;
+ }
+ }
+}
+
+@keyframes shimmer {
+ 0% {
+ background-position: -200% 0;
+ }
+ 100% {
+ background-position: 200% 0;
+ }
+}
+
+// Responsive design
+@media (max-width: 768px) {
+ .chart-container {
+ .filter-controls {
+ flex-direction: column;
+ }
+
+ .filter-item {
+ min-width: 100%;
+ }
+
+ .chart-header {
+ .header-row {
+ .chart-title {
+ font-size: 16px;
+ }
+ }
+ }
+
+ .chart-content {
+ min-height: 250px;
+ }
+
+ .chart-legend {
+ flex-direction: column;
+ align-items: center;
+
+ .legend-item {
+ width: 100%;
+ max-width: 300px;
+ justify-content: space-between;
+ }
+ }
+
+ .chart-content {
+ min-height: 250px;
+
+ canvas {
+ max-height: calc(100% - 60px); // More space for legend on mobile
+ }
+ }
+ }
+}
\ 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 137455b..09fc78e 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
@@ -1,30 +1,1132 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked, OnDestroy } 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-doughnut-chart',
templateUrl: './doughnut-chart.component.html',
styleUrls: ['./doughnut-chart.component.scss']
})
-export class DoughnutChartComponent implements OnInit {
- public doughnutChartLabels: string[] = [
- "Download Sales",
- "In-Store Sales",
- "Mail-Order Sales"
- ];
- public doughnutChartData: number[] = [350, 450, 100];
- public doughnutChartType: string = "doughnut";
+export class DoughnutChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
+ @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; // 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
- // events
- public chartClicked(e: any): void {
- console.log(e);
- }
+ public doughnutChartLabels: string[] = ["Category A", "Category B", "Category C"];
+ public doughnutChartData: any[] = [
+ {
+ data: [30, 50, 20],
+ backgroundColor: [
+ '#FF6384',
+ '#36A2EB',
+ '#FFCE56'
+ ],
+ hoverBackgroundColor: [
+ '#FF6384',
+ '#36A2EB',
+ '#FFCE56'
+ ]
+ }
+ ];
+ public doughnutChartType: string = "doughnut";
+ public doughnutChartOptions: any = {
+ responsive: true,
+ maintainAspectRatio: false,
+ cutout: '60%', // This creates the doughnut effect (Chart.js v3+ syntax)
+ plugins: {
+ legend: {
+ display: false // We'll create our own legend
+ },
+ tooltip: {
+ enabled: true,
+ mode: 'index',
+ intersect: false,
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
+ titleFont: {
+ size: 16,
+ color: '#fff'
+ },
+ bodyFont: {
+ size: 14,
+ color: '#fff'
+ },
+ cornerRadius: 4,
+ displayColors: false
+ }
+ },
+ animation: {
+ animateRotate: true,
+ animateScale: false
+ },
+ elements: {
+ arc: {
+ borderWidth: 2,
+ borderColor: '#fff'
+ }
+ },
+ layout: {
+ padding: {
+ top: 20,
+ bottom: 20,
+ left: 20,
+ right: 20
+ }
+ }
+ };
+
+ // Chart colors for consistent styling
+ private chartColors: string[] = [
+ '#FF6384',
+ '#36A2EB',
+ '#FFCE56',
+ '#4BC0C0',
+ '#9966FF',
+ '#FF9F40',
+ '#FF6384',
+ '#C9CBCF'
+ ];
+
+ // Multi-layer drilldown state tracking
+ drilldownStack: any[] = []; // Stack to track drilldown navigation history
+ currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
+ originalDoughnutChartLabels: string[] = [];
+ originalDoughnutChartData: number[] = [];
+
+ // No data state
+ noDataAvailable: boolean = false;
+
+ // Loading state
+ isLoading: boolean = false;
+
+ // Flag to prevent infinite loops
+ private isFetchingData: boolean = false;
+
+ // Subscriptions to unsubscribe on destroy
+ private subscriptions: Subscription[] = [];
- public chartHovered(e: any): void {
- console.log(e);
- }
- constructor() { }
+ // 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
+ ) { }
ngOnInit(): void {
+ // Subscribe to filter changes
+ this.subscriptions.push(
+ this.filterService.filterState$.subscribe(filters => {
+ // When filters change, refresh the chart data
+ this.fetchChartData();
+ })
+ );
+
+ // Validate initial data
+ this.validateChartData();
+ // Only fetch data if we have the required inputs, otherwise show default data
+ if (this.table && this.xAxis && this.yAxis) {
+ this.fetchChartData();
+ }
+ }
+
+ /**
+ * Validate and sanitize chart data
+ */
+ private validateChartData(): void {
+ // Ensure we have valid arrays
+ if (!Array.isArray(this.doughnutChartLabels)) {
+ this.doughnutChartLabels = [];
+ }
+
+ if (!Array.isArray(this.doughnutChartData)) {
+ this.doughnutChartData = [];
+ }
+
+ // Ensure we have some data to display
+ if (this.doughnutChartLabels.length === 0 && this.doughnutChartData.length === 0) {
+ // Add default data to ensure chart visibility
+ this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
+ this.doughnutChartData = [
+ {
+ data: [30, 50, 20],
+ backgroundColor: [
+ '#FF6384',
+ '#36A2EB',
+ '#FFCE56'
+ ],
+ hoverBackgroundColor: [
+ '#FF6384',
+ '#36A2EB',
+ '#FFCE56'
+ ]
+ }
+ ];
+ }
+
+ // Ensure we have matching arrays
+ if (this.doughnutChartLabels.length !== (this.doughnutChartData[0]?.data?.length || 0)) {
+ const maxLength = Math.max(this.doughnutChartLabels.length, this.doughnutChartData[0]?.data?.length || 0);
+ while (this.doughnutChartLabels.length < maxLength) {
+ this.doughnutChartLabels.push(`Label ${this.doughnutChartLabels.length + 1}`);
+ }
+ if (this.doughnutChartData[0]) {
+ while (this.doughnutChartData[0].data.length < maxLength) {
+ this.doughnutChartData[0].data.push(0);
+ }
+ }
+ }
+ }
+
+ /**
+ * Force chart redraw
+ */
+ public redrawChart(): void {
+ // This method can be called to force a chart redraw if needed
+ console.log('Redrawing doughnut chart');
}
-}
+ 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;
+ 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 && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged || drilldownFiltersChanged ||
+ drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
+ drilldownLayersChanged)) {
+ console.log('Chart configuration changed, fetching new data');
+ this.fetchChartData();
+ }
+
+ // If we have the required inputs and haven't fetched data yet, fetch it
+ if ((xAxisChanged || yAxisChanged || tableChanged) && this.table && this.xAxis && this.yAxis && !this.isFetchingData) {
+ console.log('Required inputs available, fetching data');
+ this.fetchChartData();
+ }
+ }
+
+ ngAfterViewChecked() {
+ // Debugging: Log component state after view checks
+ console.log('DoughnutChartComponent state:', {
+ labels: this.doughnutChartLabels,
+ data: this.doughnutChartData,
+ hasData: this.doughnutChartLabels.length > 0 && this.doughnutChartData.length > 0
+ });
+ }
+
+ 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
+ refreshData(): void {
+ this.fetchChartData();
+ }
+
+ fetchChartData(): void {
+ // Set loading state
+ this.isLoading = true;
+
+ // Set flag to prevent recursive calls
+ this.isFetchingData = true;
+
+ // If we're in drilldown mode, fetch the appropriate drilldown data
+ if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
+ this.fetchDrilldownData();
+ // Reset flags after fetching
+ this.isFetchingData = false;
+ this.isLoading = false;
+ return;
+ }
+
+ // If we have the necessary data, fetch chart data from the service
+ if (this.table && this.xAxis && this.yAxis) {
+ console.log('Fetching doughnut chart data for:', { 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);
+
+ // 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('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
+ this.dashboardService.getChartData(this.table, 'doughnut', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
+ (data: any) => {
+ console.log('Received doughnut chart data:', data);
+ if (data === null) {
+ console.warn('API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ this.doughnutChartLabels = [];
+ this.doughnutChartData = [];
+ // Reset flags after fetching
+ this.isFetchingData = false;
+ this.isLoading = false;
+ 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;
+ this.doughnutChartLabels = data.chartLabels;
+
+ // Handle different data structures
+ let chartDataValues;
+ if (Array.isArray(data.chartData)) {
+ // If chartData is already an array of values
+ if (data.chartData.length > 0 && typeof data.chartData[0] !== 'object') {
+ chartDataValues = data.chartData;
+ }
+ // If chartData is an array with one object containing the data
+ else if (data.chartData.length > 0 && data.chartData[0].data) {
+ chartDataValues = data.chartData[0].data;
+ }
+ // Default case
+ else {
+ chartDataValues = data.chartData;
+ }
+ } else {
+ chartDataValues = [data.chartData];
+ }
+
+ this.doughnutChartData = [
+ {
+ data: chartDataValues,
+ backgroundColor: this.chartColors.slice(0, chartDataValues.length),
+ hoverBackgroundColor: this.chartColors.slice(0, chartDataValues.length)
+ }
+ ];
+ console.log('Updated doughnut chart with data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
+ } 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.datasets;
+ console.log('Updated doughnut chart with legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
+ } else {
+ console.warn('Received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ // Keep default data instead of empty arrays
+ this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
+ this.doughnutChartData = [
+ {
+ data: [30, 50, 20],
+ backgroundColor: [
+ '#FF6384',
+ '#36A2EB',
+ '#FFCE56'
+ ],
+ hoverBackgroundColor: [
+ '#FF6384',
+ '#36A2EB',
+ '#FFCE56'
+ ]
+ }
+ ];
+ }
+ // Reset flags after fetching
+ this.isFetchingData = false;
+ this.isLoading = false;
+ },
+ (error) => {
+ console.error('Error fetching doughnut chart data:', error);
+ this.noDataAvailable = true;
+ // Keep default data in case of error
+ this.doughnutChartLabels = ["Category A", "Category B", "Category C"];
+ this.doughnutChartData = [
+ {
+ data: [30, 50, 20],
+ backgroundColor: [
+ '#FF6384',
+ '#36A2EB',
+ '#FFCE56'
+ ],
+ hoverBackgroundColor: [
+ '#FF6384',
+ '#36A2EB',
+ '#FFCE56'
+ ]
+ }
+ ];
+ // Reset flags after fetching
+ this.isFetchingData = false;
+ this.isLoading = false;
+ }
+ );
+ } else {
+ 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 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.doughnutChartLabels = [];
+ this.doughnutChartData = [];
+ 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.doughnutChartLabels = [];
+ this.doughnutChartData = [];
+ 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)
+ let filterParams = '';
+ if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
+ const filterObj = {};
+ drilldownConfig.filters.forEach((filter: any) => {
+ if (filter.field && filter.value) {
+ filterObj[filter.field] = filter.value;
+ }
+ });
+ if (Object.keys(filterObj).length > 0) {
+ filterParams = JSON.stringify(filterObj);
+ }
+ }
+
+ // Add common filters to drilldown filter parameters
+ const commonFilters = this.filterService.getFilterValues();
+ if (Object.keys(commonFilters).length > 0) {
+ // Merge common filters with drilldown filters
+ const mergedFilterObj = {};
+
+ // Add drilldown filters first
+ if (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);
+
+ // 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, 'doughnut', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, filterParams).subscribe(
+ (data: any) => {
+ console.log('Received drilldown data:', data);
+ if (data === null) {
+ console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ this.doughnutChartLabels = [];
+ this.doughnutChartData = [];
+ 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;
+ this.doughnutChartLabels = data.chartLabels;
+
+ // Handle different data structures
+ let chartDataValues;
+ if (Array.isArray(data.chartData)) {
+ // If chartData is already an array of values
+ if (data.chartData.length > 0 && typeof data.chartData[0] !== 'object') {
+ chartDataValues = data.chartData;
+ }
+ // If chartData is an array with one object containing the data
+ else if (data.chartData.length > 0 && data.chartData[0].data) {
+ chartDataValues = data.chartData[0].data;
+ }
+ // Default case
+ else {
+ chartDataValues = data.chartData;
+ }
+ } else {
+ chartDataValues = [data.chartData];
+ }
+
+ this.doughnutChartData = [
+ {
+ data: chartDataValues,
+ backgroundColor: this.chartColors.slice(0, chartDataValues.length),
+ hoverBackgroundColor: this.chartColors.slice(0, chartDataValues.length)
+ }
+ ];
+ console.log('Updated doughnut chart with drilldown data:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
+ // Set loading state to false
+ this.isLoading = false;
+ } else if (data && data.labels && data.datasets) {
+ // Backend has already filtered the data, just display it
+ this.noDataAvailable = data.labels.length === 0;
+ this.doughnutChartLabels = data.labels;
+ this.doughnutChartData = data.datasets;
+ console.log('Updated doughnut chart with drilldown legacy data format:', { labels: this.doughnutChartLabels, data: this.doughnutChartData });
+ // Set loading state to false
+ this.isLoading = false;
+ } else {
+ console.warn('Drilldown received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ // Keep current data instead of empty arrays
+ // Set loading state to false
+ this.isLoading = false;
+ }
+ },
+ (error) => {
+ console.error('Error fetching drilldown data:', error);
+ this.noDataAvailable = true;
+ // Keep current data in case of error
+ // Set loading state to false
+ this.isLoading = false;
+ }
+ );
+
+ // Set loading state
+ this.isLoading = true;
+ }
+
+ // Reset to original data (go back to base level)
+ resetToOriginalData(): void {
+ console.log('Resetting to original data');
+ console.log('Current stack before reset:', this.drilldownStack);
+ console.log('Current level before reset:', this.currentDrilldownLevel);
+
+ this.currentDrilldownLevel = 0;
+ this.drilldownStack = [];
+
+ if (this.originalDoughnutChartLabels.length > 0) {
+ this.doughnutChartLabels = [...this.originalDoughnutChartLabels];
+ console.log('Restored original labels');
+ }
+ if (this.originalDoughnutChartData.length > 0) {
+ this.doughnutChartData = JSON.parse(JSON.stringify(this.originalDoughnutChartData));
+ console.log('Restored original data');
+ }
+
+ console.log('After reset - labels:', this.doughnutChartLabels);
+ console.log('After reset - data:', this.doughnutChartData);
+
+ // 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();
+ }
+ }
+
+ // Get legend color for a specific index
+ getLegendColor(index: number): string {
+ return this.chartColors[index % this.chartColors.length];
+ }
+
+ // events
+ public chartClicked(e: any): void {
+ console.log('Doughnut 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 label of the clicked element
+ const clickedLabel = this.doughnutChartLabels[clickedIndex];
+
+ console.log('Clicked on doughnut slice:', { 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.originalDoughnutChartLabels = [...this.doughnutChartLabels];
+ this.originalDoughnutChartData = [...this.doughnutChartData];
+ 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,
+ clickedIndex: clickedIndex,
+ clickedLabel: clickedLabel,
+ clickedValue: clickedLabel // Using label 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('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 e00b379..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,10 +1,319 @@
-
-
-
-
-Update
+
+
+
+
+
0">
+
Base Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
0 && currentDrilldownLevel > 0">
+
Drilldown Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+
Layer Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+ Clear All Filters
+
+
+
+
+
+
+
+
0">
+
+
+
+
+
+
+
+
+ Drilldown Level: {{currentDrilldownLevel}}
+ 0">
+ (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
+
+
+
+
+
+
+
+
+
+
+
0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
+ Drilldown Level: {{currentDrilldownLevel}}
+
+ Back to Level {{currentDrilldownLevel - 1}}
+
+
+ Back to Main View
+
+
+
+
{{ charttitle }}
+
+
+
+
+
Loading chart data...
+
+
+
+
+
No chart data available
+
+
+
+
0 && dynamicChartData.length > 0"
+ [datasets]="dynamicChartData"
+ [options]="barChartOptions"
+ [type]="barChartType"
+ [labels]="dynamicChartLabels"
+ (chartHover)="chartHovered($event)"
+ (chartClick)="chartClicked($event)">
+
+
+
Update
+
\ 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.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 2173924..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
@@ -1,18 +1,93 @@
-import { Component, OnInit, ViewChild } from '@angular/core';
-import { ChartConfiguration, ChartData, } from 'chart.js';
+import { Component, OnInit, ViewChild, Input, OnChanges, SimpleChanges } from '@angular/core';
+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',
templateUrl: './dynamic-chart.component.html',
styleUrls: ['./dynamic-chart.component.scss']
})
-export class DynamicChartComponent implements OnInit {
+export class DynamicChartComponent implements OnInit, OnChanges {
+ @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; // Add connection input
+ // Drilldown configuration inputs
+ @Input() drilldownEnabled: boolean = false;
+ @Input() drilldownApiUrl: string;
+ @Input() drilldownXAxis: string;
+ @Input() drilldownYAxis: string;
+ @Input() drilldownParameter: string; // Add drilldown parameter input
+ @Input() baseFilters: any[] = []; // Add base filters input
+ @Input() drilldownFilters: any[] = []; // Add drilldown filters input
+ // Multi-layer drilldown configuration inputs
+ @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
- constructor() { }
+ @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined;
+
+ 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();
+ }
+
+ 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;
+ 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 && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged || drilldownFiltersChanged ||
+ drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
+ drilldownLayersChanged)) {
+ console.log('Chart configuration changed, fetching new data');
+ this.fetchChartData();
+ }
}
- @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined;
public barChartOptions: ChartConfiguration['options'] = {
elements: {
@@ -34,37 +109,786 @@ export class DynamicChartComponent implements OnInit {
public dynamicChartLabels: string[] = [ '2006', '2007', '2008', '2009', '2010', '2011', '2012' ];
public barChartType: string = 'bar';
- // public barChartData: ChartData<'bar'> = {
- // labels: this.barChartLabels,
- // datasets: [
- // { data: [ 65, 59, 80, 81, 56, 55, 40 ], label: 'Series A' },
- // { data: [ 28, 48, 40, 19, 86, 27, 90 ], label: 'Series B' }
- // ]
- // };
-
-
public dynamicChartData: any = [
{ data: [65, 59, 90, 81, 56, 55, 40], label: "Series A" },
{ data: [28, 48, 40, 19, 96, 27, 100], label: "Series B" }
];
+
+ // Multi-layer drilldown state tracking
+ drilldownStack: any[] = []; // Stack to track drilldown navigation history
+ currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
+ originalDynamicChartLabels: string[] = [];
+ originalDynamicChartData: any = [];
+
+ // No data state
+ noDataAvailable: boolean = false;
+
+ // 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;
+
+ // If we're in drilldown mode, fetch the appropriate drilldown data
+ if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
+ this.fetchDrilldownData();
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ return;
+ }
+
+ // If we have the necessary data, fetch chart data from the service
+ if (this.table && this.xAxis && this.yAxis) {
+ console.log('Fetching dynamic chart data for:', { 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);
+
+ // 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}` : ''}`;
+ console.log('Dynamic 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
+ this.dashboardService.getChartData(this.table, 'dynamic', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
+ (data: any) => {
+ console.log('Received dynamic chart data:', data);
+ if (data === null) {
+ console.warn('Dynamic chart API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ this.dynamicChartLabels = [];
+ this.dynamicChartData = [];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ return;
+ }
+
+ // Handle the actual data structure returned by the API
+ if (data && data.chartLabels && data.chartData) {
+ // Map the API response to the format expected by the chart
+ this.noDataAvailable = data.chartLabels.length === 0;
+ this.dynamicChartLabels = data.chartLabels;
+ this.dynamicChartData = data.chartData;
+ // Trigger change detection
+ this.dynamicChartData = [...this.dynamicChartData];
+ console.log('Updated dynamic chart with data:', { labels: this.dynamicChartLabels, data: this.dynamicChartData });
+ } else if (data && data.labels && data.datasets) {
+ // Handle the original expected format as fallback
+ this.noDataAvailable = data.labels.length === 0;
+ this.dynamicChartLabels = data.labels;
+ this.dynamicChartData = data.datasets;
+ // Trigger change detection
+ this.dynamicChartData = [...this.dynamicChartData];
+ console.log('Updated dynamic chart with legacy data format:', { labels: this.dynamicChartLabels, data: this.dynamicChartData });
+ } else {
+ console.warn('Dynamic chart received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ this.dynamicChartLabels = [];
+ this.dynamicChartData = [];
+ }
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ },
+ (error) => {
+ console.error('Error fetching dynamic chart data:', error);
+ this.noDataAvailable = true;
+ this.dynamicChartLabels = [];
+ this.dynamicChartData = [];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ // Keep default data in case of error
+ }
+ );
+ } else {
+ console.log('Missing required data for dynamic chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
+ this.noDataAvailable = true;
+ this.dynamicChartLabels = [];
+ this.dynamicChartData = [];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ }
+ }
+
+ // Fetch drilldown data based on current drilldown level
+ fetchDrilldownData(): void {
+ console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
+ console.log('Drilldown stack:', this.drilldownStack);
+
+ // Get the current drilldown configuration based on the current level
+ let drilldownConfig;
+ if (this.currentDrilldownLevel === 1) {
+ // Base drilldown level
+ drilldownConfig = {
+ apiUrl: this.drilldownApiUrl,
+ xAxis: this.drilldownXAxis,
+ yAxis: this.drilldownYAxis,
+ parameter: this.drilldownParameter
+ };
+ } else {
+ // Multi-layer drilldown level
+ const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) {
+ drilldownConfig = this.drilldownLayers[layerIndex];
+ } else {
+ console.warn('Invalid drilldown layer index:', layerIndex);
+ this.noDataAvailable = true;
+ this.dynamicChartLabels = [];
+ this.dynamicChartData = [];
+ 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.dynamicChartLabels = [];
+ this.dynamicChartData = [];
+ 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)
+ let filterParams = '';
+ if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
+ const filterObj = {};
+ drilldownConfig.filters.forEach((filter: any) => {
+ if (filter.field && filter.value) {
+ filterObj[filter.field] = filter.value;
+ }
+ });
+ if (Object.keys(filterObj).length > 0) {
+ filterParams = JSON.stringify(filterObj);
+ }
+ }
+
+ // Log the URL that will be called
+ const url = `chart/getdashjson/dynamic?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
+ console.log('Drilldown data URL:', url);
+
+ // 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, 'dynamic', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, filterParams).subscribe(
+ (data: any) => {
+ console.log('Received drilldown data:', data);
+ if (data === null) {
+ console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ this.dynamicChartLabels = [];
+ this.dynamicChartData = [];
+ return;
+ }
+
+ // Handle the actual data structure returned by the API
+ if (data && data.chartLabels && data.chartData) {
+ // Map the API response to the format expected by the chart
+ this.noDataAvailable = data.chartLabels.length === 0;
+ this.dynamicChartLabels = data.chartLabels;
+ this.dynamicChartData = data.chartData;
+ // Trigger change detection
+ this.dynamicChartData = [...this.dynamicChartData];
+ console.log('Updated dynamic chart with drilldown data:', { labels: this.dynamicChartLabels, data: this.dynamicChartData });
+ } else if (data && data.labels && data.datasets) {
+ // Handle the original expected format as fallback
+ this.noDataAvailable = data.labels.length === 0;
+ this.dynamicChartLabels = data.labels;
+ this.dynamicChartData = data.datasets;
+ // Trigger change detection
+ this.dynamicChartData = [...this.dynamicChartData];
+ console.log('Updated dynamic chart with drilldown legacy data format:', { labels: this.dynamicChartLabels, data: this.dynamicChartData });
+ } else {
+ console.warn('Drilldown received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ this.dynamicChartLabels = [];
+ this.dynamicChartData = [];
+ }
+ },
+ (error) => {
+ console.error('Error fetching drilldown data:', error);
+ this.noDataAvailable = true;
+ this.dynamicChartLabels = [];
+ this.dynamicChartData = [];
+ // Keep current data in case of error
+ }
+ );
+ }
+
+ // 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.originalDynamicChartLabels.length > 0) {
+ this.dynamicChartLabels = [...this.originalDynamicChartLabels];
+ console.log('Restored original labels');
+ }
+ if (this.originalDynamicChartData.length > 0) {
+ this.dynamicChartData = [...this.originalDynamicChartData];
+ console.log('Restored original data');
+ }
+
+ console.log('After reset - labels:', this.dynamicChartLabels);
+ console.log('After reset - data:', this.dynamicChartData);
+
+ // 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();
+ }
+ }
// events
public chartClicked(e: any): void {
- console.log(e);
- }
-
- public chartHovered(e: any): void {
- console.log(e);
- }
- // public chartClicked({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
- // console.log(event, active);
- // }
-
- // public chartHovered({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
- // console.log(event, active);
- // }
-
- public randomize(): void {
- this.barChartType = this.barChartType === 'bar' ? 'line' : 'bar';
+ console.log('Dynamic 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 label of the clicked element
+ const clickedLabel = this.dynamicChartLabels[clickedIndex];
+
+ console.log('Clicked on dynamic chart 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.originalDynamicChartLabels = [...this.dynamicChartLabels];
+ this.originalDynamicChartData = [...this.dynamicChartData];
+ 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,
+ clickedIndex: clickedIndex,
+ clickedLabel: clickedLabel,
+ clickedValue: clickedLabel // Using label 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(e);
+ }
+
+ public randomize(): void {
+ let _dynamicChartData: Array = new Array(this.dynamicChartData.length);
+ for (let i = 0; i < this.dynamicChartData.length; i++) {
+ _dynamicChartData[i] = {data: new Array(this.dynamicChartData[i].data.length), label: this.dynamicChartData[i].label};
+ for (let j = 0; j < this.dynamicChartData[i].data.length; j++) {
+ _dynamicChartData[i].data[j] = Math.floor((Math.random() * 100) + 1);
+ }
+ }
+ 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 56547d5..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 +1,317 @@
-financial-chart works!
+
+
+
+
+
0">
+
Base Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
0 && currentDrilldownLevel > 0">
+
Drilldown Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+
Layer Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+ Clear All Filters
+
+
+
+
+
+
+
+
0">
+
+
+
+
+
+
+
+
+ Drilldown Level: {{currentDrilldownLevel}}
+ 0">
+ (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
+
+
+
+
+
+
+
+
+
+
+
0" style="background-color: #e0e0e0; padding: 5px; margin-bottom: 10px; border-radius: 4px; text-align: center;">
+ Drilldown Level: {{currentDrilldownLevel}}
+
+ Back to Level {{currentDrilldownLevel - 1}}
+
+
+ Back to Main View
+
+
+
+
{{ charttitle }}
+
+
+
+
+
Loading chart data...
+
+
+
+
+
No chart data available
+
+
+
+
0 && financialChartData.length > 0"
+ [datasets]="financialChartData"
+ [labels]="financialChartLabels"
+ [type]="financialChartType"
+ (chartHover)="chartHovered($event)"
+ (chartClick)="chartClicked($event)">
+
+
+
\ 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.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/financial-chart/financial-chart.component.scss
index e69de29..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
@@ -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/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 664e7cd..2528fba 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,15 +1,1088 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
+import { FilterService } from '../../common-filter/filter.service';
+import { Subscription } from 'rxjs';
+import { AlertsService } from 'src/app/services/fnd/alerts.service';
@Component({
selector: 'app-financial-chart',
templateUrl: './financial-chart.component.html',
styleUrls: ['./financial-chart.component.scss']
})
-export class FinancialChartComponent implements OnInit {
+export class FinancialChartComponent implements OnInit, OnChanges {
+ @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; // Add connection input
+ // Drilldown configuration inputs
+ @Input() drilldownEnabled: boolean = false;
+ @Input() drilldownApiUrl: string;
+ @Input() drilldownXAxis: string;
+ @Input() drilldownYAxis: string;
+ @Input() drilldownParameter: string; // Add drilldown parameter input
+ @Input() baseFilters: any[] = []; // Add base filters input
+ @Input() drilldownFilters: any[] = []; // Add drilldown filters input
+ // Multi-layer drilldown configuration inputs
+ @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
- constructor() { }
+ constructor(
+ private dashboardService: Dashboard3Service,
+ private filterService: FilterService,
+ private alertService: AlertsService
+ ) { }
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();
+ }
+
+ 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();
+ // Load filter options for dropdown/multiselect filters
+ this.loadFilterOptions();
+ 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;
+ 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 && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged || drilldownFiltersChanged ||
+ drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
+ drilldownLayersChanged)) {
+ console.log('Chart configuration changed, fetching new data');
+ this.fetchChartData();
+ }
+ }
+
+ // Default financial chart data
+ public financialChartLabels: string[] = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
+ public financialChartData: any[] = [
+ { data: [65, 59, 80, 81, 56, 55, 40], label: 'Revenue' },
+ { data: [28, 48, 40, 19, 86, 27, 90], label: 'Expenses' }
+ ];
+ public financialChartType: string = 'line';
+
+ // Multi-layer drilldown state tracking
+ drilldownStack: any[] = []; // Stack to track drilldown navigation history
+ currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
+ originalFinancialChartLabels: string[] = [];
+ originalFinancialChartData: any[] = [];
+
+ // No data state
+ noDataAvailable: boolean = false;
+
+ // 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;
+
+ // If we're in drilldown mode, fetch the appropriate drilldown data
+ if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
+ this.fetchDrilldownData();
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ return;
+ }
+
+ // If we have the necessary data, fetch chart data from the service
+ if (this.table && this.xAxis && this.yAxis) {
+ console.log('Fetching financial chart data for:', { 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);
+
+ // 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}` : ''}`;
+ console.log('Financial 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
+ this.dashboardService.getChartData(this.table, 'financial', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
+ (data: any) => {
+ console.log('Received financial chart data:', data);
+ if (data === null) {
+ console.warn('Financial chart API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ this.financialChartLabels = [];
+ this.financialChartData = [];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ return;
+ }
+
+ // Handle the actual data structure returned by the API
+ if (data && data.chartLabels && data.chartData) {
+ // Map the API response to the format expected by the chart
+ this.noDataAvailable = data.chartLabels.length === 0;
+ this.financialChartLabels = data.chartLabels;
+ this.financialChartData = data.chartData.map(dataset => ({
+ ...dataset,
+ data: dataset.data ? dataset.data.map(value => {
+ // Convert to number if it's not already
+ return isNaN(Number(value)) ? 0 : Number(value);
+ }) : []
+ }));
+ // Trigger change detection
+ this.financialChartData = [...this.financialChartData];
+ console.log('Updated financial chart with data:', { labels: this.financialChartLabels, data: this.financialChartData });
+ } else if (data && data.labels && data.datasets) {
+ // Handle the original expected format as fallback
+ this.noDataAvailable = data.labels.length === 0;
+ this.financialChartLabels = data.labels;
+ this.financialChartData = data.datasets.map(dataset => ({
+ ...dataset,
+ data: dataset.data ? dataset.data.map(value => {
+ // Convert to number if it's not already
+ return isNaN(Number(value)) ? 0 : Number(value);
+ }) : []
+ }));
+ // Trigger change detection
+ this.financialChartData = [...this.financialChartData];
+ console.log('Updated financial chart with legacy data format:', { labels: this.financialChartLabels, data: this.financialChartData });
+ } else {
+ console.warn('Financial chart received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ this.financialChartLabels = [];
+ this.financialChartData = [];
+ }
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ },
+ (error) => {
+ console.error('Error fetching financial chart data:', error);
+ this.noDataAvailable = true;
+ this.financialChartLabels = [];
+ this.financialChartData = [];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ // Keep default data in case of error
+ }
+ );
+ } else {
+ console.log('Missing required data for financial chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
+ this.noDataAvailable = true;
+ this.financialChartLabels = [];
+ this.financialChartData = [];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ }
+ }
+
+ // Fetch drilldown data based on current drilldown level
+ fetchDrilldownData(): void {
+ console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
+ console.log('Drilldown stack:', this.drilldownStack);
+
+ // Get the current drilldown configuration based on the current level
+ let drilldownConfig;
+ if (this.currentDrilldownLevel === 1) {
+ // Base drilldown level
+ drilldownConfig = {
+ apiUrl: this.drilldownApiUrl,
+ xAxis: this.drilldownXAxis,
+ yAxis: this.drilldownYAxis,
+ parameter: this.drilldownParameter
+ };
+ } else {
+ // Multi-layer drilldown level
+ const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) {
+ drilldownConfig = this.drilldownLayers[layerIndex];
+ } else {
+ console.warn('Invalid drilldown layer index:', layerIndex);
+ this.noDataAvailable = true;
+ this.financialChartLabels = [];
+ this.financialChartData = [];
+ 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.financialChartLabels = [];
+ this.financialChartData = [];
+ 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)
+ let filterParams = '';
+ if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
+ const filterObj = {};
+ drilldownConfig.filters.forEach((filter: any) => {
+ if (filter.field && filter.value) {
+ filterObj[filter.field] = filter.value;
+ }
+ });
+ if (Object.keys(filterObj).length > 0) {
+ filterParams = JSON.stringify(filterObj);
+ }
+ }
+
+ // Log the URL that will be called
+ const url = `chart/getdashjson/financial?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
+ console.log('Drilldown data URL:', url);
+
+ // 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, 'financial', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, filterParams).subscribe(
+ (data: any) => {
+ console.log('Received drilldown data:', data);
+ if (data === null) {
+ console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ this.financialChartLabels = [];
+ this.financialChartData = [];
+ return;
+ }
+
+ // Handle the actual data structure returned by the API
+ if (data && data.chartLabels && data.chartData) {
+ // Map the API response to the format expected by the chart
+ this.noDataAvailable = data.chartLabels.length === 0;
+ this.financialChartLabels = data.chartLabels;
+ this.financialChartData = data.chartData.map(dataset => ({
+ ...dataset,
+ data: dataset.data ? dataset.data.map(value => {
+ // Convert to number if it's not already
+ return isNaN(Number(value)) ? 0 : Number(value);
+ }) : []
+ }));
+ // Trigger change detection
+ this.financialChartData = [...this.financialChartData];
+ console.log('Updated financial chart with drilldown data:', { labels: this.financialChartLabels, data: this.financialChartData });
+ } else if (data && data.labels && data.datasets) {
+ // Handle the original expected format as fallback
+ this.noDataAvailable = data.labels.length === 0;
+ this.financialChartLabels = data.labels;
+ this.financialChartData = data.datasets.map(dataset => ({
+ ...dataset,
+ data: dataset.data ? dataset.data.map(value => {
+ // Convert to number if it's not already
+ return isNaN(Number(value)) ? 0 : Number(value);
+ }) : []
+ }));
+ // Trigger change detection
+ this.financialChartData = [...this.financialChartData];
+ console.log('Updated financial chart with drilldown legacy data format:', { labels: this.financialChartLabels, data: this.financialChartData });
+ } else {
+ console.warn('Drilldown received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ this.financialChartLabels = [];
+ this.financialChartData = [];
+ }
+ },
+ (error) => {
+ console.error('Error fetching drilldown data:', error);
+ this.noDataAvailable = true;
+ this.financialChartLabels = [];
+ this.financialChartData = [];
+ // Keep current data in case of error
+ }
+ );
+ }
+
+ // 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.originalFinancialChartLabels.length > 0) {
+ this.financialChartLabels = [...this.originalFinancialChartLabels];
+ console.log('Restored original labels');
+ }
+ if (this.originalFinancialChartData.length > 0) {
+ this.financialChartData = [...this.originalFinancialChartData];
+ console.log('Restored original data');
+ }
+
+ console.log('After reset - labels:', this.financialChartLabels);
+ console.log('After reset - data:', this.financialChartData);
+
+ // 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();
+ }
+ }
+
+ // events
+ public chartClicked(e: any): void {
+ console.log('Financial 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 label of the clicked element
+ const clickedLabel = this.financialChartLabels[clickedIndex];
+
+ console.log('Clicked on financial chart 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.originalFinancialChartLabels = [...this.financialChartLabels];
+ this.originalFinancialChartData = [...this.financialChartData];
+ 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,
+ clickedIndex: clickedIndex,
+ clickedLabel: clickedLabel,
+ clickedValue: clickedLabel // Using label 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(e);
+ }
+
+ // Initialize filter values with proper default values based on type
+ private initializeFilterValues(): void {
+ console.log('Initializing filter values');
+
+ // Initialize base filters
+ if (this.baseFilters) {
+ this.baseFilters.forEach(filter => {
+ // Ensure filter has required properties
+ if (!filter.type) filter.type = 'text';
+ if (!filter.options) filter.options = '';
+ if (!filter.availableValues) filter.availableValues = '';
+
+ if (filter.value === undefined || filter.value === null) {
+ switch (filter.type) {
+ case 'multiselect':
+ filter.value = [];
+ break;
+ case 'date-range':
+ filter.value = { start: null, end: null };
+ break;
+ case 'toggle':
+ filter.value = false;
+ break;
+ default:
+ filter.value = '';
+ }
+ }
+ });
+ }
+
+ // Initialize drilldown filters
+ if (this.drilldownFilters) {
+ this.drilldownFilters.forEach(filter => {
+ // Ensure filter has required properties
+ if (!filter.type) filter.type = 'text';
+ if (!filter.options) filter.options = '';
+ if (!filter.availableValues) filter.availableValues = '';
+
+ if (filter.value === undefined || filter.value === null) {
+ switch (filter.type) {
+ case 'multiselect':
+ filter.value = [];
+ break;
+ case 'date-range':
+ filter.value = { start: null, end: null };
+ break;
+ case 'toggle':
+ filter.value = false;
+ break;
+ default:
+ filter.value = '';
+ }
+ }
+ });
+ }
+
+ // Initialize layer filters
+ if (this.drilldownLayers) {
+ this.drilldownLayers.forEach(layer => {
+ if (layer.filters) {
+ layer.filters.forEach((filter: any) => {
+ // Ensure filter has required properties
+ if (!filter.type) filter.type = 'text';
+ if (!filter.options) filter.options = '';
+ if (!filter.availableValues) filter.availableValues = '';
+
+ if (filter.value === undefined || filter.value === null) {
+ switch (filter.type) {
+ case 'multiselect':
+ filter.value = [];
+ break;
+ case 'date-range':
+ filter.value = { start: null, end: null };
+ break;
+ case 'toggle':
+ filter.value = false;
+ break;
+ default:
+ filter.value = '';
+ }
+ }
+ });
+ }
+ });
+ }
+
+ console.log('Filter values initialized:', {
+ baseFilters: this.baseFilters,
+ drilldownFilters: this.drilldownFilters,
+ drilldownLayers: this.drilldownLayers
+ });
+ }
+
+ // Check if there are active filters
+ hasActiveFilters(): boolean {
+ return (this.baseFilters && this.baseFilters.length > 0) ||
+ (this.drilldownFilters && this.drilldownFilters.length > 0) ||
+ this.hasActiveLayerFilters();
+ }
+
+ // Check if there are active layer filters for current drilldown level
+ hasActiveLayerFilters(): boolean {
+ if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
+ const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ return layerIndex < this.drilldownLayers.length &&
+ this.drilldownLayers[layerIndex].filters &&
+ this.drilldownLayers[layerIndex].filters.length > 0;
+ }
+ return false;
+ }
+
+ // Get active layer filters for current drilldown level
+ getActiveLayerFilters(): any[] {
+ if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
+ const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ if (layerIndex < this.drilldownLayers.length &&
+ this.drilldownLayers[layerIndex].filters) {
+ return this.drilldownLayers[layerIndex].filters;
+ }
+ }
+ return [];
+ }
+
+ // Get filter options for dropdown/multiselect filters
+ getFilterOptions(filter: any): string[] {
+ if (filter.options) {
+ if (Array.isArray(filter.options)) {
+ return filter.options;
+ } else if (typeof filter.options === 'string') {
+ return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
+ }
+ }
+ return [];
+ }
+
+ // Check if an option is selected for multiselect filters
+ isOptionSelected(filter: any, option: string): boolean {
+ if (!filter.value) {
+ return false;
+ }
+
+ if (Array.isArray(filter.value)) {
+ return filter.value.includes(option);
+ }
+
+ return filter.value === option;
+ }
+
+ // Handle base filter changes
+ onBaseFilterChange(filter: any): void {
+ console.log('Base filter changed:', filter);
+ // Refresh data when filter changes
+ this.fetchChartData();
+ }
+
+ // Handle drilldown filter changes
+ onDrilldownFilterChange(filter: any): void {
+ console.log('Drilldown filter changed:', filter);
+ // Refresh data when filter changes
+ this.fetchChartData();
+ }
+
+ // Handle layer filter changes
+ onLayerFilterChange(filter: any): void {
+ console.log('Layer filter changed:', filter);
+ // Refresh data when filter changes
+ this.fetchChartData();
+ }
+
+ // Handle multiselect changes
+ onMultiSelectChange(filter: any, option: string, event: any): void {
+ const checked = event.target.checked;
+
+ // Initialize filter.value as array if it's not already
+ if (!Array.isArray(filter.value)) {
+ filter.value = [];
+ }
+
+ if (checked) {
+ // Add option to array if not already present
+ if (!filter.value.includes(option)) {
+ filter.value.push(option);
+ }
+ } else {
+ // Remove option from array
+ filter.value = filter.value.filter((item: string) => item !== option);
+ }
+
+ // Refresh data when filter changes
+ this.fetchChartData();
+ }
+
+ // Handle date range changes
+ onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
+ filter.value = dateRange;
+ // Refresh data when filter changes
+ this.fetchChartData();
+ }
+
+ // Handle toggle changes
+ onToggleChange(filter: any, checked: boolean): void {
+ filter.value = checked;
+ // Refresh data when filter changes
+ this.fetchChartData();
+ }
+
+ // Toggle multiselect dropdown visibility
+ toggleMultiselect(filter: any, context: string): void {
+ const filterId = `${context}-${filter.field}`;
+ if (this.isMultiselectOpen(filter, context)) {
+ this.openMultiselects.delete(filterId);
+ } else {
+ // Close all other multiselects first
+ this.openMultiselects.clear();
+ this.openMultiselects.set(filterId, context);
+
+ // Add document click handler to close dropdown when clicking outside
+ this.addDocumentClickHandler();
+ }
+ }
+
+ // Add document click handler to close dropdowns when clicking outside
+ private addDocumentClickHandler(): void {
+ if (!this.documentClickHandler) {
+ this.documentClickHandler = (event: MouseEvent) => {
+ const target = event.target as HTMLElement;
+ // Check if click is outside any multiselect dropdown
+ if (!target.closest('.multiselect-container')) {
+ this.openMultiselects.clear();
+ this.removeDocumentClickHandler();
+ }
+ };
+
+ // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
+ setTimeout(() => {
+ document.addEventListener('click', this.documentClickHandler!);
+ }, 0);
+ }
+ }
+
+ // Remove document click handler
+ private removeDocumentClickHandler(): void {
+ if (this.documentClickHandler) {
+ document.removeEventListener('click', this.documentClickHandler);
+ this.documentClickHandler = null;
+ }
+ }
+
+ // Check if multiselect dropdown is open
+ isMultiselectOpen(filter: any, context: string): boolean {
+ const filterId = `${context}-${filter.field}`;
+ return this.openMultiselects.has(filterId);
+ }
+
+ // Get count of selected options for a multiselect filter
+ getSelectedOptionsCount(filter: any): number {
+ if (!filter.value) {
+ return 0;
+ }
+
+ if (Array.isArray(filter.value)) {
+ return filter.value.length;
+ }
+
+ return 0;
+ }
+
+ // Clear all filters
+ clearAllFilters(): void {
+ // Clear base filters
+ if (this.baseFilters) {
+ this.baseFilters.forEach(filter => {
+ if (filter.type === 'multiselect') {
+ filter.value = [];
+ } else if (filter.type === 'date-range') {
+ filter.value = { start: null, end: null };
+ } else if (filter.type === 'toggle') {
+ filter.value = false;
+ } else {
+ filter.value = '';
+ }
+ });
+ }
+
+ // Clear drilldown filters
+ if (this.drilldownFilters) {
+ this.drilldownFilters.forEach(filter => {
+ if (filter.type === 'multiselect') {
+ filter.value = [];
+ } else if (filter.type === 'date-range') {
+ filter.value = { start: null, end: null };
+ } else if (filter.type === 'toggle') {
+ filter.value = false;
+ } else {
+ filter.value = '';
+ }
+ });
+ }
+
+ // Clear layer filters
+ if (this.drilldownLayers) {
+ this.drilldownLayers.forEach(layer => {
+ if (layer.filters) {
+ layer.filters.forEach((filter: any) => {
+ if (filter.type === 'multiselect') {
+ filter.value = [];
+ } else if (filter.type === 'date-range') {
+ filter.value = { start: null, end: null };
+ } else if (filter.type === 'toggle') {
+ filter.value = false;
+ } else {
+ filter.value = '';
+ }
+ });
+ }
+ });
+ }
+
+ // Close all multiselect dropdowns
+ this.openMultiselects.clear();
+
+ // Refresh data
+ this.fetchChartData();
+ }
+
+ ngOnDestroy(): void {
+ // Unsubscribe from all subscriptions to prevent memory leaks
+ this.subscriptions.forEach(subscription => subscription.unsubscribe());
+
+ // Remove document click handler if it exists
+ this.removeDocumentClickHandler();
+ }
+
+ // Load filter options for dropdown and multiselect filters
+ private loadFilterOptions(): void {
+ console.log('Loading filter options');
+
+ // Load options for base filters
+ if (this.baseFilters && this.table) {
+ this.baseFilters.forEach((filter, index) => {
+ if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
+ this.loadFilterValuesForField(this.table, this.connection, filter.field, index, 'base');
+ }
+ });
+ }
+
+ // Load options for drilldown filters
+ if (this.drilldownFilters && this.drilldownApiUrl) {
+ this.drilldownFilters.forEach((filter, index) => {
+ if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
+ this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, filter.field, index, 'drilldown');
+ }
+ });
+ }
+
+ // Load options for layer filters
+ if (this.drilldownLayers) {
+ this.drilldownLayers.forEach((layer, layerIndex) => {
+ if (layer.filters && layer.apiUrl) {
+ layer.filters.forEach((filter, filterIndex) => {
+ if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.field) {
+ this.loadFilterValuesForField(layer.apiUrl, this.connection, filter.field, filterIndex, 'layer', layerIndex);
+ }
+ });
+ }
+ });
+ }
+ }
+
+ // Load filter values for a specific field
+ private loadFilterValuesForField(
+ apiUrl: string,
+ connectionId: number | undefined,
+ field: string,
+ filterIndex: number,
+ filterType: 'base' | 'drilldown' | 'layer',
+ layerIndex?: number
+ ): void {
+ if (apiUrl && field) {
+ this.alertService.getValuesFromUrl(apiUrl, connectionId, field).subscribe(
+ (values: string[]) => {
+ console.log(`Loaded filter values for ${filterType} filter ${field}:`, values);
+
+ // Update the filter with available values
+ if (filterType === 'base') {
+ const filter = this.baseFilters[filterIndex];
+ if (filter) {
+ filter.availableValues = values.join(', ');
+ // For dropdown/multiselect types, also update the options
+ if (filter.type === 'dropdown' || filter.type === 'multiselect') {
+ filter.options = filter.availableValues;
+ }
+ }
+ } else if (filterType === 'drilldown') {
+ const filter = this.drilldownFilters[filterIndex];
+ if (filter) {
+ filter.availableValues = values.join(', ');
+ // For dropdown/multiselect types, also update the options
+ if (filter.type === 'dropdown' || filter.type === 'multiselect') {
+ filter.options = filter.availableValues;
+ }
+ }
+ } else if (filterType === 'layer' && layerIndex !== undefined) {
+ const layer = this.drilldownLayers[layerIndex];
+ if (layer && layer.filters) {
+ const filter = layer.filters[filterIndex];
+ if (filter) {
+ filter.availableValues = values.join(', ');
+ // For dropdown/multiselect types, also update the options
+ if (filter.type === 'dropdown' || filter.type === 'multiselect') {
+ filter.options = filter.availableValues;
+ }
+ }
+ }
+ }
+ },
+ (error) => {
+ console.error('Error loading available values for field:', field, error);
+ }
+ );
+ }
+ }
+
+ // Handle base filter field change
+ onBaseFilterFieldChange(index: number, field: string): void {
+ const filter = this.baseFilters[index];
+ if (filter) {
+ filter.field = field;
+ // If field changes, reset value and options
+ filter.value = '';
+ filter.options = '';
+ filter.availableValues = '';
+
+ // If we have a field and table URL, load available values
+ if (field && this.table && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
+ this.loadFilterValuesForField(this.table, this.connection, field, index, 'base');
+ }
+ }
+ }
+
+ // Handle base filter type change
+ onBaseFilterTypeChange(index: number, type: string): void {
+ const filter = this.baseFilters[index];
+ if (filter) {
+ filter.type = type;
+ // If type changes to dropdown/multiselect and we have a field, load available values
+ if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.table) {
+ this.loadFilterValuesForField(this.table, this.connection, filter.field, index, 'base');
+ }
+ }
+ }
+
+ // Handle drilldown filter field change
+ onDrilldownFilterFieldChange(index: number, field: string): void {
+ const filter = this.drilldownFilters[index];
+ if (filter) {
+ filter.field = field;
+ // If field changes, reset value and options
+ filter.value = '';
+ filter.options = '';
+ filter.availableValues = '';
+
+ // If we have a field and drilldown API URL, load available values
+ if (field && this.drilldownApiUrl && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
+ this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, field, index, 'drilldown');
+ }
+ }
+ }
+
+ // Handle drilldown filter type change
+ onDrilldownFilterTypeChange(index: number, type: string): void {
+ const filter = this.drilldownFilters[index];
+ if (filter) {
+ filter.type = type;
+ // If type changes to dropdown/multiselect and we have a field, load available values
+ if ((type === 'dropdown' || type === 'multiselect') && filter.field && this.drilldownApiUrl) {
+ this.loadFilterValuesForField(this.drilldownApiUrl, this.connection, filter.field, index, 'drilldown');
+ }
+ }
+ }
+
+ // Handle layer filter field change
+ onLayerFilterFieldChange(layerIndex: number, filterIndex: number, field: string): void {
+ const layer = this.drilldownLayers[layerIndex];
+ if (layer && layer.filters) {
+ const filter = layer.filters[filterIndex];
+ if (filter) {
+ filter.field = field;
+ // If field changes, reset value and options
+ filter.value = '';
+ filter.options = '';
+ filter.availableValues = '';
+
+ // If we have a field and layer API URL, load available values
+ if (field && layer.apiUrl && (filter.type === 'dropdown' || filter.type === 'multiselect')) {
+ this.loadFilterValuesForField(layer.apiUrl, this.connection, field, filterIndex, 'layer', layerIndex);
+ }
+ }
+ }
+ }
+
+ // Handle layer filter type change
+ onLayerFilterTypeChange(layerIndex: number, filterIndex: number, type: string): void {
+ const layer = this.drilldownLayers[layerIndex];
+ if (layer && layer.filters) {
+ const filter = layer.filters[filterIndex];
+ if (filter) {
+ filter.type = type;
+ // If type changes to dropdown/multiselect and we have a field, load available values
+ if ((type === 'dropdown' || type === 'multiselect') && filter.field && layer.apiUrl) {
+ this.loadFilterValuesForField(layer.apiUrl, this.connection, filter.field, filterIndex, 'layer', layerIndex);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/grid-view/grid-view.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/grid-view/grid-view.component.html
index 94c1edc..67590d9 100644
--- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/grid-view/grid-view.component.html
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/grid-view/grid-view.component.html
@@ -1,62 +1,306 @@
-
-
-
-
User Group Maintenance
+
+
+
+
+
0">
+
Base Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
0 && currentDrilldownLevel > 0">
+
Drilldown Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+
Layer Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+ Clear All Filters
+
+
+
+
+
0" style="text-align: right;">
+
+
+ Back to {{drilldownStack.length > 1 ? 'Previous Level' : 'Main Data'}}
+
+
+
+
+
+
0">
+
+
+
+
+
+
+
+
+ Drilldown Level: {{currentDrilldownLevel}}
+ 0">
+ (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedKey}} = {{drilldownStack[drilldownStack.length - 1].clickedValue}})
+
+
+
+
+
+
+
+
- Loading ...
- {{error}}
-
-
- User Group No
-
-
- Group Name
-
-
- Description
-
-
- Group Level
-
-
- Status
-
-
-
- Updated Date
-
-
-
-
- {{user.usrGrp}}
- {{user.groupName}}
- {{user.groupDesc}}
- {{user.groupLevel}}
- {{user.status}}
-
- {{user.updateDateFormated}}
-
-
-
-
+
+
+ Loading ...
+
+ {{error}}
+
+
+
+
+
+ {{header.displayName}}
+
+
+
+
+
+
+ {{item[header.key]}}
+
-
+
Record per page
@@ -65,5 +309,5 @@
-
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/grid-view/grid-view.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/grid-view/grid-view.component.scss
index 99c5f44..a2d9ba4 100644
--- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/grid-view/grid-view.component.scss
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/grid-view/grid-view.component.scss
@@ -1,12 +1,180 @@
-@import '../../../../../../../styles1.scss';
-input.ng-invalid.ng-touched {
- border-color: red;
+.filter-section {
+ margin-bottom: 20px;
+ padding: 15px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ background-color: #f9f9f9;
}
-.error_mess {
- color: red;
+.filter-group {
+ margin-bottom: 15px;
+
+ h4 {
+ margin-top: 0;
+ margin-bottom: 10px;
+ color: #333;
+ font-weight: 600;
+ }
}
-clr-datagrid{
- height: 400px; /* Adjust the height as needed */
+
+.filter-controls {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 15px;
+}
+
+.filter-item {
+ flex: 1 1 300px;
+ min-width: 250px;
+ padding: 10px;
+ background: white;
+ border: 1px solid #e0e0e0;
+ border-radius: 4px;
+}
+
+.filter-label {
+ font-weight: 500;
+ margin-bottom: 8px;
+ color: #555;
+ font-size: 14px;
+}
+
+.filter-input {
+ width: 100%;
+
+ .filter-text-input,
+ .filter-select,
+ .filter-date {
+ width: 100%;
+ padding: 6px 12px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ font-size: 14px;
+ }
+
+ .filter-select {
+ height: 34px;
+ }
+}
+
+.multiselect-container {
+ position: relative;
+}
+
+.multiselect-display {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 6px 12px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ background: white;
+ cursor: pointer;
+ min-height: 34px;
+
+ .multiselect-label {
+ flex: 1;
+ font-size: 14px;
+ }
+
+ .multiselect-value {
+ color: #666;
+ font-size: 12px;
+ margin-right: 8px;
+ }
+
+ .dropdown-icon {
+ flex-shrink: 0;
+ transition: transform 0.2s ease;
+ }
+
+ &:hover {
+ border-color: #999;
+ }
+}
+
+.multiselect-dropdown {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ z-index: 1000;
+ background: white;
+ border: 1px solid #ccc;
+ border-top: none;
+ border-radius: 0 0 4px 4px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ max-height: 200px;
overflow-y: auto;
+
+ .checkbox-group {
+ padding: 8px;
+
+ .checkbox-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 0;
+
+ .checkbox-label {
+ margin: 0;
+ font-size: 14px;
+ cursor: pointer;
+ }
+ }
+ }
+}
+
+.date-range {
+ .date-input-group {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .date-separator {
+ margin: 0 5px;
+ color: #777;
+ }
+}
+
+.toggle {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .toggle-label {
+ margin: 0;
+ font-size: 14px;
+ cursor: pointer;
+ }
+}
+
+.filter-actions {
+ margin-top: 15px;
+ padding-top: 15px;
+ border-top: 1px solid #eee;
+
+ .btn {
+ font-size: 13px;
+ }
+}
+
+.dg-wrapper {
+ padding: 15px;
+}
+
+clr-datagrid {
+ margin-top: 10px;
+}
+
+// Responsive design
+@media (max-width: 768px) {
+ .filter-controls {
+ flex-direction: column;
+ }
+
+ .filter-item {
+ min-width: 100%;
+ }
}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/grid-view/grid-view.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/grid-view/grid-view.component.ts
index dfb2de1..bccaa81 100644
--- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/grid-view/grid-view.component.ts
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/grid-view/grid-view.component.ts
@@ -1,54 +1,980 @@
-import { Component, OnInit } from '@angular/core';
-import { FormBuilder, FormGroup, Validators } from '@angular/forms';
-import { ActivatedRoute, Router } from '@angular/router';
-import { ExcelService } from 'src/app/services/excel.service';
-import * as moment from 'moment';
+import { Component, OnInit, Input, OnChanges, OnDestroy, SimpleChanges, ViewChild } from '@angular/core';
import { UsergrpmaintainceService } from 'src/app/services/admin/usergrpmaintaince.service';
-import { ToastrService } from 'ngx-toastr';
-import { MenuGroupService } from 'src/app/services/admin/menu-group.service';
+import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
+// Add FilterService import
+import { FilterService } from '../../common-filter/filter.service';
+// Add Subscription import
+import { Subscription } from 'rxjs';
+
@Component({
selector: 'app-grid-view',
templateUrl: './grid-view.component.html',
styleUrls: ['./grid-view.component.scss']
})
-export class GridViewComponent implements OnInit {
+export class GridViewComponent implements OnInit, OnChanges, OnDestroy {
+ @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; // Add connection input
+ // Drilldown configuration inputs
+ @Input() drilldownEnabled: boolean = false;
+ @Input() drilldownApiUrl: string;
+ @Input() drilldownXAxis: string;
+ @Input() drilldownYAxis: string;
+ @Input() drilldownParameter: string; // Add drilldown parameter input
+ @Input() baseFilters: any[] = []; // Add base filters input
+ @Input() drilldownFilters: any[] = []; // Add drilldown filters input
+ // Multi-layer drilldown configuration inputs
+ @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
+
loading = false;
- public entryForm: FormGroup;
- givendata;
+ givendata: any[] = [];
orders;
- error;
- modalAdd= false;
- modaledit=false;
- modaldelete=false;
- rowSelected :any= {};
+ error: string;
+ modalAdd = false;
+ modaledit = false;
+ modaldelete = false;
+ rowSelected: any = {};
mcreate;
medit;
showdata;
- submitted=false;
+ submitted = false;
+ dynamicHeaders: any[] = [];
+
+ // Multi-layer drilldown state tracking
+ drilldownStack: any[] = []; // Stack to track drilldown navigation history
+ currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
+ originalGridData: any[] = [];
+
+ // No data state
+ noDataAvailable: boolean = false;
+
+ // Flag to prevent infinite loops
+ private isFetchingData: boolean = false;
+
+ // Add subscriptions to unsubscribe on destroy
+ private subscriptions: Subscription[] = [];
+
+ // Add a flag to track if filters have been initialized
+ private filtersInitialized: boolean = false;
+
+ // Add properties to track open multiselect dropdowns
+ private openMultiselects: Map
= new Map(); // Map of filterId -> context
+
+ // Add property to track document click handler
+ private documentClickHandler: ((event: MouseEvent) => void) | null = null;
constructor(
- private excel: ExcelService,
- private toastr:ToastrService,
- private _fb: FormBuilder,
- private router: Router,
- private route: ActivatedRoute,
- private menuGroupService: MenuGroupService,
- private mainservice:UsergrpmaintainceService,
+ private mainservice: UsergrpmaintainceService,
+ private dashboardService: Dashboard3Service,
+ // Add FilterService to constructor
+ private filterService: FilterService
) { }
ngOnInit(): void {
- this.mainservice.getAll().subscribe((data) => {
- console.log(data);
- this.givendata = data;
- if(this.givendata.length==0){
- this.error="No data Available";
- console.log(this.error)
- }
- },(error) => {
- console.log(error);
- if(error){
- this.error="Server Error";
- }
+ // Subscribe to filter changes
+ this.subscriptions.push(
+ this.filterService.filterState$.subscribe(filters => {
+ // When filters change, refresh the grid data
+ console.log('GridView: Filter state changed:', filters);
+ this.fetchGridData();
+ })
+ );
+
+ this.fetchGridData();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ console.log('GridViewComponent input changes:', changes);
+
+ // Check if any of the key properties have changed
+ 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;
+ // Drilldown configuration changes
+ const drilldownEnabledChanged = changes.drilldownEnabled && !changes.drilldownEnabled.firstChange;
+ const drilldownApiUrlChanged = changes.drilldownApiUrl && !changes.drilldownApiUrl.firstChange;
+ const drilldownXAxisChanged = changes.drilldownXAxis && !changes.drilldownXAxis.firstChange;
+ const drilldownYAxisChanged = changes.drilldownYAxis && !changes.drilldownYAxis.firstChange;
+ const drilldownLayersChanged = changes.drilldownLayers && !changes.drilldownLayers.firstChange;
+
+ // Initialize filter values if they haven't been initialized yet
+ if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
+ this.initializeFilterValues();
+ this.filtersInitialized = true;
+ }
+
+ // Respond to input changes
+ if (!this.isFetchingData && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged ||
+ drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
+ drilldownLayersChanged)) {
+ console.log('X or Y axis or table or connection or base filters or drilldown config changed, fetching new data');
+ // Only fetch data if xAxis, yAxis, table, connection, baseFilters or drilldown config has changed (and it's not the first change)
+ this.fetchGridData();
+ }
+ }
+
+ // Initialize filter values with proper default values based on type
+ private initializeFilterValues(): void {
+ console.log('Initializing filter values');
+
+ // Initialize base filters
+ if (this.baseFilters) {
+ this.baseFilters.forEach(filter => {
+ if (filter.value === undefined || filter.value === null) {
+ switch (filter.type) {
+ case 'multiselect':
+ filter.value = [];
+ break;
+ case 'date-range':
+ filter.value = { start: null, end: null };
+ break;
+ case 'toggle':
+ filter.value = false;
+ break;
+ default:
+ filter.value = '';
+ }
+ }
+ });
+ }
+
+ // Initialize drilldown filters
+ if (this.drilldownFilters) {
+ this.drilldownFilters.forEach(filter => {
+ if (filter.value === undefined || filter.value === null) {
+ switch (filter.type) {
+ case 'multiselect':
+ filter.value = [];
+ break;
+ case 'date-range':
+ filter.value = { start: null, end: null };
+ break;
+ case 'toggle':
+ filter.value = false;
+ break;
+ default:
+ filter.value = '';
+ }
+ }
+ });
+ }
+
+ // Initialize layer filters
+ if (this.drilldownLayers) {
+ this.drilldownLayers.forEach(layer => {
+ if (layer.filters) {
+ layer.filters.forEach((filter: any) => {
+ if (filter.value === undefined || filter.value === null) {
+ switch (filter.type) {
+ case 'multiselect':
+ filter.value = [];
+ break;
+ case 'date-range':
+ filter.value = { start: null, end: null };
+ break;
+ case 'toggle':
+ filter.value = false;
+ break;
+ default:
+ filter.value = '';
+ }
+ }
+ });
+ }
+ });
+ }
+
+ console.log('Filter values initialized:', {
+ baseFilters: this.baseFilters,
+ drilldownFilters: this.drilldownFilters,
+ drilldownLayers: this.drilldownLayers
});
}
-}
+
+ // Dynamic headers for the grid
+
+ fetchGridData(): void {
+ // Set flag to prevent recursive calls
+ this.isFetchingData = true;
+
+ // If we're in drilldown mode, fetch the appropriate drilldown data
+ if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
+ this.fetchDrilldownData();
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ return;
+ }
+
+ // If we have the necessary data, fetch grid data from the service
+ // if (this.table && this.xAxis) {
+ if (this.table) {
+
+ console.log('=== GRID VIEW DEBUG INFO ===');
+ console.log('Table:', this.table);
+ console.log('X-Axis:', this.xAxis);
+ console.log('Y-Axis:', this.yAxis);
+ console.log('Connection:', this.connection);
+
+ // Convert yAxis to string if it's an array
+ const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis;
+
+ // Get the parameter value from the drilldown stack for base level (should be empty)
+ let parameterValue = '';
+
+ // Log the URL that will be called
+ let url = `chart/getdashjson/grid?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
+ console.log('Grid data URL:', url);
+
+ // Get filter parameters from base filters
+ const filterObj = {};
+
+ // Add base filters
+ if (this.baseFilters && this.baseFilters.length > 0) {
+ this.baseFilters.forEach(filter => {
+ if (filter.field && filter.value) {
+ filterObj[filter.field] = filter.value;
+ }
+ });
+ }
+
+ // Add common filters directly as key-value pairs
+ const commonFilters = this.filterService.getFilterValues();
+ const filterDefinitions = this.filterService.getFilters();
+
+ // Add common filters using the field name as the key
+ Object.keys(commonFilters).forEach(filterId => {
+ const filterValue = commonFilters[filterId];
+
+ // Find the filter definition to get the field name
+ const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
+
+ if (filterDef && filterDef.field) {
+ const fieldName = filterDef.field;
+ if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
+ filterObj[fieldName] = filterValue;
+ }
+ }
+ });
+
+ // Convert to JSON string for API call
+ let filterParams = '';
+ if (Object.keys(filterObj).length > 0) {
+ filterParams = JSON.stringify(filterObj);
+ }
+
+ console.log('GridView: Final filter object to send to API:', filterObj);
+
+ // Fetch data from the dashboard service, similar to other chart components
+ this.dashboardService.getChartData(this.table, 'grid', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
+ (data: any) => {
+ console.log('=== GRID VIEW DATA RESPONSE ===');
+ console.log('Received grid data:', data);
+ if (data === null) {
+ console.warn('Grid API returned null data. Check if the API endpoint is working correctly.');
+ this.error = "No data Available";
+ this.noDataAvailable = true;
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ return;
+ }
+
+ // Handle the actual data structure returned by the API
+ if (data && data.chartData) {
+ this.givendata = data.chartData;
+ this.extractDynamicHeaders(data.chartData);
+ this.error = this.givendata.length === 0 ? "No data Available" : undefined;
+ this.noDataAvailable = this.givendata.length === 0;
+ console.log('Updated grid with data:', this.givendata);
+ } else if (data && data.data) {
+ // Handle the original expected format as fallback
+ this.givendata = data.data;
+ this.extractDynamicHeaders(data.data);
+ this.error = this.givendata.length === 0 ? "No data Available" : undefined;
+ this.noDataAvailable = this.givendata.length === 0;
+ console.log('Updated grid with legacy data format:', this.givendata);
+ } else if (Array.isArray(data)) {
+ // Handle case where data is directly an array
+ this.givendata = data;
+ this.extractDynamicHeaders(data);
+ this.error = this.givendata.length === 0 ? "No data Available" : undefined;
+ this.noDataAvailable = this.givendata.length === 0;
+ console.log('Updated grid with array data:', this.givendata);
+ } else {
+ console.warn('Grid received data does not have expected structure', data);
+ this.error = "No valid data received";
+ this.givendata = [];
+ this.noDataAvailable = true;
+ }
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ }, (error) => {
+ console.log('Error fetching grid data:', error);
+ this.error = "Server Error";
+ this.noDataAvailable = true;
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ });
+ } else if (this.table) {
+ console.log('Missing xAxis, falling back to default data fetching');
+ // Fallback to default data fetching when only table is provided
+ // Convert yAxis to string if it's an array
+ const yAxisString = Array.isArray(this.yAxis) ? this.yAxis.join(',') : this.yAxis;
+
+ // Fetch data from the dashboard service, similar to other chart components
+ this.dashboardService.getChartData(this.table, 'grid', this.xAxis, yAxisString, this.connection).subscribe(
+ (data: any) => {
+ // this.mainservice.getAll().subscribe((data: any) => {
+ console.log('recv data ', data);
+ this.givendata = Array.isArray(data) ? data : [];
+ this.extractDynamicHeaders(data);
+ this.error = this.givendata && this.givendata.length === 0 ? "No data Available" : undefined;
+ this.noDataAvailable = this.givendata && this.givendata.length === 0;
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ }, (error) => {
+ console.log(error);
+ this.error = "Server Error";
+ this.noDataAvailable = true;
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ });
+ } else {
+ console.log('Missing required data for grid:', { table: this.table });
+ this.error = "Table name is required";
+ this.noDataAvailable = true;
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ }
+ }
+
+ // Fetch drilldown data based on current drilldown level
+ fetchDrilldownData(): void {
+ console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
+ console.log('Drilldown stack:', this.drilldownStack);
+
+ // Get the current drilldown configuration based on the current level
+ let drilldownConfig;
+ if (this.currentDrilldownLevel === 1) {
+ // Base drilldown level
+ drilldownConfig = {
+ apiUrl: this.drilldownApiUrl,
+ xAxis: this.drilldownXAxis,
+ yAxis: this.drilldownYAxis,
+ parameter: this.drilldownParameter
+ };
+ } else {
+ // Multi-layer drilldown level
+ const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) {
+ drilldownConfig = this.drilldownLayers[layerIndex];
+ } else {
+ console.warn('Invalid drilldown layer index:', layerIndex);
+ this.error = "Invalid drilldown configuration";
+ this.givendata = [];
+ this.noDataAvailable = true;
+ return;
+ }
+ }
+
+ console.log('Drilldown config for level', this.currentDrilldownLevel, ':', drilldownConfig);
+
+ // Check if we have valid drilldown configuration
+ if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) {
+ console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
+ this.error = "Missing drilldown configuration";
+ this.givendata = [];
+ this.noDataAvailable = true;
+ return;
+ }
+
+ // Get the parameter value from the drilldown stack
+ let parameterValue = '';
+ if (this.drilldownStack.length > 0) {
+ const lastEntry = this.drilldownStack[this.drilldownStack.length - 1];
+ parameterValue = lastEntry.clickedValue || '';
+ console.log('Parameter value from last click:', parameterValue);
+ }
+
+ // Get the parameter field from drilldown config
+ const parameterField = drilldownConfig.parameter || '';
+ console.log('Parameter field:', parameterField);
+
+ console.log('Fetching drilldown data for level:', this.currentDrilldownLevel, {
+ apiUrl: drilldownConfig.apiUrl,
+ xAxis: drilldownConfig.xAxis,
+ yAxis: drilldownConfig.yAxis,
+ parameterField: parameterField,
+ parameterValue: parameterValue,
+ connection: this.connection
+ });
+
+ // Build the actual API URL with parameter replacement
+ let actualApiUrl = drilldownConfig.apiUrl;
+ console.log('Original API URL:', actualApiUrl);
+ console.log('Parameter value to use:', parameterValue);
+ console.log('Parameter field:', parameterField);
+
+ // Check if the URL contains angle brackets for parameter replacement
+ const hasAngleBrackets = /<[^>]+>/.test(actualApiUrl);
+
+ if (hasAngleBrackets && parameterValue) {
+ // Replace angle brackets placeholder with actual value
+ console.log('Replacing angle brackets with parameter value');
+ const encodedValue = encodeURIComponent(parameterValue);
+ actualApiUrl = actualApiUrl.replace(/<[^>]+>/g, encodedValue);
+ console.log('URL after angle bracket replacement:', actualApiUrl);
+ }
+
+ // Log the URL that will be called
+ let url = `chart/getdashjson/grid?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
+ if (parameterField && parameterValue) {
+ url += `¶meter=${encodeURIComponent(parameterField)}¶meterValue=${encodeURIComponent(parameterValue)}`;
+ }
+ console.log('Drilldown data URL:', url);
+
+ // Convert drilldown layer filters to filter parameters (if applicable)
+ const filterObj = {};
+
+ // Add drilldown layer filters
+ if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
+ drilldownConfig.filters.forEach((filter: any) => {
+ if (filter.field && filter.value) {
+ filterObj[filter.field] = filter.value;
+ }
+ });
+ }
+
+ // Add drilldownFilters
+ if (this.drilldownFilters && this.drilldownFilters.length > 0) {
+ this.drilldownFilters.forEach(filter => {
+ if (filter.field && filter.value) {
+ filterObj[filter.field] = filter.value;
+ }
+ });
+ }
+
+ // Add common filters
+ const commonFilters = this.filterService.getFilterValues();
+ const filterDefinitions = this.filterService.getFilters();
+ Object.keys(commonFilters).forEach(filterId => {
+ const filterValue = commonFilters[filterId];
+
+ // Find the filter definition to get the field name
+ const filterDef = this.filterService.getFilters().find(f => f.id === filterId);
+
+ if (filterDef && filterDef.field) {
+ const fieldName = filterDef.field;
+ if (filterValue !== undefined && filterValue !== null && filterValue !== '') {
+ filterObj[fieldName] = filterValue;
+ }
+ }
+ });
+
+ // Convert to JSON string for API call
+ let drilldownFilterParams = '';
+ if (Object.keys(filterObj).length > 0) {
+ drilldownFilterParams = JSON.stringify(filterObj);
+ }
+
+ console.log('Drilldown filter parameters:', drilldownFilterParams);
+
+ // For drilldown level, we pass the parameter value from the drilldown stack and drilldown filters
+ this.dashboardService.getChartData(
+ drilldownConfig.apiUrl, 'grid',
+ drilldownConfig.xAxis, drilldownConfig.yAxis,
+ this.connection,
+ parameterField, parameterValue,
+ drilldownFilterParams
+ ).subscribe(
+ (data: any) => {
+ console.log('Received drilldown data:', data);
+ if (data === null) {
+ console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
+ this.error = "No data Available";
+ this.givendata = [];
+ this.noDataAvailable = true;
+ return;
+ }
+
+ // Handle the actual data structure returned by the API
+ if (data && data.chartData) {
+ this.givendata = data.chartData;
+ this.extractDynamicHeaders(data.chartData);
+ this.error = this.givendata.length === 0 ? "No data Available" : undefined;
+ this.noDataAvailable = this.givendata.length === 0;
+ console.log('Updated grid with drilldown data:', this.givendata);
+ } else if (data && data.data) {
+ // Handle the original expected format as fallback
+ this.givendata = data.data;
+ this.extractDynamicHeaders(data.data);
+ this.error = this.givendata.length === 0 ? "No data Available" : undefined;
+ this.noDataAvailable = this.givendata.length === 0;
+ console.log('Updated grid with drilldown legacy data format:', this.givendata);
+ } else if (Array.isArray(data)) {
+ // Handle case where data is directly an array
+ this.givendata = data;
+ this.extractDynamicHeaders(data);
+ this.error = this.givendata.length === 0 ? "No data Available" : undefined;
+ this.noDataAvailable = this.givendata.length === 0;
+ console.log('Updated grid with drilldown array data:', this.givendata);
+ } else {
+ console.warn('Drilldown received data does not have expected structure', data);
+ this.error = "No valid data received";
+ this.givendata = [];
+ this.noDataAvailable = true;
+ }
+ },
+ (error) => {
+ console.error('Error fetching drilldown data:', error);
+ this.error = "Server Error";
+ this.givendata = [];
+ this.noDataAvailable = true;
+ }
+ );
+ }
+
+ // Reset to original data (go back to base level)
+ resetToOriginalData(): void {
+ console.log('Resetting to original data');
+ console.log('Current stack before reset:', this.drilldownStack);
+ console.log('Current level before reset:', this.currentDrilldownLevel);
+
+ this.currentDrilldownLevel = 0;
+ this.drilldownStack = [];
+
+ if (this.originalGridData.length > 0) {
+ // Create a deep copy to avoid reference issues
+ this.givendata = JSON.parse(JSON.stringify(this.originalGridData));
+ this.extractDynamicHeaders(this.givendata);
+ console.log('Restored original data');
+ }
+
+ console.log('After reset - data:', this.givendata);
+
+ // Re-fetch original data
+ this.fetchGridData();
+ }
+
+ // Navigate back to previous drilldown level
+ navigateBack(): void {
+ console.log('Navigating back, current stack:', this.drilldownStack);
+ console.log('Current level:', this.currentDrilldownLevel);
+
+ if (this.drilldownStack.length > 0) {
+ // Remove the last entry from the stack
+ const removedEntry = this.drilldownStack.pop();
+ console.log('Removed entry from stack:', removedEntry);
+
+ // Update the current drilldown level
+ this.currentDrilldownLevel = this.drilldownStack.length;
+ console.log('New level after pop:', this.currentDrilldownLevel);
+ console.log('Stack after pop:', this.drilldownStack);
+
+ if (this.drilldownStack.length > 0) {
+ // Fetch data for the previous level
+ console.log('Fetching data for previous level');
+ this.fetchDrilldownData();
+ } else {
+ // Back to base level
+ console.log('Back to base level, resetting to original data');
+ this.resetToOriginalData();
+ }
+ } else {
+ // Already at base level, reset to original data
+ console.log('Already at base level, resetting to original data');
+ this.resetToOriginalData();
+ }
+ }
+
+ // Method to handle grid row clicks for drilldown
+ onRowClick(item: any, key: string): void {
+ console.log('Grid row clicked:', { item, key });
+
+ // If drilldown is enabled
+ if (this.drilldownEnabled) {
+ // Get the value for the clicked key
+ const clickedValue = item[key];
+
+ console.log('Clicked on row value:', { key, value: clickedValue });
+
+ // If we're not at the base level, store original data
+ if (this.currentDrilldownLevel === 0) {
+ // Store original data before entering drilldown mode
+ // Create a deep copy to avoid reference issues
+ this.originalGridData = JSON.parse(JSON.stringify(this.givendata));
+ console.log('Stored original data for drilldown');
+ }
+
+ // Determine the next drilldown level
+ const nextDrilldownLevel = this.currentDrilldownLevel + 1;
+
+ console.log('Next drilldown level will be:', nextDrilldownLevel);
+
+ // Check if there's a drilldown configuration for this level
+ let hasDrilldownConfig = false;
+ let drilldownConfig;
+
+ if (nextDrilldownLevel === 1) {
+ // Base drilldown level
+ drilldownConfig = {
+ apiUrl: this.drilldownApiUrl,
+ xAxis: this.drilldownXAxis,
+ yAxis: this.drilldownYAxis,
+ parameter: this.drilldownParameter
+ };
+ hasDrilldownConfig = !!this.drilldownApiUrl && !!this.drilldownXAxis && !!this.drilldownYAxis;
+ } else {
+ // Multi-layer drilldown level
+ const layerIndex = nextDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ if (layerIndex < this.drilldownLayers.length) {
+ drilldownConfig = this.drilldownLayers[layerIndex];
+ hasDrilldownConfig = drilldownConfig.enabled &&
+ !!drilldownConfig.apiUrl &&
+ !!drilldownConfig.xAxis &&
+ !!drilldownConfig.yAxis;
+ }
+ }
+
+ console.log('Drilldown config for next level:', drilldownConfig);
+ console.log('Has drilldown config:', hasDrilldownConfig);
+
+ // If there's a drilldown configuration for the next level, proceed
+ if (hasDrilldownConfig) {
+ // Add this click to the drilldown stack
+ const stackEntry = {
+ level: nextDrilldownLevel,
+ clickedKey: key,
+ clickedValue: clickedValue
+ };
+
+ this.drilldownStack.push(stackEntry);
+
+ console.log('Added to drilldown stack:', stackEntry);
+ console.log('Current drilldown stack:', this.drilldownStack);
+
+ // Update the current drilldown level
+ this.currentDrilldownLevel = nextDrilldownLevel;
+
+ console.log('Entering drilldown level:', this.currentDrilldownLevel);
+
+ // Fetch drilldown data for the new level
+ this.fetchDrilldownData();
+ } else {
+ console.log('No drilldown configuration for level:', nextDrilldownLevel);
+ }
+ } else {
+ console.log('Drilldown not enabled');
+ }
+ }
+
+ /**
+ * Extract dynamic headers from the data
+ * @param data Array of data objects
+ */
+ private extractDynamicHeaders(data: any): void {
+ // Ensure data is an array
+ const dataArray = Array.isArray(data) ? data : [];
+
+ if (dataArray && dataArray.length > 0) {
+ // Get all unique keys from the data objects
+ const allKeys = new Set();
+ dataArray.forEach(item => {
+ if (item && typeof item === 'object') {
+ Object.keys(item).forEach(key => allKeys.add(key));
+ }
+ });
+
+ // Convert to array of header objects with key and display name
+ this.dynamicHeaders = Array.from(allKeys).map(key => ({
+ key: key,
+ displayName: this.formatHeader(key)
+ }));
+
+ console.log('Dynamic headers extracted:', this.dynamicHeaders);
+ } else {
+ this.dynamicHeaders = [];
+ }
+ }
+
+ /**
+ * Format header name for better display
+ * @param key The key to format
+ */
+ private formatHeader(key: string): string {
+ // Convert camelCase to Title Case
+ return key
+ .replace(/([A-Z])/g, ' $1')
+ .replace(/^./, str => str.toUpperCase());
+ }
+
+ // Check if there are active filters
+ hasActiveFilters(): boolean {
+ return (this.baseFilters && this.baseFilters.length > 0) ||
+ (this.drilldownFilters && this.drilldownFilters.length > 0) ||
+ this.hasActiveLayerFilters();
+ }
+
+ // Check if there are active layer filters for current drilldown level
+ hasActiveLayerFilters(): boolean {
+ if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
+ const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ return layerIndex < this.drilldownLayers.length &&
+ this.drilldownLayers[layerIndex].filters &&
+ this.drilldownLayers[layerIndex].filters.length > 0;
+ }
+ return false;
+ }
+
+ // Get active layer filters for current drilldown level
+ getActiveLayerFilters(): any[] {
+ if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
+ const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ if (layerIndex < this.drilldownLayers.length &&
+ this.drilldownLayers[layerIndex].filters) {
+ return this.drilldownLayers[layerIndex].filters;
+ }
+ }
+ return [];
+ }
+
+ // Get filter options for dropdown/multiselect filters
+ getFilterOptions(filter: any): string[] {
+ if (filter.options) {
+ if (Array.isArray(filter.options)) {
+ return filter.options;
+ } else if (typeof filter.options === 'string') {
+ return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
+ }
+ }
+ return [];
+ }
+
+ // Check if an option is selected for multiselect filters
+ isOptionSelected(filter: any, option: string): boolean {
+ if (!filter.value) {
+ return false;
+ }
+
+ if (Array.isArray(filter.value)) {
+ return filter.value.includes(option);
+ }
+
+ return filter.value === option;
+ }
+
+ // Handle base filter changes
+ onBaseFilterChange(filter: any): void {
+ console.log('Base filter changed:', filter);
+ // Refresh data when filter changes
+ this.fetchGridData();
+ }
+
+ // Handle drilldown filter changes
+ onDrilldownFilterChange(filter: any): void {
+ console.log('Drilldown filter changed:', filter);
+ // Refresh data when filter changes
+ this.fetchGridData();
+ }
+
+ // Handle layer filter changes
+ onLayerFilterChange(filter: any): void {
+ console.log('Layer filter changed:', filter);
+ // Refresh data when filter changes
+ this.fetchGridData();
+ }
+
+ // Handle multiselect changes
+ onMultiSelectChange(filter: any, option: string, event: any): void {
+ const checked = event.target.checked;
+
+ // Initialize filter.value as array if it's not already
+ if (!Array.isArray(filter.value)) {
+ filter.value = [];
+ }
+
+ if (checked) {
+ // Add option to array if not already present
+ if (!filter.value.includes(option)) {
+ filter.value.push(option);
+ }
+ } else {
+ // Remove option from array
+ filter.value = filter.value.filter((item: string) => item !== option);
+ }
+
+ // Refresh data when filter changes
+ this.fetchGridData();
+ }
+
+ // Handle date range changes
+ onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
+ filter.value = dateRange;
+ // Refresh data when filter changes
+ this.fetchGridData();
+ }
+
+ // Handle toggle changes
+ onToggleChange(filter: any, checked: boolean): void {
+ filter.value = checked;
+ // Refresh data when filter changes
+ this.fetchGridData();
+ }
+
+ // Toggle multiselect dropdown visibility
+ toggleMultiselect(filter: any, context: string): void {
+ const filterId = `${context}-${filter.field}`;
+ if (this.isMultiselectOpen(filter, context)) {
+ this.openMultiselects.delete(filterId);
+ } else {
+ // Close all other multiselects first
+ this.openMultiselects.clear();
+ this.openMultiselects.set(filterId, context);
+
+ // Add document click handler to close dropdown when clicking outside
+ this.addDocumentClickHandler();
+ }
+ }
+
+ // Add document click handler to close dropdowns when clicking outside
+ private addDocumentClickHandler(): void {
+ if (!this.documentClickHandler) {
+ this.documentClickHandler = (event: MouseEvent) => {
+ const target = event.target as HTMLElement;
+ // Check if click is outside any multiselect dropdown
+ if (!target.closest('.multiselect-container')) {
+ this.openMultiselects.clear();
+ this.removeDocumentClickHandler();
+ }
+ };
+
+ // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
+ setTimeout(() => {
+ document.addEventListener('click', this.documentClickHandler!);
+ }, 0);
+ }
+ }
+
+ // Remove document click handler
+ private removeDocumentClickHandler(): void {
+ if (this.documentClickHandler) {
+ document.removeEventListener('click', this.documentClickHandler);
+ this.documentClickHandler = null;
+ }
+ }
+
+ // Check if multiselect dropdown is open
+ isMultiselectOpen(filter: any, context: string): boolean {
+ const filterId = `${context}-${filter.field}`;
+ return this.openMultiselects.has(filterId);
+ }
+
+ // Get count of selected options for a multiselect filter
+ getSelectedOptionsCount(filter: any): number {
+ if (!filter.value) {
+ return 0;
+ }
+
+ if (Array.isArray(filter.value)) {
+ return filter.value.length;
+ }
+
+ return 0;
+ }
+
+ // Clear all filters
+ clearAllFilters(): void {
+ // Clear base filters
+ if (this.baseFilters) {
+ this.baseFilters.forEach(filter => {
+ if (filter.type === 'multiselect') {
+ filter.value = [];
+ } else if (filter.type === 'date-range') {
+ filter.value = { start: null, end: null };
+ } else if (filter.type === 'toggle') {
+ filter.value = false;
+ } else {
+ filter.value = '';
+ }
+ });
+ }
+
+ // Clear drilldown filters
+ if (this.drilldownFilters) {
+ this.drilldownFilters.forEach(filter => {
+ if (filter.type === 'multiselect') {
+ filter.value = [];
+ } else if (filter.type === 'date-range') {
+ filter.value = { start: null, end: null };
+ } else if (filter.type === 'toggle') {
+ filter.value = false;
+ } else {
+ filter.value = '';
+ }
+ });
+ }
+
+ // Clear layer filters
+ if (this.drilldownLayers) {
+ this.drilldownLayers.forEach(layer => {
+ if (layer.filters) {
+ layer.filters.forEach((filter: any) => {
+ if (filter.type === 'multiselect') {
+ filter.value = [];
+ } else if (filter.type === 'date-range') {
+ filter.value = { start: null, end: null };
+ } else if (filter.type === 'toggle') {
+ filter.value = false;
+ } else {
+ filter.value = '';
+ }
+ });
+ }
+ });
+ }
+
+ // Close all multiselect dropdowns
+ this.openMultiselects.clear();
+
+ // Refresh data
+ this.fetchGridData();
+ }
+
+ ngOnDestroy() {
+ // Unsubscribe from all subscriptions to prevent memory leaks
+ console.log('GridViewComponent ngOnDestroy called, unsubscribing from', this.subscriptions.length, 'subscriptions');
+ this.subscriptions.forEach(subscription => {
+ if (subscription && !subscription.closed) {
+ subscription.unsubscribe();
+ }
+ });
+ this.subscriptions = [];
+
+ // Clear data to help with garbage collection
+ this.givendata = [];
+ this.dynamicHeaders = [];
+ this.drilldownStack = [];
+ this.originalGridData = [];
+
+ // Clear multiselect tracking
+ this.openMultiselects.clear();
+
+ // Remove document click handler
+ this.removeDocumentClickHandler();
+
+ console.log('GridViewComponent destroyed and cleaned up');
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/line-chart/line-chart.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/line-chart/line-chart.component.html
index 18c620d..8d9331b 100644
--- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/line-chart/line-chart.component.html
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/line-chart/line-chart.component.html
@@ -1,12 +1,299 @@
-
-
+
+
+
+
+
0">
+
Base Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
0 && currentDrilldownLevel > 0">
+
Drilldown Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+
Layer Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+ Clear All Filters
+
+
+
+
+
+
+
+
0">
+
+
+
+
+
+
+
+
+ Drilldown Level: {{currentDrilldownLevel}}
+ 0">
+ (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
+
+
+
+
+
+
+
+
+
+
+ No data available
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/line-chart/line-chart.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/line-chart/line-chart.component.scss
index e69de29..a9282e4 100644
--- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/line-chart/line-chart.component.scss
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/line-chart/line-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/line-chart/line-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/line-chart/line-chart.component.ts
index 3eaad81..bdee0d6 100644
--- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/line-chart/line-chart.component.ts
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/line-chart/line-chart.component.ts
@@ -1,71 +1,964 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, Input, OnChanges, SimpleChanges, OnDestroy } 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-line-chart',
templateUrl: './line-chart.component.html',
styleUrls: ['./line-chart.component.scss']
})
-export class LineChartComponent implements OnInit {
- public lineChartData:Array
= [
- {data: [65, 59, 80, 81, 56, 55, 40], label: 'Series A'},
- {data: [28, 48, 40, 19, 86, 27, 90], label: 'Series B'},
- {data: [18, 48, 77, 9, 100, 27, 40], label: 'Series C'}
- ];
- public lineChartLabels:Array = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
- public lineChartOptions:any = {
- responsive: true
- };
- public lineChartColors:Array = [
- { // grey
- backgroundColor: 'rgba(148,159,177,0.2)',
- borderColor: 'rgba(148,159,177,1)',
- pointBackgroundColor: 'rgba(148,159,177,1)',
- pointBorderColor: '#fff',
- pointHoverBackgroundColor: '#fff',
- pointHoverBorderColor: 'rgba(148,159,177,0.8)'
- },
- { // dark grey
- backgroundColor: 'rgba(77,83,96,0.2)',
- borderColor: 'rgba(77,83,96,1)',
- pointBackgroundColor: 'rgba(77,83,96,1)',
- pointBorderColor: '#fff',
- pointHoverBackgroundColor: '#fff',
- pointHoverBorderColor: 'rgba(77,83,96,1)'
- },
- { // grey
- backgroundColor: 'rgba(148,159,177,0.2)',
- borderColor: 'rgba(148,159,177,1)',
- pointBackgroundColor: 'rgba(148,159,177,1)',
- pointBorderColor: '#fff',
- pointHoverBackgroundColor: '#fff',
- pointHoverBorderColor: 'rgba(148,159,177,0.8)'
- }
- ];
- public lineChartLegend:boolean = true;
- public lineChartType:string = 'line';
+export class LineChartComponent implements OnInit, OnChanges, OnDestroy {
+ @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; // 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
- public randomize():void {
- let _lineChartData:Array = new Array(this.lineChartData.length);
- for (let i = 0; i < this.lineChartData.length; i++) {
- _lineChartData[i] = {data: new Array(this.lineChartData[i].data.length), label: this.lineChartData[i].label};
- for (let j = 0; j < this.lineChartData[i].data.length; j++) {
- _lineChartData[i].data[j] = Math.floor((Math.random() * 100) + 1);
- }
- }
- this.lineChartData = _lineChartData;
- }
+ public lineChartData: Array = [
+ {data: [65, 59, 80, 81, 56, 55, 40], label: 'Series A'},
+ {data: [28, 48, 40, 19, 86, 27, 90], label: 'Series B'},
+ {data: [18, 48, 77, 9, 100, 27, 40], label: 'Series C'}
+ ];
+ public lineChartLabels: Array = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
+ public lineChartOptions: any = {
+ responsive: true
+ };
+ public lineChartColors: Array = [
+ { // grey
+ backgroundColor: 'rgba(148,159,177,0.2)',
+ borderColor: 'rgba(148,159,177,1)',
+ pointBackgroundColor: 'rgba(148,159,177,1)',
+ pointBorderColor: '#fff',
+ pointHoverBackgroundColor: '#fff',
+ pointHoverBorderColor: 'rgba(148,159,177,0.8)'
+ },
+ { // dark grey
+ backgroundColor: 'rgba(77,83,96,0.2)',
+ borderColor: 'rgba(77,83,96,1)',
+ pointBackgroundColor: 'rgba(77,83,96,1)',
+ pointBorderColor: '#fff',
+ pointHoverBackgroundColor: '#fff',
+ pointHoverBorderColor: 'rgba(77,83,96,1)'
+ },
+ { // grey
+ backgroundColor: 'rgba(148,159,177,0.2)',
+ borderColor: 'rgba(148,159,177,1)',
+ pointBackgroundColor: 'rgba(148,159,177,1)',
+ pointBorderColor: '#fff',
+ pointHoverBackgroundColor: '#fff',
+ pointHoverBorderColor: 'rgba(148,159,177,0.8)'
+ }
+ ];
+ public lineChartLegend: boolean = true;
+ public lineChartType: string = 'line';
+
+ // Multi-layer drilldown state tracking
+ drilldownStack: any[] = []; // Stack to track drilldown navigation history
+ currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
+ originalLineChartLabels: Array = [];
+ originalLineChartData: Array = [];
+
+ // No data state
+ noDataAvailable: boolean = false;
+
+ // Flag to prevent infinite loops
+ private isFetchingData: boolean = false;
+
+ // Subscriptions to unsubscribe on destroy
+ private subscriptions: Subscription[] = [];
- // events
- public chartClicked(e:any):void {
- console.log(e);
- }
+ // 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;
- public chartHovered(e:any):void {
- console.log(e);
- }
- constructor() { }
+ 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();
}
-}
+ ngOnChanges(changes: SimpleChanges): void {
+ console.log('LineChartComponent input changes:', changes);
+
+ // Initialize filter values if they haven't been initialized yet
+ if (!this.filtersInitialized && (changes.baseFilters || changes.drilldownFilters || changes.drilldownLayers)) {
+ this.initializeFilterValues();
+ this.filtersInitialized = true;
+ }
+
+ // Check if any of the key properties have changed
+ 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 && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged || drilldownFiltersChanged ||
+ drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
+ drilldownLayersChanged)) {
+ console.log('Chart configuration changed, fetching new data');
+ this.fetchChartData();
+ }
+
+ // Update legend visibility if it changed
+ if (changes.chartlegend !== undefined) {
+ this.lineChartLegend = changes.chartlegend.currentValue;
+ console.log('Chart legend changed to:', this.lineChartLegend);
+ }
+ }
+
+ 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
+ refreshData(): void {
+ this.fetchChartData();
+ }
+
+ fetchChartData(): void {
+ // Set flag to prevent recursive calls
+ this.isFetchingData = true;
+
+ // If we're in drilldown mode, fetch the appropriate drilldown data
+ if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
+ this.fetchDrilldownData();
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ return;
+ }
+
+ // If we have the necessary data, fetch chart data from the service
+ if (this.table && this.xAxis && this.yAxis) {
+ console.log('Fetching chart data for:', { 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);
+
+ // Log the URL that will be called
+ const url = `chart/getdashjson/line?tableName=${this.table}&xAxis=${this.xAxis}&yAxes=${yAxisString}${this.connection ? `&sureId=${this.connection}` : ''}`;
+ 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
+ this.dashboardService.getChartData(this.table, 'line', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
+ (data: any) => {
+ console.log('Received chart data:', data);
+ if (data === null) {
+ console.warn('API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ this.lineChartLabels = [];
+ this.lineChartData = [];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ 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;
+ this.lineChartLabels = data.chartLabels;
+ this.lineChartData = data.chartData;
+ // Trigger change detection
+ this.lineChartData = [...this.lineChartData];
+ console.log('Updated line chart with data:', { labels: this.lineChartLabels, data: this.lineChartData });
+ } else if (data && data.labels && data.datasets) {
+ // Backend has already filtered the data, just display it
+ this.noDataAvailable = data.labels.length === 0;
+ this.lineChartLabels = data.labels;
+ this.lineChartData = data.datasets;
+ // Trigger change detection
+ this.lineChartData = [...this.lineChartData];
+ console.log('Updated line chart with legacy data format:', { labels: this.lineChartLabels, data: this.lineChartData });
+ } else {
+ console.warn('Received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ this.lineChartLabels = [];
+ this.lineChartData = [];
+ }
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ },
+ (error) => {
+ console.error('Error fetching chart data:', error);
+ this.noDataAvailable = true;
+ this.lineChartLabels = [];
+ this.lineChartData = [];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ // Keep default data in case of error
+ }
+ );
+ } else {
+ console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
+ this.noDataAvailable = true;
+ this.lineChartLabels = [];
+ this.lineChartData = [];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ }
+ }
+
+ // Fetch drilldown data based on current drilldown level
+ fetchDrilldownData(): void {
+ console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
+ console.log('Drilldown stack:', this.drilldownStack);
+
+ // Get the current drilldown configuration based on the current level
+ let drilldownConfig;
+ if (this.currentDrilldownLevel === 1) {
+ // Base drilldown level
+ drilldownConfig = {
+ apiUrl: this.drilldownApiUrl,
+ xAxis: this.drilldownXAxis,
+ yAxis: this.drilldownYAxis,
+ parameter: this.drilldownParameter
+ };
+ } else {
+ // Multi-layer drilldown level
+ const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) {
+ drilldownConfig = this.drilldownLayers[layerIndex];
+ } else {
+ console.warn('Invalid drilldown layer index:', layerIndex);
+ this.noDataAvailable = true;
+ this.lineChartLabels = [];
+ this.lineChartData = [];
+ 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.lineChartLabels = [];
+ this.lineChartData = [];
+ 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)
+ let filterParams = '';
+ if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
+ const filterObj = {};
+ drilldownConfig.filters.forEach((filter: any) => {
+ if (filter.field && filter.value) {
+ filterObj[filter.field] = filter.value;
+ }
+ });
+ if (Object.keys(filterObj).length > 0) {
+ filterParams = JSON.stringify(filterObj);
+ }
+ }
+
+ // Add common filters to drilldown filter parameters
+ const commonFilters = this.filterService.getFilterValues();
+ if (Object.keys(commonFilters).length > 0) {
+ // Merge common filters with drilldown filters
+ const mergedFilterObj = {};
+
+ // Add drilldown filters first
+ if (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/line?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
+ console.log('Drilldown data URL:', url);
+
+ // 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, 'line', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, filterParams).subscribe(
+ (data: any) => {
+ console.log('Received drilldown data:', data);
+ if (data === null) {
+ console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ this.lineChartLabels = [];
+ this.lineChartData = [];
+ 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;
+ this.lineChartLabels = data.chartLabels;
+ this.lineChartData = data.chartData;
+ // Trigger change detection
+ this.lineChartData = [...this.lineChartData];
+ console.log('Updated line chart with drilldown data:', { labels: this.lineChartLabels, data: this.lineChartData });
+ } else if (data && data.labels && data.datasets) {
+ // Backend has already filtered the data, just display it
+ this.noDataAvailable = data.labels.length === 0;
+ this.lineChartLabels = data.labels;
+ this.lineChartData = data.datasets;
+ // Trigger change detection
+ this.lineChartData = [...this.lineChartData];
+ console.log('Updated line chart with drilldown legacy data format:', { labels: this.lineChartLabels, data: this.lineChartData });
+ } else {
+ console.warn('Drilldown received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ this.lineChartLabels = [];
+ this.lineChartData = [];
+ }
+ },
+ (error) => {
+ console.error('Error fetching drilldown data:', error);
+ this.noDataAvailable = true;
+ this.lineChartLabels = [];
+ this.lineChartData = [];
+ // Keep current data in case of error
+ }
+ );
+ }
+
+ // 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.originalLineChartLabels.length > 0) {
+ this.lineChartLabels = [...this.originalLineChartLabels];
+ console.log('Restored original labels');
+ }
+ if (this.originalLineChartData.length > 0) {
+ this.lineChartData = [...this.originalLineChartData];
+ console.log('Restored original data');
+ }
+
+ console.log('After reset - labels:', this.lineChartLabels);
+ console.log('After reset - data:', this.lineChartData);
+
+ // 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();
+ }
+ }
+
+ // Ensure labels and data arrays have the same length
+ private syncLabelAndDataArrays(): void {
+ // For line charts, we need to ensure all datasets have the same number of data points
+ if (this.lineChartData && this.lineChartData.length > 0 && this.lineChartLabels) {
+ const labelCount = this.lineChartLabels.length;
+
+ this.lineChartData.forEach(dataset => {
+ if (dataset.data) {
+ // If dataset has more data points than labels, truncate the data
+ if (dataset.data.length > labelCount) {
+ dataset.data = dataset.data.slice(0, labelCount);
+ }
+ // If dataset has fewer data points than labels, pad with zeros
+ else if (dataset.data.length < labelCount) {
+ while (dataset.data.length < labelCount) {
+ dataset.data.push(0);
+ }
+ }
+ }
+ });
+ }
+ }
+
+ public randomize(): void {
+ let _lineChartData: Array = new Array(this.lineChartData.length);
+ for (let i = 0; i < this.lineChartData.length; i++) {
+ _lineChartData[i] = {data: new Array(this.lineChartData[i].data.length), label: this.lineChartData[i].label};
+ for (let j = 0; j < this.lineChartData[i].data.length; j++) {
+ _lineChartData[i].data[j] = Math.floor((Math.random() * 100) + 1);
+ }
+ }
+ this.lineChartData = _lineChartData;
+ }
+
+ // events
+ public chartClicked(e: any): void {
+ console.log('Line 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 label of the clicked element
+ const clickedLabel = this.lineChartLabels[clickedIndex];
+
+ console.log('Clicked on line 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.originalLineChartLabels = [...this.lineChartLabels];
+ this.originalLineChartData = [...this.lineChartData];
+ 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,
+ clickedIndex: clickedIndex,
+ clickedLabel: clickedLabel,
+ clickedValue: clickedLabel // Using label 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(e);
+ }
+}
\ 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 ef42eed..0e9c001 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,9 +1,312 @@
-
-
-
-
+
+
+
+
+
0">
+
Base Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
0 && currentDrilldownLevel > 0">
+
Drilldown Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+
Layer Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+ Clear All Filters
+
+
+
+
+
+
+
+
0">
+
+
+
+
+
+
+
+
+ Drilldown Level: {{currentDrilldownLevel}}
+ 0">
+ (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading chart data...
+
+
+
+
+
No chart data available
+
+
+
+
+
+
+
0">
+
+
+ {{ label }}
+ {{ pieChartData && pieChartData[i] !== undefined ? pieChartData[i] : 0 }}
+
+
+
\ 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.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/pie-chart/pie-chart.component.scss
index e69de29..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
@@ -0,0 +1,378 @@
+.pie-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;
+}
+
+.pie-chart-container:hover {
+ box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
+ transform: translateY(-2px);
+}
+
+.chart-title {
+ font-size: 26px;
+ font-weight: 700;
+ color: #2c3e50;
+ margin-bottom: 20px;
+ text-align: center;
+ padding-bottom: 15px;
+ border-bottom: 2px solid #3498db;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+.chart-wrapper {
+ position: relative;
+ flex: 1;
+ min-height: 250px;
+ margin: 15px 0;
+ background: #f8f9fa;
+ border: 1px solid #e9ecef;
+ border-radius: 8px;
+ padding: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.chart-wrapper canvas {
+ max-width: 100%;
+ max-height: 100%;
+ filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
+}
+
+.chart-wrapper canvas:hover {
+ filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15));
+ transform: scale(1.02);
+ transition: all 0.3s ease;
+}
+
+.chart-legend {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 15px;
+ margin-top: 20px;
+ padding: 20px;
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
+ border-radius: 8px;
+ border: 1px solid #dee2e6;
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
+}
+
+.legend-item {
+ display: flex;
+ align-items: center;
+ padding: 12px 20px;
+ background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
+ border-radius: 25px;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+ border: 1px solid #eaeaea;
+ cursor: pointer;
+}
+
+.legend-item:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
+ border-color: #3498db;
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
+}
+
+.legend-color {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ margin-right: 12px;
+ display: inline-block;
+ border: 2px solid white;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+}
+
+.legend-label {
+ font-size: 16px;
+ font-weight: 600;
+ color: #2c3e50;
+ margin-right: 15px;
+ white-space: nowrap;
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+}
+
+.legend-value {
+ font-size: 16px;
+ font-weight: 700;
+ color: #3498db;
+ background: linear-gradient(135deg, #e9ecef 0%, #dde1e5 100%);
+ padding: 6px 12px;
+ border-radius: 12px;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+ min-width: 40px;
+ text-align: center;
+}
+
+.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); }
+}
+
+// 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 {
+ font-size: 20px;
+ margin-bottom: 15px;
+ }
+
+ .chart-wrapper {
+ min-height: 200px;
+ }
+
+ .chart-legend {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .legend-item {
+ width: 100%;
+ max-width: 300px;
+ justify-content: space-between;
+ }
+
+ .no-data-message {
+ 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 a6647a6..eabb63c 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
@@ -1,27 +1,1058 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, Input, OnChanges, SimpleChanges, AfterViewChecked, OnDestroy } 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-pie-chart',
templateUrl: './pie-chart.component.html',
styleUrls: ['./pie-chart.component.scss']
})
-export class PieChartComponent implements OnInit {
+export class PieChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {
+ @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; // Add connection input
+ // Drilldown configuration inputs
+ @Input() drilldownEnabled: boolean = false;
+ @Input() drilldownApiUrl: string;
+ @Input() drilldownXAxis: string;
+ @Input() drilldownYAxis: string;
+ @Input() drilldownParameter: string; // Add drilldown parameter input
+ @Input() baseFilters: any[] = []; // Add base filters input
+ @Input() drilldownFilters: any[] = []; // Add drilldown filters input
+ // Multi-layer drilldown configuration inputs
+ @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
- constructor() { }
-
- ngOnInit(): void {
- }
- public pieChartLabels: string[] = ['SciFi', 'Drama', 'Comedy'];
+ public pieChartLabels: string[] = ['Category A', 'Category B', 'Category C'];
public pieChartData: number[] = [30, 50, 20];
+ public pieChartDatasets: any[] = [
+ {
+ data: [30, 50, 20],
+ label: 'Dataset 1'
+ }
+ ];
public pieChartType: string = 'pie';
+ public pieChartOptions: any = {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ display: false // We'll create our own legend
+ },
+ tooltip: {
+ enabled: true,
+ mode: 'index',
+ intersect: false,
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
+ titleFont: {
+ size: 16,
+ color: '#fff'
+ },
+ bodyFont: {
+ size: 14,
+ color: '#fff'
+ },
+ cornerRadius: 4,
+ displayColors: false
+ }
+ },
+ animation: {
+ animateRotate: true,
+ animateScale: false
+ },
+ elements: {
+ arc: {
+ borderWidth: 2,
+ borderColor: '#fff'
+ }
+ }
+ };
+
+ // Chart colors for consistent styling
+ private chartColors: string[] = [
+ '#FF6384',
+ '#36A2EB',
+ '#FFCE56',
+ '#4BC0C0',
+ '#9966FF',
+ '#FF9F40',
+ '#FF6384',
+ '#C9CBCF'
+ ];
+
+ // Multi-layer drilldown state tracking
+ drilldownStack: any[] = []; // Stack to track drilldown navigation history
+ currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
+ originalPieChartLabels: string[] = [];
+ originalPieChartData: number[] = [];
+
+ // No data state
+ noDataAvailable: boolean = false;
+
+ // Flag to prevent infinite loops
+ 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;
+
+ constructor(
+ private dashboardService: Dashboard3Service,
+ private filterService: FilterService
+ ) { }
+
+ /**
+ * Force chart redraw
+ */
+ public redrawChart(): void {
+ // This method can be called to force a chart redraw if needed
+ console.log('Redrawing pie chart');
+ this.pieChartData = [...this.pieChartData];
+ }
+
+ ngOnInit(): void {
+ // Subscribe to filter changes
+ this.subscriptions.push(
+ this.filterService.filterState$.subscribe(filters => {
+ // When filters change, refresh the chart data
+ this.fetchChartData();
+ })
+ );
+
+ console.log('PieChartComponent initialized with default data:', { labels: this.pieChartLabels, data: this.pieChartData });
+ // Validate initial data
+ this.validateChartData();
+ // Initialize datasets with default data
+ this.pieChartDatasets = [
+ {
+ data: this.pieChartData,
+ label: 'Dataset 1'
+ }
+ ];
+ // Only fetch data if we have the required inputs, otherwise show default data
+ if (this.table && this.xAxis && this.yAxis) {
+ this.fetchChartData();
+ }
+ }
+
+ 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;
+ 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 && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged || drilldownFiltersChanged ||
+ drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
+ drilldownLayersChanged)) {
+ console.log('Chart configuration changed, fetching new data');
+ this.fetchChartData();
+ }
+
+ // If we have the required inputs and haven't fetched data yet, fetch it
+ if ((xAxisChanged || yAxisChanged || tableChanged) && this.table && this.xAxis && this.yAxis && !this.isFetchingData) {
+ console.log('Required inputs available, fetching data');
+ this.fetchChartData();
+ }
+ }
+
+ 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
+ refreshData(): void {
+ this.fetchChartData();
+ }
+
+ fetchChartData(): void {
+ // Set flag to prevent recursive calls
+ this.isFetchingData = true;
+
+ // If we're in drilldown mode, fetch the appropriate drilldown data
+ if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
+ this.fetchDrilldownData();
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ return;
+ }
+
+ // If we have the necessary data, fetch chart data from the service
+ if (this.table && this.xAxis && this.yAxis) {
+ console.log('Fetching pie chart data for:', { 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);
+
+ // 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('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
+ this.dashboardService.getChartData(this.table, 'pie', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
+ (data: any) => {
+ console.log('Received pie chart data:', data);
+ if (data === null) {
+ console.warn('API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ 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;
+ this.pieChartLabels = data.chartLabels;
+
+ // Extract the actual data values from the chartData array
+ // chartData is an array with one object containing the data
+ if (data.chartData.length > 0 && data.chartData[0].data) {
+ this.pieChartData = data.chartData[0].data;
+ } else {
+ this.pieChartData = [];
+ }
+
+ // Trigger change detection
+ this.pieChartLabels = [...this.pieChartLabels];
+ this.pieChartData = [...this.pieChartData];
+ this.pieChartDatasets = [
+ {
+ data: this.pieChartData,
+ label: 'Dataset 1'
+ }
+ ];
+ console.log('Updated pie chart with data:', { labels: this.pieChartLabels, data: this.pieChartData });
+ } 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.datasets[0]?.data || [];
+ // Trigger change detection
+ this.pieChartLabels = [...this.pieChartLabels];
+ this.pieChartData = [...this.pieChartData];
+ this.pieChartDatasets = [
+ {
+ data: this.pieChartData,
+ label: 'Dataset 1'
+ }
+ ];
+ console.log('Updated pie chart with legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
+ } else {
+ console.warn('Received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ // Keep default data if no data is available
+ if (this.pieChartLabels.length === 0 && this.pieChartData.length === 0) {
+ this.pieChartLabels = ['Category A', 'Category B', 'Category C'];
+ this.pieChartData = [30, 50, 20];
+ this.pieChartDatasets = [
+ {
+ data: this.pieChartData,
+ label: 'Dataset 1'
+ }
+ ];
+ }
+ }
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ },
+ (error) => {
+ console.error('Error fetching pie chart data:', error);
+ this.noDataAvailable = true;
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ }
+ );
+ } else {
+ console.log('Missing required data for chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
+ this.noDataAvailable = true;
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ }
+ }
+
+ // Fetch drilldown data based on current drilldown level
+ fetchDrilldownData(): void {
+ console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
+ console.log('Drilldown stack:', this.drilldownStack);
+
+ // Get the current drilldown configuration based on the current level
+ let drilldownConfig;
+ if (this.currentDrilldownLevel === 1) {
+ // Base drilldown level
+ drilldownConfig = {
+ apiUrl: this.drilldownApiUrl,
+ xAxis: this.drilldownXAxis,
+ yAxis: this.drilldownYAxis,
+ parameter: this.drilldownParameter
+ };
+ } else {
+ // Multi-layer drilldown level
+ const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) {
+ drilldownConfig = this.drilldownLayers[layerIndex];
+ } else {
+ console.warn('Invalid drilldown layer index:', layerIndex);
+ this.noDataAvailable = true;
+ return;
+ }
+ }
+
+ console.log('Drilldown config for level', this.currentDrilldownLevel, ':', drilldownConfig);
+
+ // Check if we have valid drilldown configuration
+ if (!drilldownConfig || !drilldownConfig.apiUrl || !drilldownConfig.xAxis || !drilldownConfig.yAxis) {
+ console.warn('Missing drilldown configuration for level:', this.currentDrilldownLevel);
+ this.noDataAvailable = true;
+ return;
+ }
+
+ // Get the parameter value from the drilldown stack
+ let parameterValue = '';
+ if (this.drilldownStack.length > 0) {
+ const lastEntry = this.drilldownStack[this.drilldownStack.length - 1];
+ parameterValue = lastEntry.clickedValue || '';
+ console.log('Parameter value from last click:', parameterValue);
+ }
+
+ // Get the parameter field from drilldown config
+ const parameterField = drilldownConfig.parameter || '';
+ console.log('Parameter field:', parameterField);
+
+ console.log('Fetching drilldown data for level:', this.currentDrilldownLevel, {
+ apiUrl: drilldownConfig.apiUrl,
+ xAxis: drilldownConfig.xAxis,
+ yAxis: drilldownConfig.yAxis,
+ parameterField: parameterField,
+ parameterValue: parameterValue,
+ connection: this.connection
+ });
+
+ // Build the actual API URL with parameter replacement
+ let actualApiUrl = drilldownConfig.apiUrl;
+ console.log('Original API URL:', actualApiUrl);
+ console.log('Parameter value to use:', parameterValue);
+ console.log('Parameter field:', parameterField);
+
+ // Check if the URL contains angle brackets for parameter replacement
+ const hasAngleBrackets = /<[^>]+>/.test(actualApiUrl);
+
+ if (hasAngleBrackets && parameterValue) {
+ // Replace angle brackets placeholder with actual value
+ console.log('Replacing angle brackets with parameter value');
+ const encodedValue = encodeURIComponent(parameterValue);
+ actualApiUrl = actualApiUrl.replace(/<[^>]+>/g, encodedValue);
+ console.log('URL after angle bracket replacement:', actualApiUrl);
+ }
+
+ // Convert drilldown layer filters to filter parameters (if applicable)
+ let filterParams = '';
+ if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
+ const filterObj = {};
+ drilldownConfig.filters.forEach((filter: any) => {
+ if (filter.field && filter.value) {
+ filterObj[filter.field] = filter.value;
+ }
+ });
+ if (Object.keys(filterObj).length > 0) {
+ filterParams = JSON.stringify(filterObj);
+ }
+ }
+
+ // Add common filters to drilldown filter parameters
+ const commonFilters = this.filterService.getFilterValues();
+ if (Object.keys(commonFilters).length > 0) {
+ // Merge common filters with drilldown filters
+ const mergedFilterObj = {};
+
+ // Add drilldown filters first
+ if (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/pie?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
+ console.log('Drilldown data URL:', url);
+
+ // 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, 'pie', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, filterParams).subscribe(
+ (data: any) => {
+ console.log('Received drilldown data:', data);
+ if (data === null) {
+ console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ 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;
+ this.pieChartLabels = data.chartLabels;
+
+ // Extract the actual data values from the chartData array
+ // chartData is an array with one object containing the data
+ if (data.chartData.length > 0 && data.chartData[0].data) {
+ this.pieChartData = data.chartData[0].data;
+ } else {
+ this.pieChartData = [];
+ }
+
+ // Trigger change detection
+ this.pieChartLabels = [...this.pieChartLabels];
+ this.pieChartData = [...this.pieChartData];
+ this.pieChartDatasets = [
+ {
+ data: this.pieChartData,
+ label: 'Dataset 1'
+ }
+ ];
+ console.log('Updated pie chart with drilldown data:', { labels: this.pieChartLabels, data: this.pieChartData });
+ } 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.datasets[0]?.data || [];
+ // Trigger change detection
+ this.pieChartLabels = [...this.pieChartLabels];
+ this.pieChartData = [...this.pieChartData];
+ this.pieChartDatasets = [
+ {
+ data: this.pieChartData,
+ label: 'Dataset 1'
+ }
+ ];
+ console.log('Updated pie chart with drilldown legacy data format:', { labels: this.pieChartLabels, data: this.pieChartData });
+ } else {
+ console.warn('Drilldown received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ // Keep current data if no data is available
+ }
+ },
+ (error) => {
+ console.error('Error fetching drilldown data:', error);
+ this.noDataAvailable = true;
+ }
+ );
+ }
+
+ // Reset to original data (go back to base level)
+ resetToOriginalData(): void {
+ console.log('Resetting to original data');
+ console.log('Current stack before reset:', this.drilldownStack);
+ console.log('Current level before reset:', this.currentDrilldownLevel);
+
+ this.currentDrilldownLevel = 0;
+ this.drilldownStack = [];
+
+ if (this.originalPieChartLabels.length > 0) {
+ this.pieChartLabels = [...this.originalPieChartLabels];
+ console.log('Restored original labels');
+ }
+ if (this.originalPieChartData.length > 0) {
+ this.pieChartData = [...this.originalPieChartData];
+ console.log('Restored original data');
+ }
+
+ console.log('After reset - labels:', this.pieChartLabels);
+ console.log('After reset - data:', this.pieChartData);
+
+ // 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();
+ }
+ }
+
+ // 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];
+ }
+
+ // Method to determine if chart should be displayed
+ shouldShowChart(): boolean {
+ // Show chart if we have data
+ if (this.pieChartLabels.length > 0 && this.pieChartData.length > 0) {
+ return true;
+ }
+
+ // Show chart if we're still fetching data
+ if (this.isFetchingData) {
+ return true;
+ }
+
+ // Show chart if we have default data
+ if (this.pieChartLabels.length > 0 && this.originalPieChartLabels.length > 0) {
+ return true;
+ }
+
+ return false;
+ }
// events
- public chartClicked(e: any): void {
- console.log(e);
- }
+ public chartClicked(e: any): void {
+ console.log('Pie 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 label of the clicked element
+ const clickedLabel = this.pieChartLabels[clickedIndex];
+
+ console.log('Clicked on pie slice:', { 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.originalPieChartLabels = [...this.pieChartLabels];
+ this.originalPieChartData = [...this.pieChartData];
+ 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,
+ clickedIndex: clickedIndex,
+ clickedLabel: clickedLabel,
+ clickedValue: clickedLabel // Using label 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(e);
- }
+ public chartHovered(e: any): void {
+ 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..080c7c4 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,292 @@
-
-
-
-
-
+
+
+
+
+
0">
+
Base Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
0 && currentDrilldownLevel > 0">
+
Drilldown Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+
Layer Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+ Clear All Filters
+
+
+
+
+
+
+
+
0">
+
+
+
+
+
+
+
+
+ Drilldown Level: {{currentDrilldownLevel}}
+ 0">
+ (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 e69de29..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
@@ -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/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 56652fe..18f6ee6 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,37 +1,1018 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
+import { FilterService } from '../../common-filter/filter.service';
+import { Subscription } from 'rxjs';
@Component({
selector: 'app-polar-chart',
templateUrl: './polar-chart.component.html',
styleUrls: ['./polar-chart.component.scss']
})
-export class PolarChartComponent implements OnInit {
+export class PolarChartComponent implements OnInit, OnChanges {
+ @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; // Add connection input
+ // Drilldown configuration inputs
+ @Input() drilldownEnabled: boolean = false;
+ @Input() drilldownApiUrl: string;
+ @Input() drilldownXAxis: string;
+ @Input() drilldownYAxis: string;
+ @Input() drilldownParameter: string; // Add drilldown parameter input
+ @Input() baseFilters: any[] = []; // Add base filters input
+ @Input() drilldownFilters: any[] = []; // Add drilldown filters input
+ // Multi-layer drilldown configuration inputs
+ @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
- constructor() { }
+ 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();
}
+
+ 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;
+ 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 && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged || drilldownFiltersChanged ||
+ drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
+ drilldownLayersChanged)) {
+ console.log('Chart configuration changed, fetching new data');
+ this.fetchChartData();
+ }
+ }
+
public polarAreaChartLabels: string[] = [ 'Download Sales', 'In-Store Sales', 'Mail Sales', 'Telesales', 'Corporate Sales' ];
public polarAreaChartData: any = [
{ data: [ 300, 500, 100, 40, 120 ], label: 'Series 1'}
];
+ public polarAreaChartOptions: any = {
+ responsive: true,
+ maintainAspectRatio: false,
+ layout: {
+ padding: {
+ left: 10,
+ right: 10,
+ top: 10,
+ bottom: 30
+ }
+ },
+ plugins: {
+ legend: {
+ display: true,
+ position: 'top'
+ }
+ },
+ scales: {
+ r: {
+ ticks: {
+ backdropColor: 'rgba(0, 0, 0, 0)'
+ }
+ }
+ }
+ };
+
public polarAreaChartType: string = 'polarArea';
+
+ // Multi-layer drilldown state tracking
+ drilldownStack: any[] = []; // Stack to track drilldown navigation history
+ currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
+ originalPolarAreaChartLabels: string[] = [];
+ originalPolarAreaChartData: any = [];
+
+ // No data state
+ noDataAvailable: boolean = false;
+
+ // 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();
+ }
- // public radarChartData: any = [
- // { data: [65, 59, 90, 81, 56, 55, 40], label: "Series A" },
- // { data: [28, 48, 40, 19, 96, 27, 100], label: "Series B" }
- // ];
+ // 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;
+
+ // If we're in drilldown mode, fetch the appropriate drilldown data
+ if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
+ this.fetchDrilldownData();
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ return;
+ }
+
+ // If we have the necessary data, fetch chart data from the service
+ if (this.table && this.xAxis && this.yAxis) {
+ console.log('Fetching polar chart data for:', { 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);
+
+ // 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}` : ''}`;
+ console.log('Polar 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
+ this.dashboardService.getChartData(this.table, 'polar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
+ (data: any) => {
+ console.log('Received polar chart data:', data);
+ if (data === null) {
+ console.warn('Polar chart API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ this.polarAreaChartLabels = [];
+ this.polarAreaChartData = [
+ {
+ data: [],
+ label: 'Dataset 1'
+ }
+ ];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ return;
+ }
+
+ // 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) {
+ // Convert the data to the expected format for polar area charts
+ const chartValues = data.chartData[0].data.map(value => {
+ // Convert to number if it's not already
+ return isNaN(Number(value)) ? 0 : Number(value);
+ });
+ // Assign data in the correct format (array of objects with data property)
+ this.polarAreaChartData = [
+ {
+ data: chartValues,
+ label: data.chartData[0].label || 'Dataset 1'
+ }
+ ];
+ } else {
+ this.polarAreaChartData = [
+ {
+ data: [],
+ label: 'Dataset 1'
+ }
+ ];
+ }
+ console.log('Updated polar chart with data:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
+ } else if (data && data.labels && data.data) {
+ // Handle the original expected format as fallback
+ this.noDataAvailable = data.labels.length === 0;
+ this.polarAreaChartLabels = data.labels;
+ // Convert the data to the expected format for polar area charts
+ const chartValues = data.data.map(value => {
+ // Convert to number if it's not already
+ return isNaN(Number(value)) ? 0 : Number(value);
+ });
+ // Assign data in the correct format (array of objects with data property)
+ this.polarAreaChartData = [
+ {
+ data: chartValues,
+ label: 'Dataset 1'
+ }
+ ];
+ console.log('Updated polar chart with legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
+ } else {
+ console.warn('Polar chart received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ this.polarAreaChartLabels = [];
+ this.polarAreaChartData = [
+ {
+ data: [],
+ label: 'Dataset 1'
+ }
+ ];
+ }
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ },
+ (error) => {
+ console.error('Error fetching polar chart data:', error);
+ this.noDataAvailable = true;
+ this.polarAreaChartLabels = [];
+ this.polarAreaChartData = [
+ {
+ data: [],
+ label: 'Dataset 1'
+ }
+ ];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ // Keep default data in case of error
+ }
+ );
+ } else {
+ console.log('Missing required data for polar chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
+ this.noDataAvailable = true;
+ this.polarAreaChartLabels = [];
+ this.polarAreaChartData = [
+ {
+ data: [],
+ label: 'Dataset 1'
+ }
+ ];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ }
+ }
+
+ // Fetch drilldown data based on current drilldown level
+ fetchDrilldownData(): void {
+ console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
+ console.log('Drilldown stack:', this.drilldownStack);
+
+ // Get the current drilldown configuration based on the current level
+ let drilldownConfig;
+ if (this.currentDrilldownLevel === 1) {
+ // Base drilldown level
+ drilldownConfig = {
+ apiUrl: this.drilldownApiUrl,
+ xAxis: this.drilldownXAxis,
+ yAxis: this.drilldownYAxis,
+ parameter: this.drilldownParameter
+ };
+ } else {
+ // Multi-layer drilldown level
+ const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) {
+ drilldownConfig = this.drilldownLayers[layerIndex];
+ } else {
+ console.warn('Invalid drilldown layer index:', layerIndex);
+ this.noDataAvailable = true;
+ this.polarAreaChartLabels = [];
+ this.polarAreaChartData = [
+ {
+ data: [],
+ label: 'Dataset 1'
+ }
+ ];
+ 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.polarAreaChartLabels = [];
+ this.polarAreaChartData = [
+ {
+ data: [],
+ label: 'Dataset 1'
+ }
+ ];
+ 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)
+ let filterParams = '';
+ if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
+ const filterObj = {};
+ drilldownConfig.filters.forEach((filter: any) => {
+ if (filter.field && filter.value) {
+ filterObj[filter.field] = filter.value;
+ }
+ });
+ if (Object.keys(filterObj).length > 0) {
+ filterParams = JSON.stringify(filterObj);
+ }
+ }
+
+ // Add common filters to drilldown filter parameters
+ const commonFilters = this.filterService.getFilterValues();
+ if (Object.keys(commonFilters).length > 0) {
+ // Merge common filters with drilldown filters
+ const mergedFilterObj = {};
+
+ // Add drilldown filters first
+ if (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);
+
+ // 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, 'polar', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, filterParams).subscribe(
+ (data: any) => {
+ console.log('Received drilldown data:', data);
+ if (data === null) {
+ console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ this.polarAreaChartLabels = [];
+ this.polarAreaChartData = [
+ {
+ data: [],
+ label: 'Dataset 1'
+ }
+ ];
+ return;
+ }
+
+ // 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
+ this.noDataAvailable = data.chartLabels.length === 0;
+ this.polarAreaChartLabels = data.chartLabels;
+ if (data.chartData && data.chartData.length > 0) {
+ // Convert the data to the expected format for polar area charts
+ const chartValues = data.chartData[0].data.map(value => {
+ // Convert to number if it's not already
+ return isNaN(Number(value)) ? 0 : Number(value);
+ });
+ // Assign data in the correct format (array of objects with data property)
+ this.polarAreaChartData = [
+ {
+ data: chartValues,
+ label: data.chartData[0].label || 'Dataset 1'
+ }
+ ];
+ } else {
+ this.polarAreaChartData = [
+ {
+ data: [],
+ label: 'Dataset 1'
+ }
+ ];
+ }
+ // Trigger change detection
+ this.polarAreaChartData = [...this.polarAreaChartData];
+ console.log('Updated polar chart with drilldown data:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
+ } else if (data && data.labels && data.data) {
+ // Handle the original expected format as fallback
+ this.noDataAvailable = data.labels.length === 0;
+ this.polarAreaChartLabels = data.labels;
+ // Convert the data to the expected format for polar area charts
+ const chartValues = data.data.map(value => {
+ // Convert to number if it's not already
+ return isNaN(Number(value)) ? 0 : Number(value);
+ });
+ // Assign data in the correct format (array of objects with data property)
+ this.polarAreaChartData = [
+ {
+ data: chartValues,
+ label: 'Dataset 1'
+ }
+ ];
+ // Trigger change detection
+ this.polarAreaChartData = [...this.polarAreaChartData];
+ console.log('Updated polar chart with drilldown legacy data format:', { labels: this.polarAreaChartLabels, data: this.polarAreaChartData });
+ } else {
+ console.warn('Drilldown received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ this.polarAreaChartLabels = [];
+ this.polarAreaChartData = [
+ {
+ data: [],
+ label: 'Dataset 1'
+ }
+ ];
+ }
+ },
+ (error) => {
+ console.error('Error fetching drilldown data:', error);
+ this.noDataAvailable = true;
+ this.polarAreaChartLabels = [];
+ this.polarAreaChartData = [
+ {
+ data: [],
+ label: 'Dataset 1'
+ }
+ ];
+ // Keep current data in case of error
+ }
+ );
+ }
+
+ // 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.originalPolarAreaChartLabels.length > 0) {
+ this.polarAreaChartLabels = [...this.originalPolarAreaChartLabels];
+ console.log('Restored original labels');
+ }
+ if (this.originalPolarAreaChartData.length > 0) {
+ this.polarAreaChartData = [...this.originalPolarAreaChartData];
+ console.log('Restored original data');
+ }
+
+ console.log('After reset - labels:', this.polarAreaChartLabels);
+ console.log('After reset - data:', this.polarAreaChartData);
+
+ // 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();
+ }
+ }
// events
- public chartClicked(e: any): void {
- console.log(e);
- }
+ public chartClicked(e: any): void {
+ console.log('Polar 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 label of the clicked element
+ const clickedLabel = this.polarAreaChartLabels[clickedIndex];
+
+ 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 = JSON.parse(JSON.stringify(this.polarAreaChartData));
+ 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,
+ clickedIndex: clickedIndex,
+ clickedLabel: clickedLabel,
+ clickedValue: clickedLabel // Using label 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(e);
- }
+ public chartHovered(e: any): void {
+ 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 3b26b0a..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,8 +1,297 @@
-
-
-
+
+
+
+
+
0">
+
Base Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
0 && currentDrilldownLevel > 0">
+
Drilldown Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+
Layer Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+ Clear All Filters
+
+
+
+
+
+
+
+
0">
+
+
+
+
+
+
+
+
+ Drilldown Level: {{currentDrilldownLevel}}
+ 0">
+ (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
+
+
+
+
+
+
+
+
+
+
+ No data available
+
+
+
+
+
+
+
+
\ 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.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/radar-chart/radar-chart.component.scss
index e69de29..a9282e4 100644
--- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/radar-chart/radar-chart.component.scss
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/radar-chart/radar-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/radar-chart/radar-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/radar-chart/radar-chart.component.ts
index 3c23092..a2637a2 100644
--- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/radar-chart/radar-chart.component.ts
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/radar-chart/radar-chart.component.ts
@@ -1,39 +1,923 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
+import { FilterService } from '../../common-filter/filter.service';
+import { Subscription } from 'rxjs';
@Component({
selector: 'app-radar-chart',
templateUrl: './radar-chart.component.html',
styleUrls: ['./radar-chart.component.scss']
})
-export class RadarChartComponent implements OnInit {
-// Radar
-public radarChartLabels: string[] = [
- "Eating",
- "Drinking",
- "Sleeping",
- "Designing",
- "Coding",
- "Cycling",
- "Running"
-];
+export class RadarChartComponent implements OnInit, OnChanges {
+ @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; // 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
-public radarChartData: any = [
- { data: [65, 59, 90, 81, 56, 55, 40], label: "Series A" },
- { data: [28, 48, 40, 19, 96, 27, 100], label: "Series B" }
-];
-public radarChartType: string = "radar";
+ // Radar
+ public radarChartLabels: string[] = [
+ "Eating",
+ "Drinking",
+ "Sleeping",
+ "Designing",
+ "Coding",
+ "Cycling",
+ "Running"
+ ];
-// events
-public chartClicked(e: any): void {
- console.log(e);
-}
+ public radarChartData: any = [
+ { data: [65, 59, 90, 81, 56, 55, 40], label: "Series A" },
+ { data: [28, 48, 40, 19, 96, 27, 100], label: "Series B" }
+ ];
+ public radarChartType: string = "radar";
+
+ // Multi-layer drilldown state tracking
+ drilldownStack: any[] = []; // Stack to track drilldown navigation history
+ currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
+ originalRadarChartLabels: string[] = [];
+ originalRadarChartData: any = [];
+
+ // No data state
+ noDataAvailable: boolean = false;
+
+ // Flag to prevent infinite loops
+ private isFetchingData: boolean = false;
+
+ // Subscriptions to unsubscribe on destroy
+ private subscriptions: Subscription[] = [];
-public chartHovered(e: any): void {
- console.log(e);
-}
- constructor() { }
+ // 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
+ ) { }
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;
+ 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 && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged || drilldownFiltersChanged ||
+ drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
+ drilldownLayersChanged)) {
+ console.log('Chart configuration changed, fetching new data');
+ 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;
+
+ // If we're in drilldown mode, fetch the appropriate drilldown data
+ if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
+ this.fetchDrilldownData();
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ return;
+ }
+
+ // If we have the necessary data, fetch chart data from the service
+ if (this.table && this.xAxis && this.yAxis) {
+ console.log('Fetching radar chart data for:', { 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);
+
+ // 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}` : ''}`;
+ console.log('Radar 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
+ this.dashboardService.getChartData(this.table, 'radar', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
+ (data: any) => {
+ console.log('Received radar chart data:', data);
+ if (data === null) {
+ console.warn('Radar chart API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ this.radarChartLabels = [];
+ this.radarChartData = [];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ return;
+ }
+
+ // Handle the actual data structure returned by the API
+ if (data && data.chartLabels && data.chartData) {
+ // Map the API response to the format expected by the chart
+ 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 => {
+ // Convert to number if it's not already
+ return isNaN(Number(value)) ? 0 : Number(value);
+ }) : []
+ }));
+ // Trigger change detection
+ this.radarChartData = [...this.radarChartData];
+ console.log('Updated radar chart with data:', { labels: this.radarChartLabels, data: this.radarChartData });
+ } else if (data && data.labels && data.datasets) {
+ // Handle the original expected format as fallback
+ this.noDataAvailable = data.labels.length === 0;
+ this.radarChartLabels = data.labels;
+ this.radarChartData = data.datasets.map(dataset => ({
+ ...dataset,
+ data: dataset.data ? dataset.data.map(value => {
+ // Convert to number if it's not already
+ return isNaN(Number(value)) ? 0 : Number(value);
+ }) : []
+ }));
+ // Trigger change detection
+ this.radarChartData = [...this.radarChartData];
+ console.log('Updated radar chart with legacy data format:', { labels: this.radarChartLabels, data: this.radarChartData });
+ } else {
+ console.warn('Radar chart received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ this.radarChartLabels = [];
+ this.radarChartData = [];
+ }
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ },
+ (error) => {
+ console.error('Error fetching radar chart data:', error);
+ this.noDataAvailable = true;
+ this.radarChartLabels = [];
+ this.radarChartData = [];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ }
+ );
+ } else {
+ console.log('Missing required data for radar chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
+ this.noDataAvailable = true;
+ this.radarChartLabels = [];
+ this.radarChartData = [];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ }
+ }
+
+ // Fetch drilldown data based on current drilldown level
+ fetchDrilldownData(): void {
+ console.log('Fetching drilldown data, current level:', this.currentDrilldownLevel);
+ console.log('Drilldown stack:', this.drilldownStack);
+
+ // Get the current drilldown configuration based on the current level
+ let drilldownConfig;
+ if (this.currentDrilldownLevel === 1) {
+ // Base drilldown level
+ drilldownConfig = {
+ apiUrl: this.drilldownApiUrl,
+ xAxis: this.drilldownXAxis,
+ yAxis: this.drilldownYAxis,
+ parameter: this.drilldownParameter
+ };
+ } else {
+ // Multi-layer drilldown level
+ const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ if (layerIndex >= 0 && layerIndex < this.drilldownLayers.length) {
+ drilldownConfig = this.drilldownLayers[layerIndex];
+ } else {
+ console.warn('Invalid drilldown layer index:', layerIndex);
+ this.noDataAvailable = true;
+ this.radarChartLabels = [];
+ this.radarChartData = [];
+ 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.radarChartLabels = [];
+ this.radarChartData = [];
+ 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)
+ let filterParams = '';
+ if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
+ const filterObj = {};
+ drilldownConfig.filters.forEach((filter: any) => {
+ if (filter.field && filter.value) {
+ filterObj[filter.field] = filter.value;
+ }
+ });
+ if (Object.keys(filterObj).length > 0) {
+ filterParams = JSON.stringify(filterObj);
+ }
+ }
+
+ // Add common filters to drilldown filter parameters
+ const commonFilters = this.filterService.getFilterValues();
+ if (Object.keys(commonFilters).length > 0) {
+ // Merge common filters with drilldown filters
+ const mergedFilterObj = {};
+
+ // Add drilldown filters first
+ if (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);
+
+ // 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, 'radar', drilldownConfig.xAxis, drilldownConfig.yAxis, this.connection, parameterField, parameterValue, filterParams).subscribe(
+ (data: any) => {
+ console.log('Received drilldown data:', data);
+ if (data === null) {
+ console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ this.radarChartLabels = [];
+ this.radarChartData = [];
+ return;
+ }
+
+ // Handle the actual data structure returned by the API
+ if (data && data.chartLabels && data.chartData) {
+ // Map the API response to the format expected by the chart
+ this.noDataAvailable = data.chartLabels.length === 0;
+ this.radarChartLabels = data.chartLabels;
+ // For radar charts, we need to ensure the data is properly formatted
+ this.radarChartData = data.chartData.map(dataset => ({
+ ...dataset,
+ data: dataset.data ? dataset.data.map(value => {
+ // Convert to number if it's not already
+ return isNaN(Number(value)) ? 0 : Number(value);
+ }) : []
+ }));
+ // Trigger change detection
+ this.radarChartData = [...this.radarChartData];
+ console.log('Updated radar chart with drilldown data:', { labels: this.radarChartLabels, data: this.radarChartData });
+ } else if (data && data.labels && data.datasets) {
+ // Handle the original expected format as fallback
+ this.noDataAvailable = data.labels.length === 0;
+ this.radarChartLabels = data.labels;
+ this.radarChartData = data.datasets.map(dataset => ({
+ ...dataset,
+ data: dataset.data ? dataset.data.map(value => {
+ // Convert to number if it's not already
+ return isNaN(Number(value)) ? 0 : Number(value);
+ }) : []
+ }));
+ // Trigger change detection
+ this.radarChartData = [...this.radarChartData];
+ console.log('Updated radar chart with drilldown legacy data format:', { labels: this.radarChartLabels, data: this.radarChartData });
+ } else {
+ console.warn('Drilldown received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ this.radarChartLabels = [];
+ this.radarChartData = [];
+ }
+ },
+ (error) => {
+ console.error('Error fetching drilldown data:', error);
+ this.noDataAvailable = true;
+ this.radarChartLabels = [];
+ this.radarChartData = [];
+ // Keep current data in case of error
+ }
+ );
+ }
+
+ // 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.originalRadarChartLabels.length > 0) {
+ this.radarChartLabels = [...this.originalRadarChartLabels];
+ console.log('Restored original labels');
+ }
+ if (this.originalRadarChartData.length > 0) {
+ this.radarChartData = [...this.originalRadarChartData];
+ console.log('Restored original data');
+ }
+
+ console.log('After reset - labels:', this.radarChartLabels);
+ console.log('After reset - data:', this.radarChartData);
+
+ // 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();
+ }
+ }
+
+ // events
+ public chartClicked(e: any): void {
+ console.log('Radar 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 label of the clicked element
+ const clickedLabel = this.radarChartLabels[clickedIndex];
+
+ console.log('Clicked on radar 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.originalRadarChartLabels = [...this.radarChartLabels];
+ this.originalRadarChartData = JSON.parse(JSON.stringify(this.radarChartData));
+ 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,
+ clickedIndex: clickedIndex,
+ clickedLabel: clickedLabel,
+ clickedValue: clickedLabel // Using label 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('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 e0b67e3..1da95d9 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,8 +1,297 @@
-
-
-
-
+
+
+
+
+
0">
+
Base Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
0 && currentDrilldownLevel > 0">
+
Drilldown Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+
Layer Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+ Clear All Filters
+
+
+
+
+
+
+
+
0">
+
+
+
+
+
+
+
+
+ Drilldown Level: {{currentDrilldownLevel}}
+ 0">
+ (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
+
+
+
+
+
+
+
+
+
+
+ No data available
+
+
+
+
+
+
+
+
\ 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.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/scatter-chart/scatter-chart.component.scss
index e69de29..a9282e4 100644
--- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/scatter-chart/scatter-chart.component.scss
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/scatter-chart/scatter-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/scatter-chart/scatter-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/scatter-chart/scatter-chart.component.ts
index 1af87bc..97bd14a 100644
--- a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/scatter-chart/scatter-chart.component.ts
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/scatter-chart/scatter-chart.component.ts
@@ -1,44 +1,94 @@
-import { Component, OnInit } from '@angular/core';
-import { ChartData,ChartDataset } from 'chart.js';
+import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { ChartData,ChartDataset,ChartOptions } from 'chart.js';
+import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
+import { FilterService } from '../../common-filter/filter.service';
+import { Subscription } from 'rxjs';
@Component({
selector: 'app-scatter-chart',
templateUrl: './scatter-chart.component.html',
styleUrls: ['./scatter-chart.component.scss']
})
-export class ScatterChartComponent implements OnInit {
+export class ScatterChartComponent implements OnInit, OnChanges {
+ @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; // Add connection input
+ // Drilldown configuration inputs
+ @Input() drilldownEnabled: boolean = false;
+ @Input() drilldownApiUrl: string;
+ @Input() drilldownXAxis: string;
+ @Input() drilldownYAxis: string;
+ @Input() drilldownParameter: string; // Add drilldown parameter input
+ @Input() baseFilters: any[] = []; // Add base filters input
+ @Input() drilldownFilters: any[] = []; // Add drilldown filters input
+ // Multi-layer drilldown configuration inputs
+ @Input() drilldownLayers: any[] = []; // Array of drilldown layer configurations
- constructor() { }
+ 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();
}
+
+ 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;
+ 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 && (xAxisChanged || yAxisChanged || tableChanged || connectionChanged || baseFiltersChanged || drilldownFiltersChanged ||
+ drilldownEnabledChanged || drilldownApiUrlChanged || drilldownXAxisChanged || drilldownYAxisChanged ||
+ drilldownLayersChanged)) {
+ console.log('Chart configuration changed, fetching new data');
+ this.fetchChartData();
+ }
+ }
+
public scatterChartLabels: string[] = [ 'Eating', 'Drinking', 'Sleeping', 'Designing', 'Coding', 'Cycling', 'Running' ];
public scatterChartData: ChartDataset[] = [
- // {
- // data: [
- // { x: 1, y: 1 },
- // { x: 2, y: 3 },
- // { x: 3, y: -2 },
- // { x: 4, y: 4 },
- // { x: 5, y: -3, r: 20 },
- // ],
- // label: 'Series A',
- // pointRadius: 10,
- // backgroundColor: 'red',
- // },
- // {
- // data: [
- // { x: 2, y: 2 },
- // { x: 3, y: 1 },
- // { x: 4, y: 3 },
- // { x: 5, y: 2 },
- // { x: 6, y: 4, r: 15 },
- // ],
- // label: 'Series B',
- // pointRadius: 8,
- // backgroundColor: 'green',
- // },
{
data: [
{ x: 1, y: 1 },
@@ -63,15 +113,974 @@ export class ScatterChartComponent implements OnInit {
],
},
];
+
+ public scatterChartOptions: any = {
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ x: {
+ type: 'linear',
+ position: 'bottom',
+ title: {
+ display: true,
+ text: 'X Axis'
+ },
+ ticks: {
+ autoSkip: true,
+ maxTicksLimit: 10,
+ callback: function(value: any) {
+ if (typeof value === 'number') {
+ // Format large numbers for better readability
+ if (Math.abs(value) >= 1000000) {
+ return (value / 1000000).toFixed(1) + 'M';
+ } else if (Math.abs(value) >= 1000) {
+ return (value / 1000).toFixed(1) + 'K';
+ }
+ return value.toString();
+ }
+ return value;
+ }
+ },
+ grid: {
+ display: true,
+ color: 'rgba(0, 0, 0, 0.1)'
+ }
+ },
+ y: {
+ title: {
+ display: true,
+ text: 'Y Axis'
+ },
+ ticks: {
+ autoSkip: true,
+ maxTicksLimit: 10,
+ callback: function(value: any) {
+ if (typeof value === 'number') {
+ // Format large numbers for better readability
+ if (Math.abs(value) >= 1000000) {
+ return (value / 1000000).toFixed(1) + 'M';
+ } else if (Math.abs(value) >= 1000) {
+ return (value / 1000).toFixed(1) + 'K';
+ }
+ return value.toString();
+ }
+ return value;
+ }
+ },
+ grid: {
+ display: true,
+ color: 'rgba(0, 0, 0, 0.1)'
+ }
+ }
+ },
+ plugins: {
+ legend: {
+ display: true,
+ position: 'top',
+ },
+ tooltip: {
+ callbacks: {
+ label: function(context: any) {
+ return `(${context.parsed.x}, ${context.parsed.y})`;
+ }
+ }
+ }
+ },
+ layout: {
+ padding: {
+ left: 15,
+ right: 15,
+ top: 15,
+ bottom: 60 // Add padding at the bottom to ensure X-axis visibility
+ }
+ }
+ };
+
public scatterChartType: string = 'scatter';
+
+ // Multi-layer drilldown state tracking
+ drilldownStack: any[] = []; // Stack to track drilldown navigation history
+ currentDrilldownLevel: number = 0; // Current drilldown level (0 = base level)
+ originalScatterChartData: ChartDataset[] = [];
+
+ // No data state
+ noDataAvailable: boolean = false;
+
+ // 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
+ });
+ }
- // events
- public chartClicked(e: any): void {
- console.log(e);
- }
+ // Check if there are active filters
+ hasActiveFilters(): boolean {
+ return (this.baseFilters && this.baseFilters.length > 0) ||
+ (this.drilldownFilters && this.drilldownFilters.length > 0) ||
+ this.hasActiveLayerFilters();
+ }
- public chartHovered(e: any): void {
- console.log(e);
- }
+ // Check if there are active layer filters for current drilldown level
+ hasActiveLayerFilters(): boolean {
+ if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
+ const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ return layerIndex < this.drilldownLayers.length &&
+ this.drilldownLayers[layerIndex].filters &&
+ this.drilldownLayers[layerIndex].filters.length > 0;
+ }
+ return false;
+ }
+
+ // Get active layer filters for current drilldown level
+ getActiveLayerFilters(): any[] {
+ if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
+ const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ if (layerIndex < this.drilldownLayers.length &&
+ this.drilldownLayers[layerIndex].filters) {
+ return this.drilldownLayers[layerIndex].filters;
+ }
+ }
+ return [];
+ }
+
+ // Get filter options for dropdown/multiselect filters
+ getFilterOptions(filter: any): string[] {
+ if (filter.options) {
+ if (Array.isArray(filter.options)) {
+ return filter.options;
+ } else if (typeof filter.options === 'string') {
+ return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
+ }
+ }
+ return [];
+ }
+
+ // Check if an option is selected for multiselect filters
+ isOptionSelected(filter: any, option: string): boolean {
+ if (!filter.value) {
+ return false;
+ }
+
+ if (Array.isArray(filter.value)) {
+ return filter.value.includes(option);
+ }
+
+ return filter.value === option;
+ }
+
+ // Handle base filter changes
+ onBaseFilterChange(filter: any): void {
+ console.log('Base filter changed:', filter);
+ // Refresh data when filter changes
+ this.fetchChartData();
+ }
+
+ // Handle drilldown filter changes
+ onDrilldownFilterChange(filter: any): void {
+ console.log('Drilldown filter changed:', filter);
+ // Refresh data when filter changes
+ this.fetchChartData();
+ }
+
+ // Handle layer filter changes
+ onLayerFilterChange(filter: any): void {
+ console.log('Layer filter changed:', filter);
+ // Refresh data when filter changes
+ this.fetchChartData();
+ }
+
+ // Handle multiselect changes
+ onMultiSelectChange(filter: any, option: string, event: any): void {
+ const checked = event.target.checked;
+
+ // Initialize filter.value as array if it's not already
+ if (!Array.isArray(filter.value)) {
+ filter.value = [];
+ }
+
+ if (checked) {
+ // Add option to array if not already present
+ if (!filter.value.includes(option)) {
+ filter.value.push(option);
+ }
+ } else {
+ // Remove option from array
+ filter.value = filter.value.filter((item: string) => item !== option);
+ }
+
+ // Refresh data when filter changes
+ this.fetchChartData();
+ }
+
+ // Handle date range changes
+ onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
+ filter.value = dateRange;
+ // Refresh data when filter changes
+ this.fetchChartData();
+ }
+
+ // Handle toggle changes
+ onToggleChange(filter: any, checked: boolean): void {
+ filter.value = checked;
+ // Refresh data when filter changes
+ this.fetchChartData();
+ }
+
+ // Toggle multiselect dropdown visibility
+ toggleMultiselect(filter: any, context: string): void {
+ const filterId = `${context}-${filter.field}`;
+ if (this.isMultiselectOpen(filter, context)) {
+ this.openMultiselects.delete(filterId);
+ } else {
+ // Close all other multiselects first
+ this.openMultiselects.clear();
+ this.openMultiselects.set(filterId, context);
+
+ // Add document click handler to close dropdown when clicking outside
+ this.addDocumentClickHandler();
+ }
+ }
+
+ // Add document click handler to close dropdowns when clicking outside
+ private addDocumentClickHandler(): void {
+ if (!this.documentClickHandler) {
+ this.documentClickHandler = (event: MouseEvent) => {
+ const target = event.target as HTMLElement;
+ // Check if click is outside any multiselect dropdown
+ if (!target.closest('.multiselect-container')) {
+ this.openMultiselects.clear();
+ this.removeDocumentClickHandler();
+ }
+ };
+
+ // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
+ setTimeout(() => {
+ document.addEventListener('click', this.documentClickHandler!);
+ }, 0);
+ }
+ }
+
+ // Remove document click handler
+ private removeDocumentClickHandler(): void {
+ if (this.documentClickHandler) {
+ document.removeEventListener('click', this.documentClickHandler);
+ this.documentClickHandler = null;
+ }
+ }
+
+ // Check if multiselect dropdown is open
+ isMultiselectOpen(filter: any, context: string): boolean {
+ const filterId = `${context}-${filter.field}`;
+ return this.openMultiselects.has(filterId);
+ }
+
+ // Get count of selected options for a multiselect filter
+ getSelectedOptionsCount(filter: any): number {
+ if (!filter.value) {
+ return 0;
+ }
+
+ if (Array.isArray(filter.value)) {
+ return filter.value.length;
+ }
+
+ return 0;
+ }
+
+ // Clear all filters
+ clearAllFilters(): void {
+ // Clear base filters
+ if (this.baseFilters) {
+ this.baseFilters.forEach(filter => {
+ if (filter.type === 'multiselect') {
+ filter.value = [];
+ } else if (filter.type === 'date-range') {
+ filter.value = { start: null, end: null };
+ } else if (filter.type === 'toggle') {
+ filter.value = false;
+ } else {
+ filter.value = '';
+ }
+ });
+ }
+
+ // Clear drilldown filters
+ if (this.drilldownFilters) {
+ this.drilldownFilters.forEach(filter => {
+ if (filter.type === 'multiselect') {
+ filter.value = [];
+ } else if (filter.type === 'date-range') {
+ filter.value = { start: null, end: null };
+ } else if (filter.type === 'toggle') {
+ filter.value = false;
+ } else {
+ filter.value = '';
+ }
+ });
+ }
+
+ // Clear layer filters
+ if (this.drilldownLayers) {
+ this.drilldownLayers.forEach(layer => {
+ if (layer.filters) {
+ layer.filters.forEach((filter: any) => {
+ if (filter.type === 'multiselect') {
+ filter.value = [];
+ } else if (filter.type === 'date-range') {
+ filter.value = { start: null, end: null };
+ } else if (filter.type === 'toggle') {
+ filter.value = false;
+ } else {
+ filter.value = '';
+ }
+ });
+ }
+ });
+ }
+
+ // Close all multiselect dropdowns
+ this.openMultiselects.clear();
+
+ // Refresh data
+ this.fetchChartData();
+ }
+
+ // Transform data to scatter chart format
+ private transformToScatterData(labels: any[], data: any[]): ChartDataset[] {
+ // For scatter charts, we need to transform the data into scatter format
+ // Scatter charts expect data in the format: {x: number, y: number}
+ console.log('Transforming data to scatter format:', { labels, data });
+
+ // If we have the expected scatter data format, return it as is
+ if (data && data.length > 0 && data[0].data && data[0].data.length > 0 &&
+ typeof data[0].data[0] === 'object' && data[0].data[0].hasOwnProperty('x') &&
+ data[0].data[0].hasOwnProperty('y')) {
+ return data;
+ }
+
+ // Transform the data properly for scatter chart
+ // Assuming labels are x-values and data[0].data are y-values
+ if (labels && data && data.length > 0 && data[0].data) {
+ const yValues = data[0].data;
+ const label = data[0].label || 'Dataset 1';
+
+ // Create scatter points from labels (x) and data (y)
+ const scatterPoints = [];
+ const minLength = Math.min(labels.length, yValues.length);
+
+ for (let i = 0; i < minLength; i++) {
+ // Convert to numbers if they're strings
+ const x = typeof labels[i] === 'string' ? parseFloat(labels[i]) : labels[i];
+ const y = typeof yValues[i] === 'string' ? parseFloat(yValues[i]) : yValues[i];
+
+ // Only add valid points
+ if (!isNaN(x) && !isNaN(y)) {
+ scatterPoints.push({ x, y });
+ }
+ }
+
+ // Generate different colors for each point to avoid all points showing the same color
+ const backgroundColors = [];
+ const borderColors = [];
+
+ for (let i = 0; i < scatterPoints.length; i++) {
+ // Generate a color based on the point index for variety
+ const hue = (i * 137.508) % 360; // Use golden angle to spread colors
+ backgroundColors.push(`hsla(${hue}, 70%, 50%, 0.6)`);
+ borderColors.push(`hsla(${hue}, 70%, 40%, 1)`);
+ }
+
+ // Create a single dataset with all scatter points
+ const scatterDatasets: ChartDataset[] = [
+ {
+ data: scatterPoints,
+ label: label,
+ pointRadius: 8,
+ pointHoverRadius: 10,
+ backgroundColor: backgroundColors,
+ borderColor: borderColors,
+ borderWidth: 1,
+ pointHoverBackgroundColor: 'rgba(255, 99, 132, 1)',
+ }
+ ];
+
+ console.log('Transformed scatter data:', scatterDatasets);
+ return scatterDatasets;
+ }
+
+ // Otherwise, create a default scatter dataset
+ const scatterDatasets: ChartDataset[] = [
+ {
+ data: [
+ { x: 1, y: 1 },
+ { x: 2, y: 3 },
+ { x: 3, y: -2 },
+ { x: 4, y: 4 },
+ { x: 5, y: -3 },
+ ],
+ label: 'Dataset 1',
+ pointRadius: 10,
+ backgroundColor: [
+ 'red',
+ 'green',
+ 'blue',
+ 'purple',
+ 'yellow',
+ 'brown',
+ 'magenta',
+ 'cyan',
+ 'orange',
+ 'pink'
+ ],
+ }
+ ];
+
+ return scatterDatasets;
+ }
+
+ fetchChartData(): void {
+ // Set flag to prevent recursive calls
+ this.isFetchingData = true;
+
+ // If we're in drilldown mode, fetch the appropriate drilldown data
+ if (this.currentDrilldownLevel > 0 && this.drilldownStack.length > 0) {
+ this.fetchDrilldownData();
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ return;
+ }
+
+ // If we have the necessary data, fetch chart data from the service
+ if (this.table && this.xAxis && this.yAxis) {
+ console.log('Fetching scatter chart data for:', { 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);
+
+ // 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}` : ''}`;
+ console.log('Scatter 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
+ this.dashboardService.getChartData(this.table, 'scatter', this.xAxis, yAxisString, this.connection, '', '', filterParams).subscribe(
+ (data: any) => {
+ console.log('Received scatter chart data:', data);
+ if (data === null) {
+ console.warn('Scatter chart API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ this.scatterChartData = [];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ return;
+ }
+
+ // 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);
+
+ // Update chart options with axis titles
+ this.updateChartOptionsWithAxisTitles();
+
+ console.log('Updated scatter chart with data:', this.scatterChartData);
+ } else if (data && data.labels && data.datasets) {
+ // Handle the original expected format as fallback
+ this.noDataAvailable = data.labels.length === 0;
+ this.scatterChartData = data.datasets;
+
+ // Update chart options with axis titles
+ this.updateChartOptionsWithAxisTitles();
+
+ console.log('Updated scatter chart with legacy data format:', this.scatterChartData);
+ } else {
+ console.warn('Scatter chart received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ this.scatterChartData = [];
+ }
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ },
+ (error) => {
+ console.error('Error fetching scatter chart data:', error);
+ this.noDataAvailable = true;
+ this.scatterChartData = [];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ // Keep default data in case of error
+ }
+ );
+ } else {
+ console.log('Missing required data for scatter chart:', { table: this.table, xAxis: this.xAxis, yAxis: this.yAxis, connection: this.connection });
+ this.noDataAvailable = true;
+ this.scatterChartData = [];
+ // Reset flag after fetching
+ this.isFetchingData = false;
+ }
+ }
+
+ // Update chart options with axis titles
+ private updateChartOptionsWithAxisTitles(): void {
+ // Update X axis title
+ if (this.scatterChartOptions.scales && this.scatterChartOptions.scales.x) {
+ this.scatterChartOptions.scales.x.title.text = this.xAxis || 'X Axis';
+ }
+
+ // Update Y axis title
+ if (this.scatterChartOptions.scales && this.scatterChartOptions.scales.y) {
+ const yAxisLabel = Array.isArray(this.yAxis) ? this.yAxis[0] : this.yAxis;
+ this.scatterChartOptions.scales.y.title.text = yAxisLabel || 'Y Axis';
+ }
+ }
+
+ // Fetch drilldown data based on current drilldown level
+ 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.scatterChartData = [];
+ 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.scatterChartData = [];
+ 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)
+ let filterParams = '';
+ if (drilldownConfig.filters && drilldownConfig.filters.length > 0) {
+ const filterObj = {};
+ drilldownConfig.filters.forEach((filter: any) => {
+ if (filter.field && filter.value) {
+ filterObj[filter.field] = filter.value;
+ }
+ });
+ if (Object.keys(filterObj).length > 0) {
+ filterParams = JSON.stringify(filterObj);
+ }
+ }
+
+ // Add common filters to drilldown filter parameters
+ const commonFilters = this.filterService.getFilterValues();
+ if (Object.keys(commonFilters).length > 0) {
+ // Merge common filters with drilldown filters
+ const mergedFilterObj = {};
+
+ // Add drilldown filters first
+ if (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/scatter?tableName=${actualApiUrl}&xAxis=${drilldownConfig.xAxis}&yAxes=${drilldownConfig.yAxis}${this.connection ? `&sureId=${this.connection}` : ''}`;
+ console.log('Drilldown data URL:', url);
+
+ // 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, filterParams).subscribe(
+ (data: any) => {
+ console.log('Received drilldown data:', data);
+ if (data === null) {
+ console.warn('Drilldown API returned null data. Check if the API endpoint is working correctly.');
+ this.noDataAvailable = true;
+ this.scatterChartData = [];
+ return;
+ }
+
+ // 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
+ this.noDataAvailable = data.chartLabels.length === 0;
+ this.scatterChartData = this.transformToScatterData(data.chartLabels, data.chartData);
+ console.log('Updated scatter chart with drilldown data:', this.scatterChartData);
+ } else if (data && data.labels && data.datasets) {
+ // Handle the original expected format as fallback
+ this.noDataAvailable = data.labels.length === 0;
+ this.scatterChartData = data.datasets;
+ console.log('Updated scatter chart with drilldown legacy data format:', this.scatterChartData);
+ } else {
+ console.warn('Drilldown received data does not have expected structure', data);
+ this.noDataAvailable = true;
+ this.scatterChartData = [];
+ }
+ },
+ (error) => {
+ console.error('Error fetching drilldown data:', error);
+ this.noDataAvailable = true;
+ this.scatterChartData = [];
+ // Keep current data in case of error
+ }
+ );
+ }
+
+ // 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.originalScatterChartData.length > 0) {
+ this.scatterChartData = [...this.originalScatterChartData];
+ console.log('Restored original data');
+ }
+
+ console.log('After reset - data:', this.scatterChartData);
+
+ // 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();
+ }
+ }
+
+ // events
+ public chartClicked(e: any): void {
+ console.log('Scatter 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
+ 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 = JSON.parse(JSON.stringify(this.scatterChartData));
+ 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('Scatter chart hovered:', e);
+ }
+
+ ngOnDestroy(): void {
+ this.subscriptions.forEach(sub => sub.unsubscribe());
+ // Clean up document click handler
+ this.removeDocumentClickHandler();
+ }
}
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/README.md b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/README.md
new file mode 100644
index 0000000..beb38a0
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/README.md
@@ -0,0 +1,125 @@
+# Shield Dashboard
+
+A professional analytics dashboard UI similar to the "Shield Overall Dashboard" design.
+
+## Components
+
+### Layout
+- **Left Sidebar**: Contains application name, KPI metrics, and filter controls
+- **Main Dashboard Area**: Grid-based responsive layout with various data visualization components
+
+### Components
+
+1. **Sidebar Filters**
+ - Application header with title
+ - KPI metrics display (Total Leads, Total Deals)
+ - Filter controls (Sales Rep, Partner, Traction Channel, Sub Product Line)
+ - Reset filters button
+
+2. **Bar Chart**
+ - Title: "Deal Stage Wise Progress"
+ - Visualizes deal progress across different stages
+ - Responsive design with loading shimmer effect
+
+3. **Donut Charts**
+ - End Customer Stage Split
+ - Segment Penetration
+ - Interactive with tooltips
+
+4. **Map Chart**
+ - Dealer locations with colored markers
+ - Interactive hover tooltips showing dealer status
+ - Legend for status colors
+
+5. **Data Table**
+ - Cross/Up Selling Scope
+ - Scrollable with sticky header
+ - Alternating row colors
+ - Sortable columns
+ - Probability bars with color coding
+
+6. **Deal Details Card**
+ - Company Name, Stage, Description, Amount, Stage Days
+ - Color-coded deal stages
+ - Responsive grid layout
+
+7. **Quarterwise Flow**
+ - Placeholder for chart configuration
+ - "Chart configuration incomplete" message
+
+## Features
+
+### Responsiveness
+- Grid-based layout adapts to different screen sizes
+- Mobile-friendly design with stacked layout on small screens
+- Flexible components that resize appropriately
+
+### Filter Functionality
+- Global filters that affect all dashboard components
+- Shared state management using BehaviorSubject
+- Real-time updates when filters change
+- Reset filters functionality
+
+### Visual Design
+- Dark Navy Blue for headers and bars
+- Bright Orange for highlights and KPI boxes
+- White background for charts and tables
+- Modern color scheme with consistent styling
+
+### Interactive Elements
+- Chart tooltips on hover
+- Interactive map with dealer status information
+- Sortable data table
+- Loading shimmer effects during data updates
+
+## Technical Implementation
+
+### Technologies Used
+- Angular 16
+- Chart.js with ng2-charts
+- Clarity Design System
+- SCSS for styling
+
+### State Management
+- Shared service using BehaviorSubject for filter state
+- Reactive components that update based on filter changes
+- Simulated data updates (no backend integration)
+
+### Responsive Design
+- CSS Grid and Flexbox layouts
+- Media queries for different screen sizes
+- Relative units for scalable components
+
+## Component Structure
+
+```
+shield-dashboard/
+├── shield-dashboard.component.ts|.html|.scss
+├── services/
+│ └── dashboard-filter.service.ts
+├── components/
+│ ├── sidebar-filters/
+│ │ ├── sidebar-filters.component.ts|.html|.scss
+│ ├── bar-chart/
+│ │ ├── bar-chart.component.ts|.html|.scss
+│ ├── donut-chart/
+│ │ ├── donut-chart.component.ts|.html|.scss
+│ ├── map-chart/
+│ │ ├── map-chart.component.ts|.html|.scss
+│ ├── data-table/
+│ │ ├── data-table.component.ts|.html|.scss
+│ ├── deal-details-card/
+│ │ ├── deal-details-card.component.ts|.html|.scss
+│ ├── quarterwise-flow/
+│ │ ├── quarterwise-flow.component.ts|.html|.scss
+│ └── loading-shimmer/
+│ ├── loading-shimmer.component.ts|.html|.scss
+└── shield-dashboard-routing.module.ts
+```
+
+## Usage
+
+To navigate to the Shield Dashboard, visit:
+`/cns-portal/shield-dashboard`
+
+The dashboard is fully responsive and will adapt to different screen sizes. All components are interconnected through the shared filter service, so changing any filter will update all visualizations in real-time.
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/bar-chart/bar-chart.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/bar-chart/bar-chart.component.html
new file mode 100644
index 0000000..39f22df
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/bar-chart/bar-chart.component.html
@@ -0,0 +1,20 @@
+
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/bar-chart/bar-chart.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/bar-chart/bar-chart.component.scss
new file mode 100644
index 0000000..0f2b47c
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/bar-chart/bar-chart.component.scss
@@ -0,0 +1,70 @@
+.chart-container {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+
+ .chart-header {
+ margin-bottom: 20px;
+
+ h3 {
+ font-size: 18px;
+ font-weight: 600;
+ color: #0a192f;
+ margin: 0;
+ }
+ }
+
+ .chart-wrapper {
+ flex: 1;
+ position: relative;
+
+ .chart-content {
+ position: relative;
+ height: 100%;
+
+ &.loading {
+ opacity: 0.7;
+
+ canvas {
+ filter: blur(2px);
+ }
+ }
+
+ canvas {
+ max-width: 100%;
+ max-height: 100%;
+ transition: filter 0.3s ease;
+ }
+
+ .loading-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255, 255, 255, 0.8);
+
+ .shimmer-bar {
+ width: 80%;
+ height: 20px;
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+ border-radius: 4px;
+ }
+ }
+ }
+ }
+}
+
+@keyframes shimmer {
+ 0% {
+ background-position: -200% 0;
+ }
+ 100% {
+ background-position: 200% 0;
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/bar-chart/bar-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/bar-chart/bar-chart.component.ts
new file mode 100644
index 0000000..8ed0664
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/bar-chart/bar-chart.component.ts
@@ -0,0 +1,140 @@
+import { Component, OnInit, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
+import { BaseChartDirective } from 'ng2-charts';
+import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js';
+import { DashboardFilterService } from '../../services/dashboard-filter.service';
+import { Subscription } from 'rxjs';
+
+@Component({
+ selector: 'app-shield-bar-chart',
+ templateUrl: './bar-chart.component.html',
+ styleUrls: ['./bar-chart.component.scss']
+})
+export class BarChartComponent implements OnInit, AfterViewInit, OnDestroy {
+ @ViewChild(BaseChartDirective) chart?: BaseChartDirective;
+
+ private filterSubscription: Subscription = new Subscription();
+
+ // Loading state
+ isLoading: boolean = false;
+
+ public barChartOptions: ChartConfiguration['options'] = {
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ x: {
+ grid: {
+ color: 'rgba(0, 0, 0, 0.05)'
+ },
+ ticks: {
+ color: '#64748b'
+ }
+ },
+ y: {
+ beginAtZero: true,
+ grid: {
+ color: 'rgba(0, 0, 0, 0.05)'
+ },
+ ticks: {
+ color: '#64748b'
+ }
+ }
+ },
+ plugins: {
+ legend: {
+ display: false
+ },
+ tooltip: {
+ backgroundColor: 'rgba(10, 25, 47, 0.9)',
+ titleColor: '#ff6b35',
+ bodyColor: '#ffffff',
+ borderColor: 'rgba(255, 107, 53, 0.3)',
+ borderWidth: 1,
+ cornerRadius: 6
+ }
+ }
+ };
+
+ public barChartType: ChartType = 'bar';
+ public barChartPlugins = [];
+
+ public barChartData: ChartData<'bar'> = {
+ labels: ['Prospecting', 'Qualification', 'Needs Analysis', 'Value Proposition', 'Decision Making', 'Negotiation'],
+ datasets: [
+ {
+ data: [65, 59, 80, 81, 56, 55],
+ label: 'Deal Progress',
+ backgroundColor: '#0a192f',
+ borderColor: '#0a192f',
+ borderWidth: 1,
+ borderRadius: 4
+ }
+ ]
+ };
+
+ constructor(private filterService: DashboardFilterService) { }
+
+ ngOnInit(): void {
+ // Subscribe to filter changes
+ this.filterSubscription.add(
+ this.filterService.filterState$.subscribe(filters => {
+ this.updateChartData(filters);
+ })
+ );
+ }
+
+ ngAfterViewInit(): void {
+ // Initial chart render
+ this.updateChart();
+ }
+
+ ngOnDestroy(): void {
+ // Unsubscribe from all subscriptions
+ this.filterSubscription.unsubscribe();
+ }
+
+ // Update chart data based on filters
+ updateChartData(filters: any): void {
+ // Show loading state
+ this.isLoading = true;
+
+ // Simulate data change based on filters
+ // In a real implementation, this would fetch new data from an API
+ const baseData = [65, 59, 80, 81, 56, 55];
+
+ // Apply filter effects (simplified logic)
+ let multiplier = 1;
+ if (filters.salesRep) multiplier *= 0.9;
+ if (filters.partner) multiplier *= 0.85;
+ if (filters.tractionChannel) multiplier *= 0.95;
+ if (filters.subProductLine) multiplier *= 0.8;
+
+ // Add a small delay to simulate loading
+ setTimeout(() => {
+ this.barChartData.datasets[0].data = baseData.map(value =>
+ Math.floor(value * multiplier)
+ );
+
+ // Update chart
+ this.updateChart();
+
+ // Hide loading state
+ this.isLoading = false;
+ }, 300);
+ }
+
+ // Update chart with new data
+ updateChart(): void {
+ if (this.chart) {
+ this.chart.update();
+ }
+ }
+
+ // events
+ public chartClicked({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
+ console.log(event, active);
+ }
+
+ public chartHovered({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
+ console.log(event, active);
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/data-table/data-table.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/data-table/data-table.component.html
new file mode 100644
index 0000000..b637376
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/data-table/data-table.component.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+ Company Name
+
+
+
+
+
+
+ Contact Person
+
+
+
+
+
+
+ Product
+
+
+
+
+
+
+ Potential Value
+
+
+
+
+
+
+ Probability
+
+
+
+
+
+
+ Next Action
+
+
+
+
+
+
+ Action Date
+
+
+
+
+
+
+
+
+
+ {{ item.companyName }}
+ {{ item.contactPerson }}
+ {{ item.product }}
+ {{ formatCurrency(item.potentialValue) }}
+
+
+
70 ? '#0a192f' : item.probability > 50 ? '#ff6b35' : '#64748b'">
+
{{ formatProbability(item.probability) }}
+
+
+ {{ item.nextAction }}
+ {{ item.actionDate }}
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/data-table/data-table.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/data-table/data-table.component.scss
new file mode 100644
index 0000000..59a1454
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/data-table/data-table.component.scss
@@ -0,0 +1,107 @@
+.table-container {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+
+ .table-header {
+ margin-bottom: 20px;
+
+ h3 {
+ font-size: 18px;
+ font-weight: 600;
+ color: #0a192f;
+ margin: 0;
+ }
+ }
+
+ .table-wrapper {
+ flex: 1;
+ overflow: auto;
+
+ .data-table {
+ width: 100%;
+ border-collapse: collapse;
+ background: white;
+
+ thead {
+ background: #f1f5f9;
+ position: sticky;
+ top: 0;
+ z-index: 5;
+
+ th {
+ padding: 12px 15px;
+ text-align: left;
+ font-weight: 600;
+ color: #0a192f;
+ border-bottom: 2px solid #e2e8f0;
+ cursor: pointer;
+ user-select: none;
+ position: relative;
+
+ &:hover {
+ background: #e2e8f0;
+ }
+
+ &.sortable {
+ padding-right: 30px;
+ }
+
+ .sort-indicator {
+ position: absolute;
+ right: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: #ff6b35;
+ }
+ }
+ }
+
+ tbody {
+ tr {
+ border-bottom: 1px solid #e2e8f0;
+
+ &.even {
+ background: #f8fafc;
+ }
+
+ &:hover {
+ background: #f1f5f9;
+ }
+
+ td {
+ padding: 12px 15px;
+ color: #334155;
+ font-size: 14px;
+
+ .probability-bar {
+ position: relative;
+ width: 100%;
+ height: 20px;
+ background: #e2e8f0;
+ border-radius: 10px;
+ overflow: hidden;
+
+ .probability-fill {
+ height: 100%;
+ border-radius: 10px;
+ transition: width 0.3s ease;
+ }
+
+ .probability-text {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 12px;
+ font-weight: 500;
+ color: white;
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/data-table/data-table.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/data-table/data-table.component.ts
new file mode 100644
index 0000000..3c1f598
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/data-table/data-table.component.ts
@@ -0,0 +1,117 @@
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { DashboardFilterService } from '../../services/dashboard-filter.service';
+import { Subscription } from 'rxjs';
+
+interface CrossSellingItem {
+ id: number;
+ companyName: string;
+ contactPerson: string;
+ product: string;
+ potentialValue: number;
+ probability: number;
+ nextAction: string;
+ actionDate: string;
+}
+
+@Component({
+ selector: 'app-shield-data-table',
+ templateUrl: './data-table.component.html',
+ styleUrls: ['./data-table.component.scss']
+})
+export class DataTableComponent implements OnInit, OnDestroy {
+ private filterSubscription: Subscription = new Subscription();
+
+ // Mock data for cross/up selling scope
+ originalCrossSellingData: CrossSellingItem[] = [
+ { id: 1, companyName: 'Tech Solutions Inc', contactPerson: 'John Smith', product: 'Product A', potentialValue: 50000, probability: 75, nextAction: 'Follow-up call', actionDate: '2023-06-15' },
+ { id: 2, companyName: 'Global Enterprises', contactPerson: 'Sarah Johnson', product: 'Product B', potentialValue: 75000, probability: 60, nextAction: 'Send proposal', actionDate: '2023-06-18' },
+ { id: 3, companyName: 'Innovative Systems', contactPerson: 'Mike Brown', product: 'Product C', potentialValue: 30000, probability: 85, nextAction: 'Demo scheduled', actionDate: '2023-06-20' },
+ { id: 4, companyName: 'Future Tech Ltd', contactPerson: 'Emily Davis', product: 'Product A', potentialValue: 45000, probability: 70, nextAction: 'Send quote', actionDate: '2023-06-22' },
+ { id: 5, companyName: 'Digital Dynamics', contactPerson: 'Robert Wilson', product: 'Product B', potentialValue: 60000, probability: 55, nextAction: 'Meeting scheduled', actionDate: '2023-06-25' },
+ { id: 6, companyName: 'Alpha Solutions', contactPerson: 'Lisa Miller', product: 'Product C', potentialValue: 40000, probability: 80, nextAction: 'Follow-up email', actionDate: '2023-06-28' },
+ { id: 7, companyName: 'Beta Innovations', contactPerson: 'David Taylor', product: 'Product A', potentialValue: 55000, probability: 65, nextAction: 'Product demo', actionDate: '2023-06-28' },
+ { id: 8, companyName: 'Gamma Technologies', contactPerson: 'Jennifer Anderson', product: 'Product B', potentialValue: 70000, probability: 50, nextAction: 'Proposal review', actionDate: '2023-07-05' }
+ ];
+
+ crossSellingData: CrossSellingItem[] = [...this.originalCrossSellingData];
+
+ // Sorting properties
+ sortColumn: keyof CrossSellingItem = 'companyName';
+ sortDirection: 'asc' | 'desc' = 'asc';
+
+ constructor(private filterService: DashboardFilterService) { }
+
+ ngOnInit(): void {
+ // Subscribe to filter changes
+ this.filterSubscription.add(
+ this.filterService.filterState$.subscribe(filters => {
+ this.updateTableData(filters);
+ })
+ );
+ }
+
+ ngOnDestroy(): void {
+ // Unsubscribe from all subscriptions
+ this.filterSubscription.unsubscribe();
+ }
+
+ // Update table data based on filters
+ updateTableData(filters: any): void {
+ // Simulate data change based on filters
+ // In a real implementation, this would fetch new data from an API
+
+ if (filters.salesRep || filters.partner || filters.tractionChannel || filters.subProductLine) {
+ // Apply filter effects (simplified logic)
+ const filteredData = this.originalCrossSellingData.filter(item => {
+ // Simple filtering logic - in a real app, this would be more sophisticated
+ return Math.random() > 0.3; // Randomly filter out some items
+ });
+
+ this.crossSellingData = filteredData;
+ } else {
+ // No filters applied, show all data
+ this.crossSellingData = [...this.originalCrossSellingData];
+ }
+
+ // Re-apply current sorting
+ this.sortTable(this.sortColumn);
+ }
+
+ // Sort table by column
+ sortTable(column: keyof CrossSellingItem): void {
+ if (this.sortColumn === column) {
+ this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
+ } else {
+ this.sortColumn = column;
+ this.sortDirection = 'asc';
+ }
+
+ this.crossSellingData.sort((a, b) => {
+ const aValue = a[column];
+ const bValue = b[column];
+
+ if (aValue < bValue) {
+ return this.sortDirection === 'asc' ? -1 : 1;
+ }
+ if (aValue > bValue) {
+ return this.sortDirection === 'asc' ? 1 : -1;
+ }
+ return 0;
+ });
+ }
+
+ // Format currency
+ formatCurrency(value: number): string {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0
+ }).format(value);
+ }
+
+ // Format probability as percentage
+ formatProbability(probability: number): string {
+ return `${probability}%`;
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/deal-details-card/deal-details-card.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/deal-details-card/deal-details-card.component.html
new file mode 100644
index 0000000..f02e7af
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/deal-details-card/deal-details-card.component.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ {{ deal.description }}
+
+
+
+
Amount
+
{{ formatCurrency(deal.amount) }}
+
+
+
Stage Days
+
{{ deal.stageDays }} days
+
+
+
Contact
+
{{ deal.contactPerson }}
+
+
+
Last Contact
+
{{ deal.lastContact }}
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/deal-details-card/deal-details-card.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/deal-details-card/deal-details-card.component.scss
new file mode 100644
index 0000000..e82e719
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/deal-details-card/deal-details-card.component.scss
@@ -0,0 +1,102 @@
+.deal-details-container {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+
+ .deal-details-header {
+ margin-bottom: 20px;
+
+ h3 {
+ font-size: 18px;
+ font-weight: 600;
+ color: #0a192f;
+ margin: 0;
+ }
+ }
+
+ .deal-cards {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ overflow-y: auto;
+
+ .deal-card {
+ background: white;
+ border-radius: 10px;
+ padding: 20px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ border: 1px solid #e2e8f0;
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ }
+
+ .deal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 15px;
+
+ .company-name {
+ font-size: 18px;
+ font-weight: 600;
+ color: #0a192f;
+ flex: 1;
+ }
+
+ .deal-stage {
+ padding: 6px 12px;
+ border-radius: 20px;
+ font-size: 12px;
+ font-weight: 500;
+ color: white;
+ text-align: center;
+ min-width: 120px;
+ }
+ }
+
+ .deal-description {
+ color: #64748b;
+ font-size: 14px;
+ line-height: 1.5;
+ margin-bottom: 20px;
+ }
+
+ .deal-info {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 15px;
+
+ .info-item {
+ .info-label {
+ font-size: 12px;
+ color: #94a3b8;
+ margin-bottom: 4px;
+ }
+
+ .info-value {
+ font-size: 14px;
+ font-weight: 500;
+ color: #0a192f;
+ }
+ }
+ }
+ }
+ }
+}
+
+// Responsive design
+@media (max-width: 768px) {
+ .deal-details-container {
+ .deal-cards {
+ .deal-card {
+ .deal-info {
+ grid-template-columns: 1fr;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/deal-details-card/deal-details-card.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/deal-details-card/deal-details-card.component.ts
new file mode 100644
index 0000000..66bfe5a
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/deal-details-card/deal-details-card.component.ts
@@ -0,0 +1,119 @@
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { DashboardFilterService } from '../../services/dashboard-filter.service';
+import { Subscription } from 'rxjs';
+
+interface DealDetail {
+ id: number;
+ companyName: string;
+ stage: string;
+ description: string;
+ amount: number;
+ stageDays: number;
+ contactPerson: string;
+ lastContact: string;
+}
+
+@Component({
+ selector: 'app-shield-deal-details-card',
+ templateUrl: './deal-details-card.component.html',
+ styleUrls: ['./deal-details-card.component.scss']
+})
+export class DealDetailsCardComponent implements OnInit, OnDestroy {
+ private filterSubscription: Subscription = new Subscription();
+
+ // Mock deal details data
+ originalDealDetails: DealDetail[] = [
+ {
+ id: 1,
+ companyName: 'Tech Solutions Inc',
+ stage: 'Negotiation',
+ description: 'Enterprise software solution for HR management',
+ amount: 125000,
+ stageDays: 15,
+ contactPerson: 'John Smith',
+ lastContact: '2023-06-10'
+ },
+ {
+ id: 2,
+ companyName: 'Global Enterprises',
+ stage: 'Decision Making',
+ description: 'Cloud infrastructure services migration',
+ amount: 85000,
+ stageDays: 8,
+ contactPerson: 'Sarah Johnson',
+ lastContact: '2023-06-12'
+ },
+ {
+ id: 3,
+ companyName: 'Innovative Systems',
+ stage: 'Value Proposition',
+ description: 'Custom AI implementation for logistics',
+ amount: 210000,
+ stageDays: 22,
+ contactPerson: 'Mike Brown',
+ lastContact: '2023-06-05'
+ }
+ ];
+
+ dealDetails: DealDetail[] = [...this.originalDealDetails];
+
+ // Stage colors
+ stageColors: { [key: string]: string } = {
+ 'Prospecting': '#93c5fd',
+ 'Qualification': '#60a5fa',
+ 'Needs Analysis': '#3b82f6',
+ 'Value Proposition': '#1d4ed8',
+ 'Decision Making': '#0a192f',
+ 'Negotiation': '#ff6b35'
+ };
+
+ constructor(private filterService: DashboardFilterService) { }
+
+ ngOnInit(): void {
+ // Subscribe to filter changes
+ this.filterSubscription.add(
+ this.filterService.filterState$.subscribe(filters => {
+ this.updateDealData(filters);
+ })
+ );
+ }
+
+ ngOnDestroy(): void {
+ // Unsubscribe from all subscriptions
+ this.filterSubscription.unsubscribe();
+ }
+
+ // Update deal data based on filters
+ updateDealData(filters: any): void {
+ // Simulate data change based on filters
+ // In a real implementation, this would fetch new data from an API
+
+ if (filters.salesRep || filters.partner || filters.tractionChannel || filters.subProductLine) {
+ // Apply filter effects (simplified logic)
+ const filteredData = this.originalDealDetails.filter(item => {
+ // Simple filtering logic - in a real app, this would be more sophisticated
+ return Math.random() > 0.2; // Randomly filter out some items
+ });
+
+ this.dealDetails = filteredData;
+ } else {
+ // No filters applied, show all data
+ this.dealDetails = [...this.originalDealDetails];
+ }
+ }
+
+ // Get stage color
+ getStageColor(stage: string): string {
+ return this.stageColors[stage] || '#64748b';
+ }
+
+ // Format currency
+ formatCurrency(value: number): string {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0
+ }).format(value);
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/donut-chart/donut-chart.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/donut-chart/donut-chart.component.html
new file mode 100644
index 0000000..2d1d1df
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/donut-chart/donut-chart.component.html
@@ -0,0 +1,21 @@
+
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/donut-chart/donut-chart.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/donut-chart/donut-chart.component.scss
new file mode 100644
index 0000000..b30144f
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/donut-chart/donut-chart.component.scss
@@ -0,0 +1,70 @@
+.chart-container {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+
+ .chart-header {
+ margin-bottom: 20px;
+
+ h3 {
+ font-size: 18px;
+ font-weight: 600;
+ color: #0a192f;
+ margin: 0;
+ }
+ }
+
+ .chart-wrapper {
+ flex: 1;
+ position: relative;
+
+ .chart-content {
+ position: relative;
+ height: 100%;
+
+ &.loading {
+ opacity: 0.7;
+
+ canvas {
+ filter: blur(2px);
+ }
+ }
+
+ canvas {
+ max-width: 100%;
+ max-height: 100%;
+ transition: filter 0.3s ease;
+ }
+
+ .loading-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255, 255, 255, 0.8);
+
+ .shimmer-donut {
+ width: 120px;
+ height: 120px;
+ border-radius: 50%;
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+ }
+ }
+ }
+ }
+}
+
+@keyframes shimmer {
+ 0% {
+ background-position: -200% 0;
+ }
+ 100% {
+ background-position: 200% 0;
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/donut-chart/donut-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/donut-chart/donut-chart.component.ts
new file mode 100644
index 0000000..f986f07
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/donut-chart/donut-chart.component.ts
@@ -0,0 +1,195 @@
+import { Component, Input, OnInit, ViewChild, OnDestroy } from '@angular/core';
+import { BaseChartDirective } from 'ng2-charts';
+import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js';
+import { DashboardFilterService } from '../../services/dashboard-filter.service';
+import { Subscription } from 'rxjs';
+
+@Component({
+ selector: 'app-shield-donut-chart',
+ templateUrl: './donut-chart.component.html',
+ styleUrls: ['./donut-chart.component.scss']
+})
+export class DonutChartComponent implements OnInit, OnDestroy {
+ @Input() chartType: 'endCustomer' | 'segmentPenetration' = 'endCustomer';
+
+ @ViewChild(BaseChartDirective) chart?: BaseChartDirective;
+
+ private filterSubscription: Subscription = new Subscription();
+
+ // Loading state
+ isLoading: boolean = false;
+
+ public donutChartOptions: ChartConfiguration['options'] = {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ display: true,
+ position: 'bottom',
+ labels: {
+ color: '#64748b',
+ font: {
+ size: 12
+ },
+ padding: 20
+ }
+ },
+ tooltip: {
+ backgroundColor: 'rgba(10, 25, 47, 0.9)',
+ titleColor: '#ff6b35',
+ bodyColor: '#ffffff',
+ borderColor: 'rgba(255, 107, 53, 0.3)',
+ borderWidth: 1,
+ cornerRadius: 6,
+ callbacks: {
+ label: function(context) {
+ const label = context.label || '';
+ const value = context.parsed;
+ return `${label}: ${value}`;
+ }
+ }
+ }
+ }
+ };
+
+ public donutChartType: ChartType = 'doughnut';
+ public donutChartPlugins = [];
+
+ // Data for End Customer Stage Split
+ public endCustomerData: ChartData<'doughnut'> = {
+ labels: ['Prospecting', 'Qualification', 'Needs Analysis', 'Value Proposition', 'Decision Making'],
+ datasets: [
+ {
+ data: [30, 25, 20, 15, 10],
+ backgroundColor: [
+ '#0a192f',
+ '#1e3a8a',
+ '#3b82f6',
+ '#60a5fa',
+ '#93c5fd'
+ ],
+ hoverBackgroundColor: [
+ '#1e3a8a',
+ '#3b82f6',
+ '#60a5fa',
+ '#93c5fd',
+ '#bfdbfe'
+ ],
+ borderWidth: 0
+ }
+ ]
+ };
+
+ // Data for Segment Penetration
+ public segmentPenetrationData: ChartData<'doughnut'> = {
+ labels: ['Enterprise', 'Mid-Market', 'SMB', 'Startup'],
+ datasets: [
+ {
+ data: [40, 30, 20, 10],
+ backgroundColor: [
+ '#ff6b35',
+ '#ff8c66',
+ '#ffb099',
+ '#ffd6cc'
+ ],
+ hoverBackgroundColor: [
+ '#ff8c66',
+ '#ffb099',
+ '#ffd6cc',
+ '#ffffff'
+ ],
+ borderWidth: 0
+ }
+ ]
+ };
+
+ constructor(private filterService: DashboardFilterService) { }
+
+ ngOnInit(): void {
+ // Subscribe to filter changes
+ this.filterSubscription.add(
+ this.filterService.filterState$.subscribe(filters => {
+ this.updateChartData(filters);
+ })
+ );
+ }
+
+ ngOnDestroy(): void {
+ // Unsubscribe from all subscriptions
+ this.filterSubscription.unsubscribe();
+ }
+
+ // Get chart data based on chart type
+ get chartData(): ChartData<'doughnut'> {
+ return this.chartType === 'endCustomer' ? this.endCustomerData : this.segmentPenetrationData;
+ }
+
+ // Get chart title based on chart type
+ get chartTitle(): string {
+ return this.chartType === 'endCustomer'
+ ? 'End Customer Stage Split'
+ : 'Segment Penetration';
+ }
+
+ // Update chart data based on filters
+ updateChartData(filters: any): void {
+ // Show loading state
+ this.isLoading = true;
+
+ // Simulate data change based on filters
+ // In a real implementation, this would fetch new data from an API
+
+ // Add a small delay to simulate loading
+ setTimeout(() => {
+ if (this.chartType === 'endCustomer') {
+ const baseData = [30, 25, 20, 15, 10];
+
+ // Apply filter effects (simplified logic)
+ let multiplier = 1;
+ if (filters.salesRep) multiplier *= 0.9;
+ if (filters.partner) multiplier *= 0.85;
+ if (filters.tractionChannel) multiplier *= 0.95;
+ if (filters.subProductLine) multiplier *= 0.8;
+
+ this.endCustomerData.datasets[0].data = baseData.map(value =>
+ Math.floor(value * multiplier)
+ );
+ } else {
+ const baseData = [40, 30, 20, 10];
+
+ // Apply filter effects (simplified logic)
+ let multiplier = 1;
+ if (filters.salesRep) multiplier *= 0.85;
+ if (filters.partner) multiplier *= 0.9;
+ if (filters.tractionChannel) multiplier *= 0.95;
+ if (filters.subProductLine) multiplier *= 0.75;
+
+ this.segmentPenetrationData.datasets[0].data = baseData.map(value =>
+ Math.floor(value * multiplier)
+ );
+ }
+
+ // Update chart
+ this.updateChart();
+
+ // Hide loading state
+ this.isLoading = false;
+ }, 300);
+ }
+
+ // Update chart with new data
+ updateChart(): void {
+ if (this.chart) {
+ this.chart.update();
+ }
+ }
+
+ // events
+ public chartClicked({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
+ console.log(event, active);
+ }
+
+ public chartHovered({ event, active }: { event?: ChartEvent, active?: {}[] }): void {
+ console.log(event, active);
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/loading-shimmer/loading-shimmer.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/loading-shimmer/loading-shimmer.component.scss
new file mode 100644
index 0000000..a365e59
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/loading-shimmer/loading-shimmer.component.scss
@@ -0,0 +1,45 @@
+.shimmer-container {
+ position: relative;
+ overflow: hidden;
+
+ .shimmer-content {
+ transition: opacity 0.3s ease;
+ }
+
+ .shimmer-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(255, 255, 255, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 100;
+
+ .shimmer-animation {
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+ border-radius: 8px;
+ }
+ }
+
+ &.active {
+ .shimmer-content {
+ opacity: 0.5;
+ }
+ }
+}
+
+@keyframes shimmer {
+ 0% {
+ background-position: -200% 0;
+ }
+ 100% {
+ background-position: 200% 0;
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/loading-shimmer/loading-shimmer.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/loading-shimmer/loading-shimmer.component.ts
new file mode 100644
index 0000000..d17d8e3
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/loading-shimmer/loading-shimmer.component.ts
@@ -0,0 +1,19 @@
+import { Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'app-shield-loading-shimmer',
+ template: `
+
+ `,
+ styleUrls: ['./loading-shimmer.component.scss']
+})
+export class LoadingShimmerComponent {
+ @Input() loading: boolean = false;
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/map-chart/map-chart.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/map-chart/map-chart.component.html
new file mode 100644
index 0000000..d11c78d
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/map-chart/map-chart.component.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
{{ getStatusLabel(status) }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/map-chart/map-chart.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/map-chart/map-chart.component.scss
new file mode 100644
index 0000000..71e33d3
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/map-chart/map-chart.component.scss
@@ -0,0 +1,135 @@
+.map-container {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+
+ .map-header {
+ margin-bottom: 20px;
+
+ h3 {
+ font-size: 18px;
+ font-weight: 600;
+ color: #0a192f;
+ margin: 0;
+ }
+ }
+
+ .map-wrapper {
+ flex: 1;
+ position: relative;
+
+ .map-placeholder {
+ height: 100%;
+ background: #f8fafc;
+ border-radius: 8px;
+ position: relative;
+ overflow: hidden;
+
+ .india-map {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ background: #e2e8f0;
+
+ .map-outline {
+ position: absolute;
+ top: 10%;
+ left: 10%;
+ right: 10%;
+ bottom: 10%;
+ background: #cbd5e1;
+ border-radius: 50% 40% 45% 50% / 40% 50% 40% 45%;
+ box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.1);
+ }
+
+ .dealer-marker {
+ position: absolute;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ cursor: pointer;
+ transform: translate(-50%, -50%);
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+
+ &:hover {
+ transform: translate(-50%, -50%) scale(1.3);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
+ z-index: 10;
+ }
+ }
+ }
+
+ .map-legend {
+ position: absolute;
+ bottom: 20px;
+ left: 20px;
+ background: rgba(255, 255, 255, 0.9);
+ border-radius: 8px;
+ padding: 12px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ display: flex;
+ gap: 15px;
+
+ .legend-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+
+ .legend-color {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ }
+
+ .legend-label {
+ font-size: 12px;
+ color: #64748b;
+ }
+ }
+ }
+
+ .dealer-tooltip {
+ position: absolute;
+ background: rgba(10, 25, 47, 0.95);
+ color: white;
+ border-radius: 6px;
+ padding: 10px 15px;
+ transform: translate(-50%, -100%);
+ margin-top: -10px;
+ z-index: 100;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ min-width: 180px;
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ margin-left: -5px;
+ border-width: 5px;
+ border-style: solid;
+ border-color: rgba(10, 25, 47, 0.95) transparent transparent transparent;
+ }
+
+ .tooltip-content {
+ .dealer-name {
+ font-weight: 600;
+ margin-bottom: 4px;
+ }
+
+ .dealer-location {
+ font-size: 13px;
+ color: #cbd5e1;
+ margin-bottom: 4px;
+ }
+
+ .dealer-status {
+ font-size: 12px;
+ font-weight: 500;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/map-chart/map-chart.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/map-chart/map-chart.component.ts
new file mode 100644
index 0000000..08310bf
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/map-chart/map-chart.component.ts
@@ -0,0 +1,109 @@
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { DashboardFilterService } from '../../services/dashboard-filter.service';
+import { Subscription } from 'rxjs';
+
+interface DealerLocation {
+ id: number;
+ name: string;
+ lat: number;
+ lng: number;
+ status: 'active' | 'inactive' | 'training' | 'onboarding';
+ city: string;
+ state: string;
+}
+
+@Component({
+ selector: 'app-shield-map-chart',
+ templateUrl: './map-chart.component.html',
+ styleUrls: ['./map-chart.component.scss']
+})
+export class MapChartComponent implements OnInit, OnDestroy {
+ private filterSubscription: Subscription = new Subscription();
+
+ // Mock dealer location data
+ originalDealerLocations: DealerLocation[] = [
+ { id: 1, name: 'ABC Motors', lat: 28.6139, lng: 77.2090, status: 'active', city: 'New Delhi', state: 'Delhi' },
+ { id: 2, name: 'XYZ Auto', lat: 19.0760, lng: 72.8777, status: 'active', city: 'Mumbai', state: 'Maharashtra' },
+ { id: 3, name: 'PQR Dealers', lat: 13.0827, lng: 80.2707, status: 'training', city: 'Chennai', state: 'Tamil Nadu' },
+ { id: 4, name: 'LMN Enterprises', lat: 12.9716, lng: 77.5946, status: 'inactive', city: 'Bangalore', state: 'Karnataka' },
+ { id: 5, name: 'DEF Solutions', lat: 22.5726, lng: 88.3639, status: 'active', city: 'Kolkata', state: 'West Bengal' },
+ { id: 6, name: 'GHI Services', lat: 25.3176, lng: 82.9739, status: 'onboarding', city: 'Varanasi', state: 'Uttar Pradesh' },
+ { id: 7, name: 'JKL Group', lat: 23.0225, lng: 72.5714, status: 'active', city: 'Ahmedabad', state: 'Gujarat' },
+ { id: 8, name: 'MNO Corp', lat: 18.5204, lng: 73.8567, status: 'training', city: 'Pune', state: 'Maharashtra' }
+ ];
+
+ dealerLocations: DealerLocation[] = [...this.originalDealerLocations];
+
+ // Status colors
+ statusColors = {
+ active: '#0a192f',
+ inactive: '#64748b',
+ training: '#ff6b35',
+ onboarding: '#f59e0b'
+ };
+
+ // Status labels
+ statusLabels = {
+ active: 'Active',
+ inactive: 'Inactive',
+ training: 'Training',
+ onboarding: 'Onboarding'
+ };
+
+ hoveredDealer: DealerLocation | null = null;
+
+ constructor(private filterService: DashboardFilterService) { }
+
+ ngOnInit(): void {
+ // Subscribe to filter changes
+ this.filterSubscription.add(
+ this.filterService.filterState$.subscribe(filters => {
+ this.updateMapData(filters);
+ })
+ );
+ }
+
+ ngOnDestroy(): void {
+ // Unsubscribe from all subscriptions
+ this.filterSubscription.unsubscribe();
+ }
+
+ // Update map data based on filters
+ updateMapData(filters: any): void {
+ // Simulate data change based on filters
+ // In a real implementation, this would fetch new data from an API
+
+ if (filters.salesRep || filters.partner || filters.tractionChannel || filters.subProductLine) {
+ // Apply filter effects (simplified logic)
+ const filteredData = this.originalDealerLocations.filter(location => {
+ // Simple filtering logic - in a real app, this would be more sophisticated
+ return Math.random() > 0.25; // Randomly filter out some locations
+ });
+
+ this.dealerLocations = filteredData;
+ } else {
+ // No filters applied, show all data
+ this.dealerLocations = [...this.originalDealerLocations];
+ }
+ }
+
+ // Get status color based on dealer status
+ getStatusColor(status: string): string {
+ return (this.statusColors as any)[status] || '#64748b';
+ }
+
+ // Get status label based on dealer status
+ getStatusLabel(status: string): string {
+ return (this.statusLabels as any)[status] || status;
+ }
+
+ // Handle dealer hover
+ onDealerHover(dealer: DealerLocation): void {
+ this.hoveredDealer = dealer;
+ }
+
+ // Handle dealer leave
+ onDealerLeave(): void {
+ this.hoveredDealer = null;
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/quarterwise-flow/quarterwise-flow.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/quarterwise-flow/quarterwise-flow.component.html
new file mode 100644
index 0000000..bdb8fa1
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/quarterwise-flow/quarterwise-flow.component.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+ ℹ️
+
+
+
Chart Configuration Incomplete
+
Please configure the quarterwise flow chart settings to visualize the data.
+
+
+ ⚙️
+ Configure Chart
+
+
+
+
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/quarterwise-flow/quarterwise-flow.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/quarterwise-flow/quarterwise-flow.component.scss
new file mode 100644
index 0000000..054b5f0
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/quarterwise-flow/quarterwise-flow.component.scss
@@ -0,0 +1,70 @@
+.quarterwise-flow-container {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+
+ .quarterwise-flow-header {
+ margin-bottom: 20px;
+
+ h3 {
+ font-size: 18px;
+ font-weight: 600;
+ color: #0a192f;
+ margin: 0;
+ }
+ }
+
+ .quarterwise-flow-content {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .placeholder-message {
+ text-align: center;
+ padding: 30px;
+ background: #f8fafc;
+ border-radius: 12px;
+ border: 1px dashed #cbd5e1;
+
+ .message-icon {
+ color: #94a3b8;
+ margin-bottom: 20px;
+ }
+
+ .message-text {
+ h4 {
+ color: #0a192f;
+ margin: 0 0 10px 0;
+ font-weight: 600;
+ }
+
+ p {
+ color: #64748b;
+ margin: 0 0 20px 0;
+ font-size: 14px;
+ line-height: 1.5;
+ }
+ }
+
+ .configure-button {
+ padding: 10px 20px;
+ background: #0a192f;
+ color: white;
+ border: none;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ transition: background-color 0.2s ease;
+
+ &:hover {
+ background: #1e3a8a;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/quarterwise-flow/quarterwise-flow.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/quarterwise-flow/quarterwise-flow.component.ts
new file mode 100644
index 0000000..678b47d
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/quarterwise-flow/quarterwise-flow.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-shield-quarterwise-flow',
+ templateUrl: './quarterwise-flow.component.html',
+ styleUrls: ['./quarterwise-flow.component.scss']
+})
+export class QuarterwiseFlowComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/sidebar-filters/sidebar-filters.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/sidebar-filters/sidebar-filters.component.html
new file mode 100644
index 0000000..87f3121
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/sidebar-filters/sidebar-filters.component.html
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/sidebar-filters/sidebar-filters.component.html.new b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/sidebar-filters/sidebar-filters.component.html.new
new file mode 100644
index 0000000..95849af
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/sidebar-filters/sidebar-filters.component.html.new
@@ -0,0 +1,25 @@
+
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/sidebar-filters/sidebar-filters.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/sidebar-filters/sidebar-filters.component.scss
new file mode 100644
index 0000000..6ddf655
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/sidebar-filters/sidebar-filters.component.scss
@@ -0,0 +1,216 @@
+.sidebar-filters {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+
+ .componentbtn {
+ margin: 10px;
+ width: calc(100% - 20px);
+ }
+
+ .nav-list {
+ padding: 0;
+ margin: 0 10px;
+
+ .nav-link {
+ display: flex;
+ align-items: center;
+ padding: 8px 12px;
+ margin-bottom: 5px;
+ background: #f8f9fa;
+ border: 1px solid #e9ecef;
+ border-radius: 4px;
+ cursor: move;
+
+ &:hover {
+ background: #e9ecef;
+ }
+
+ .has-badge {
+ margin-left: auto;
+ }
+ }
+ }
+
+ .app-header {
+ margin-bottom: 30px;
+
+ .app-name {
+ font-size: 24px;
+ font-weight: 700;
+ margin: 0 0 5px 0;
+ color: #ff6b35;
+ }
+
+ .dashboard-title {
+ font-size: 18px;
+ font-weight: 500;
+ margin: 0;
+ color: #cbd5e1;
+ }
+ }
+
+ .kpi-section {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ margin-bottom: 30px;
+
+ .kpi-card {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 10px;
+ padding: 15px;
+ text-align: center;
+
+ .kpi-title {
+ font-size: 14px;
+ color: #94a3b8;
+ margin-bottom: 8px;
+ }
+
+ .kpi-value {
+ font-size: 24px;
+ font-weight: 700;
+ color: white;
+ }
+ }
+
+ .total-leads {
+ border-left: 3px solid #ff6b35;
+ }
+
+ .total-deals {
+ border-left: 3px solid #0a192f;
+ }
+ }
+
+ .component-palette-section {
+ margin: 20px;
+
+ .component-palette-button {
+ width: 100%;
+ padding: 12px;
+ background: rgba(255, 107, 53, 0.2);
+ color: white;
+ border: 1px solid rgba(255, 107, 53, 0.5);
+ border-radius: 8px;
+ font-size: 16px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+
+ &:hover {
+ background: rgba(255, 107, 53, 0.3);
+ border-color: #ff6b35;
+ }
+ }
+
+ .component-palette {
+ margin-top: 15px;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 8px;
+ padding: 15px;
+
+ .palette-title {
+ font-size: 16px;
+ color: white;
+ margin: 0 0 15px 0;
+ text-align: center;
+ }
+
+ .component-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ .component-item {
+ padding: 10px 15px;
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 6px;
+ color: white;
+ cursor: move;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: rgba(255, 107, 53, 0.2);
+ border-color: #ff6b35;
+ }
+
+ .drag-icon {
+ font-size: 16px;
+ }
+ }
+ }
+ }
+ }
+
+ .filters-section {
+ flex: 1;
+
+ .filters-title {
+ font-size: 18px;
+ color: white;
+ margin: 0 0 20px 0;
+ padding-bottom: 10px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ }
+
+ .filter-group {
+ margin-bottom: 20px;
+
+ label {
+ display: block;
+ font-size: 14px;
+ color: #cbd5e1;
+ margin-bottom: 8px;
+ }
+
+ .filter-select {
+ width: 100%;
+ padding: 10px 15px;
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 8px;
+ color: white;
+ font-size: 14px;
+
+ &:focus {
+ outline: none;
+ border-color: #ff6b35;
+ box-shadow: 0 0 0 2px rgba(255, 107, 53, 0.2);
+ }
+
+ option {
+ background: #0a192f;
+ color: white;
+ }
+ }
+ }
+
+ .reset-button {
+ width: 100%;
+ padding: 12px;
+ background: rgba(255, 255, 255, 0.1);
+ color: white;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 8px;
+ font-size: 16px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: rgba(255, 107, 53, 0.2);
+ border-color: #ff6b35;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/sidebar-filters/sidebar-filters.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/sidebar-filters/sidebar-filters.component.ts
new file mode 100644
index 0000000..295f2c1
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/sidebar-filters/sidebar-filters.component.ts
@@ -0,0 +1,16 @@
+import { Component, OnInit } from '@angular/core';
+import { DashboardFilterService } from '../../services/dashboard-filter.service';
+
+@Component({
+ selector: 'app-shield-sidebar-filters',
+ templateUrl: './sidebar-filters.component.html',
+ styleUrls: ['./sidebar-filters.component.scss']
+})
+export class SidebarFiltersComponent implements OnInit {
+
+ constructor(private filterService: DashboardFilterService) { }
+
+ ngOnInit(): void {
+ // Component initialization
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/sidebar-filters/sidebar-filters.component.ts.new b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/sidebar-filters/sidebar-filters.component.ts.new
new file mode 100644
index 0000000..295f2c1
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/components/sidebar-filters/sidebar-filters.component.ts.new
@@ -0,0 +1,16 @@
+import { Component, OnInit } from '@angular/core';
+import { DashboardFilterService } from '../../services/dashboard-filter.service';
+
+@Component({
+ selector: 'app-shield-sidebar-filters',
+ templateUrl: './sidebar-filters.component.html',
+ styleUrls: ['./sidebar-filters.component.scss']
+})
+export class SidebarFiltersComponent implements OnInit {
+
+ constructor(private filterService: DashboardFilterService) { }
+
+ ngOnInit(): void {
+ // Component initialization
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/index.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/index.ts
new file mode 100644
index 0000000..337ea1a
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/index.ts
@@ -0,0 +1,10 @@
+export * from './shield-dashboard.component';
+
+// Export all components
+export * from './components/sidebar-filters/sidebar-filters.component';
+export * from './components/bar-chart/bar-chart.component';
+export * from './components/donut-chart/donut-chart.component';
+export * from './components/map-chart/map-chart.component';
+export * from './components/data-table/data-table.component';
+export * from './components/deal-details-card/deal-details-card.component';
+export * from './components/quarterwise-flow/quarterwise-flow.component';
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/services/dashboard-filter.service.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/services/dashboard-filter.service.ts
new file mode 100644
index 0000000..97e4593
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/services/dashboard-filter.service.ts
@@ -0,0 +1,84 @@
+import { Injectable } from '@angular/core';
+import { BehaviorSubject, Observable } from 'rxjs';
+
+// Define the filter state interface
+export interface FilterState {
+ salesRep: string;
+ partner: string;
+ tractionChannel: string;
+ subProductLine: string;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class DashboardFilterService {
+ // Shared filter state using BehaviorSubject
+ private filterStateSubject = new BehaviorSubject({
+ salesRep: '',
+ partner: '',
+ tractionChannel: '',
+ subProductLine: ''
+ });
+
+ public filterState$ = this.filterStateSubject.asObservable();
+
+ // KPI data
+ private kpiDataSubject = new BehaviorSubject<{ totalLeads: number; totalDeals: number }>({
+ totalLeads: 1248,
+ totalDeals: 842
+ });
+
+ public kpiData$ = this.kpiDataSubject.asObservable();
+
+ constructor() { }
+
+ // Update filter state
+ updateFilter(filterType: keyof FilterState, value: string): void {
+ const currentState = this.filterStateSubject.value;
+ const newState = { ...currentState, [filterType]: value };
+ this.filterStateSubject.next(newState);
+
+ // Simulate KPI data change based on filters
+ this.updateKpiData(newState);
+ }
+
+ // Reset all filters to default values
+ resetFilters(): void {
+ this.filterStateSubject.next({
+ salesRep: '',
+ partner: '',
+ tractionChannel: '',
+ subProductLine: ''
+ });
+
+ // Reset KPI data to default values
+ this.kpiDataSubject.next({
+ totalLeads: 1248,
+ totalDeals: 842
+ });
+ }
+
+ // Update KPI data based on filters (simulated)
+ private updateKpiData(filters: FilterState): void {
+ // This is a simplified simulation - in a real app, this would come from an API
+ let totalLeads = 1248;
+ let totalDeals = 842;
+
+ // Apply filter effects (simplified logic)
+ if (filters.salesRep) totalLeads = Math.floor(totalLeads * 0.8);
+ if (filters.partner) totalDeals = Math.floor(totalDeals * 0.9);
+ if (filters.tractionChannel) totalLeads = Math.floor(totalLeads * 0.85);
+ if (filters.subProductLine) totalDeals = Math.floor(totalDeals * 0.95);
+
+ this.kpiDataSubject.next({
+ totalLeads,
+ totalDeals
+ });
+ }
+
+ // Get current filter values
+ getCurrentFilters(): FilterState {
+ return this.filterStateSubject.value;
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard-routing.module.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard-routing.module.ts
new file mode 100644
index 0000000..c14cfdb
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard-routing.module.ts
@@ -0,0 +1,16 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { ShieldDashboardComponent } from './shield-dashboard.component';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: ShieldDashboardComponent
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class ShieldDashboardRoutingModule { }
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.component.html b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.component.html
new file mode 100644
index 0000000..6393fb8
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.component.html
@@ -0,0 +1,493 @@
+
+
+
+
+
+
+
+
+
0">
+
Deleted Items
+
+
+ {{ item.name }}
+
+ Restore
+
+
+
+
+ Clear All
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{item.name}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Configure Chart
+
+
+
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.component.scss b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.component.scss
new file mode 100644
index 0000000..dbce2de
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.component.scss
@@ -0,0 +1,220 @@
+.shield-dashboard {
+ padding: 20px;
+ background-color: #f5f5f5;
+ min-height: 100vh;
+}
+
+.dashboard-container {
+ display: flex;
+ gap: 20px;
+}
+
+.sidebar {
+ flex: 0 0 250px;
+ background: white;
+ border-radius: 4px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ padding: 15px;
+}
+
+.main-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.kpi-section {
+ display: flex;
+ gap: 20px;
+ margin-bottom: 20px;
+}
+
+.kpi-card {
+ flex: 1;
+ background: white;
+ border-radius: 4px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ padding: 20px;
+ text-align: center;
+}
+
+.kpi-title {
+ font-size: 14px;
+ color: #666;
+ margin-bottom: 10px;
+}
+
+.kpi-value {
+ font-size: 24px;
+ font-weight: bold;
+ color: #333;
+}
+
+.total-leads {
+ border-top: 4px solid #4285f4;
+}
+
+.total-deals {
+ border-top: 4px solid #0f9d58;
+}
+
+.deleted-items-section {
+ background: white;
+ border-radius: 4px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ padding: 20px;
+
+ h3 {
+ margin-top: 0;
+ color: #333;
+ border-bottom: 1px solid #eee;
+ padding-bottom: 10px;
+ }
+
+ .deleted-items-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-bottom: 15px;
+
+ .deleted-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ background: #f8f9fa;
+ border: 1px solid #e9ecef;
+ border-radius: 4px;
+ padding: 8px 12px;
+
+ span {
+ font-size: 14px;
+ color: #495057;
+ }
+ }
+ }
+}
+
+.dashboard-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
+ gap: 20px;
+}
+
+.grid-item {
+ background: white;
+ border-radius: 4px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ padding: 15px;
+ position: relative;
+}
+
+.grid-item-content {
+ margin-top: 30px;
+}
+
+.drag-handler {
+ cursor: move;
+}
+
+.drop-zone-indicator {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 500px;
+ background-color: #f8f9fa;
+ border: 2px dashed #dee2e6;
+ border-radius: 4px;
+ color: #6c757d;
+ font-size: 18px;
+ text-align: center;
+
+ p {
+ margin: 0;
+ }
+}
+
+/* Gridster specific styles */
+gridster {
+ background: transparent !important;
+}
+
+gridster-item {
+ background: white;
+ border-radius: 4px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ overflow: hidden;
+}
+
+/* Configuration Modal Styles */
+.section-divider {
+ margin-top: 20px;
+ padding-top: 15px;
+ border-top: 1px solid #eee;
+}
+
+.section-divider:first-child {
+ border-top: none;
+ padding-top: 0;
+ margin-top: 0;
+}
+
+.filter-item {
+ margin-bottom: 15px;
+ padding: 12px;
+ border: 1px solid #e9ecef;
+ border-radius: 4px;
+ background-color: #f8f9fa;
+}
+
+.filter-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.filter-content {
+ margin-top: 10px;
+}
+
+.drilldown-layer {
+ margin-top: 20px;
+ padding: 15px;
+ border: 1px solid #dee2e6;
+ border-radius: 4px;
+ background-color: #ffffff;
+}
+
+.layer-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #eee;
+}
+
+.layer-header h5 {
+ margin: 0;
+ color: #495057;
+}
+
+/* Responsive design */
+@media (max-width: 768px) {
+ .dashboard-container {
+ flex-direction: column;
+ }
+
+ .sidebar {
+ flex: 0 0 auto;
+ }
+
+ .kpi-section {
+ flex-direction: column;
+ }
+
+ .filter-item .clr-row > div {
+ margin-bottom: 10px;
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.component.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.component.ts
new file mode 100644
index 0000000..5fec6fc
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.component.ts
@@ -0,0 +1,596 @@
+import { Component, OnInit } from '@angular/core';
+import { GridsterConfig, GridsterItem } from 'angular-gridster2';
+import { FormBuilder, FormGroup } from '@angular/forms';
+import { DatastoreService } from 'src/app/services/fnd/datastore.service';
+import { AlertsService } from 'src/app/services/fnd/alerts.service';
+import { SureconnectService } from '../../sureconnect/sureconnect.service';
+
+interface ShieldDashboardItem extends GridsterItem {
+ chartType: string;
+ name: string;
+ id: number;
+ component?: any;
+ // Configuration properties
+ charttitle?: string;
+ connection?: string;
+ table?: string;
+ xAxis?: string;
+ yAxis?: string[];
+ chartlegend?: boolean;
+ showlabel?: boolean;
+ baseFilters?: any[];
+ drilldownEnabled?: boolean;
+ drilldownApiUrl?: string;
+ drilldownXAxis?: string;
+ drilldownYAxis?: string;
+ drilldownParameter?: string;
+ drilldownFilters?: any[];
+ drilldownLayers?: any[];
+}
+
+interface WidgetModel {
+ name: string;
+ identifier: string;
+}
+
+@Component({
+ selector: 'app-shield-dashboard',
+ templateUrl: './shield-dashboard.component.html',
+ styleUrls: ['./shield-dashboard.component.scss']
+})
+export class ShieldDashboardComponent implements OnInit {
+ options: GridsterConfig;
+ dashboard: Array;
+
+ // Configuration modal
+ configModalOpen = false;
+ modeledit = false; // Add this to match the main dashboard pattern
+ configForm: FormGroup;
+ selectedItem: ShieldDashboardItem | null = null;
+
+ // Component palette
+ showComponentPalette = false;
+ WidgetsMock: WidgetModel[] = [
+ {
+ name: 'Bar Chart',
+ identifier: 'bar_chart'
+ },
+ {
+ name: 'Doughnut Chart',
+ identifier: 'doughnut_chart'
+ },
+ {
+ name: 'Map Chart',
+ identifier: 'map_chart'
+ },
+ {
+ name: 'Data Table',
+ identifier: 'grid_view'
+ },
+ {
+ name: 'Deal Details',
+ identifier: 'to_do_chart'
+ },
+ {
+ name: 'Quarterwise Flow',
+ identifier: 'line_chart'
+ },
+ {
+ name: 'Compact Filter',
+ identifier: 'compact_filter'
+ }
+ ];
+
+ // Services data
+ storedata: any[] = [];
+ columnData: any[] = [];
+ sureconnectData: any[] = [];
+ drilldownColumnData: any[] = [];
+ layerColumnData: { [key: number]: any[] } = {};
+
+ // Keep track of deleted items
+ deletedItems: Array = [];
+
+ constructor(
+ private _fb: FormBuilder,
+ private datastoreService: DatastoreService,
+ private alertService: AlertsService,
+ private sureconnectService: SureconnectService
+ ) { }
+
+ ngOnInit(): void {
+ this.options = {
+ gridType: 'fit',
+ enableEmptyCellDrop: true,
+ emptyCellDropCallback: this.onDrop,
+ pushItems: true,
+ swap: true,
+ pushDirections: { north: true, east: true, south: true, west: true },
+ resizable: { enabled: true },
+ itemChangeCallback: this.itemChange.bind(this),
+ draggable: {
+ enabled: true,
+ ignoreContent: true,
+ dropOverItems: true,
+ dragHandleClass: 'drag-handler',
+ ignoreContentClass: 'no-drag',
+ },
+ displayGrid: 'always',
+ minCols: 10,
+ minRows: 10,
+ itemResizeCallback: this.itemResize.bind(this)
+ };
+
+ // Initialize the dashboard with empty canvas
+ this.dashboard = [];
+
+ // Initialize form
+ this.configForm = this._fb.group({
+ charttitle: [''],
+ connection: [''],
+ table: [''],
+ xAxis: [''],
+ yAxis: [''],
+ chartlegend: [true],
+ showlabel: [true]
+ });
+
+ // Load service data
+ this.loadServicesData();
+ }
+
+ // Load initial data from services
+ loadServicesData() {
+ // Load sureconnect data
+ this.sureconnectService.getAll().subscribe((data: any[]) => {
+ this.sureconnectData = data;
+ });
+
+ // Load datastore data
+ this.datastoreService.getAll().subscribe((data) => {
+ this.storedata = data as any[];
+ });
+ }
+
+ // Toggle component palette visibility
+ toggleComponentPalette(): void {
+ this.showComponentPalette = !this.showComponentPalette;
+ }
+
+ // Handle drag start event for components - matching the working implementation
+ onDrag(event: DragEvent, identifier: string): void {
+ console.log("on drag", identifier);
+ console.log("on drag ", event);
+ if (event.dataTransfer) {
+ event.dataTransfer.setData('widgetIdentifier', identifier);
+ }
+ }
+
+ onDrop(ev: any) {
+ // Handle dropping new components onto the dashboard
+ console.log('Item dropped:', ev);
+
+ // Get the component identifier from the drag event
+ const componentType = ev.dataTransfer ? ev.dataTransfer.getData('widgetIdentifier') : '';
+ console.log('Component type dropped:', componentType);
+
+ if (componentType) {
+ this.addComponentToDashboard(componentType);
+ } else {
+ console.log('No component type found in drag data');
+ }
+ }
+
+ // Add a new component to the dashboard
+ addComponentToDashboard(componentType: string) {
+ // Generate a new ID for the component
+ const newId = this.dashboard.length > 0 ? Math.max(...this.dashboard.map(item => item.id), 0) + 1 : 1;
+
+ let newItem: ShieldDashboardItem;
+
+ switch (componentType) {
+ case "bar_chart":
+ newItem = {
+ cols: 5,
+ rows: 6,
+ y: 0,
+ x: 0,
+ chartType: 'bar-chart',
+ name: 'Bar Chart',
+ id: newId,
+ chartlegend: true,
+ showlabel: true,
+ baseFilters: [],
+ drilldownEnabled: false,
+ drilldownLayers: []
+ };
+ break;
+ case "doughnut_chart":
+ // For doughnut charts, we'll need to determine which one based on existing items
+ const donutCount = this.dashboard.filter(item => item.chartType === 'donut-chart').length;
+ if (donutCount % 2 === 0) {
+ newItem = {
+ cols: 5,
+ rows: 6,
+ y: 0,
+ x: 0,
+ chartType: 'donut-chart',
+ name: 'End Customer Donut',
+ id: newId,
+ chartlegend: true,
+ showlabel: true,
+ baseFilters: [],
+ drilldownEnabled: false,
+ drilldownLayers: []
+ };
+ } else {
+ newItem = {
+ cols: 5,
+ rows: 6,
+ y: 0,
+ x: 0,
+ chartType: 'donut-chart',
+ name: 'Segment Penetration Donut',
+ id: newId,
+ chartlegend: true,
+ showlabel: true,
+ baseFilters: [],
+ drilldownEnabled: false,
+ drilldownLayers: []
+ };
+ }
+ break;
+ case "map_chart":
+ newItem = {
+ cols: 5,
+ rows: 6,
+ y: 0,
+ x: 0,
+ chartType: 'map-chart',
+ name: 'Map Chart',
+ id: newId,
+ chartlegend: true,
+ showlabel: true,
+ baseFilters: [],
+ drilldownEnabled: false,
+ drilldownLayers: []
+ };
+ break;
+ case "grid_view":
+ newItem = {
+ cols: 10,
+ rows: 6,
+ y: 0,
+ x: 0,
+ chartType: 'data-table',
+ name: 'Data Table',
+ id: newId,
+ chartlegend: true,
+ showlabel: true,
+ baseFilters: [],
+ drilldownEnabled: false,
+ drilldownLayers: []
+ };
+ break;
+ case "to_do_chart":
+ newItem = {
+ cols: 5,
+ rows: 6,
+ y: 0,
+ x: 0,
+ chartType: 'deal-details',
+ name: 'Deal Details',
+ id: newId,
+ chartlegend: true,
+ showlabel: true,
+ baseFilters: [],
+ drilldownEnabled: false,
+ drilldownLayers: []
+ };
+ break;
+ case "line_chart":
+ newItem = {
+ cols: 5,
+ rows: 6,
+ y: 0,
+ x: 0,
+ chartType: 'quarterwise-flow',
+ name: 'Quarterwise Flow',
+ id: newId,
+ chartlegend: true,
+ showlabel: true,
+ baseFilters: [],
+ drilldownEnabled: false,
+ drilldownLayers: []
+ };
+ break;
+ case "compact_filter":
+ newItem = {
+ cols: 3,
+ rows: 2,
+ y: 0,
+ x: 0,
+ chartType: 'compact-filter',
+ name: 'Compact Filter',
+ id: newId,
+ baseFilters: [],
+ drilldownEnabled: false,
+ drilldownLayers: []
+ };
+ break;
+ default:
+ newItem = {
+ cols: 5,
+ rows: 6,
+ y: 0,
+ x: 0,
+ chartType: componentType,
+ name: componentType,
+ id: newId,
+ chartlegend: true,
+ showlabel: true,
+ baseFilters: [],
+ drilldownEnabled: false,
+ drilldownLayers: []
+ };
+ }
+
+ // Add the new item to the dashboard
+ this.dashboard.push(newItem);
+ }
+
+ removeItem(item: ShieldDashboardItem) {
+ // Add the item to deleted items list before removing
+ this.deletedItems.push({...item});
+
+ // Remove the item from the dashboard
+ this.dashboard.splice(this.dashboard.indexOf(item), 1);
+ }
+
+ // Restore a deleted item
+ restoreItem(item: ShieldDashboardItem) {
+ // Remove from deleted items
+ this.deletedItems.splice(this.deletedItems.indexOf(item), 1);
+
+ // Add back to dashboard
+ this.dashboard.push(item);
+ }
+
+ // Clear all deleted items
+ clearDeletedItems() {
+ this.deletedItems = [];
+ }
+
+ itemChange() {
+ console.log('Item changed:', this.dashboard);
+ }
+
+ itemResize(item: any, itemComponent: any) {
+ console.log('Item resized:', item);
+ // Trigger a window resize event to notify charts to resize
+ window.dispatchEvent(new Event('resize'));
+ }
+
+ /**
+ * Extract only the relevant chart configuration properties to pass to chart components
+ * This prevents errors when trying to set properties that don't exist on the components
+ */
+ getChartInputs(item: any): any {
+ // Only pass properties that are relevant to chart components
+ const chartInputs = {
+ chartType: item.chartType,
+ name: item.name,
+ charttitle: item.charttitle,
+ connection: item.connection,
+ table: item.table,
+ xAxis: item.xAxis,
+ yAxis: item.yAxis,
+ chartlegend: item.chartlegend,
+ showlabel: item.showlabel,
+ baseFilters: item.baseFilters || [],
+ drilldownEnabled: item.drilldownEnabled,
+ drilldownApiUrl: item.drilldownApiUrl,
+ drilldownXAxis: item.drilldownXAxis,
+ drilldownYAxis: item.drilldownYAxis,
+ drilldownParameter: item.drilldownParameter,
+ drilldownFilters: item.drilldownFilters || [],
+ drilldownLayers: item.drilldownLayers || []
+ };
+
+ // Remove undefined properties to avoid passing unnecessary data
+ Object.keys(chartInputs).forEach(key => {
+ if (chartInputs[key] === undefined) {
+ delete chartInputs[key];
+ }
+ });
+
+ return chartInputs;
+ }
+
+ // Open configuration modal for a chart
+ editGadget(item: ShieldDashboardItem) {
+ console.log('Opening configuration modal for item:', item);
+ this.selectedItem = item;
+ this.modeledit = true; // Use modeledit instead of configModalOpen
+
+ // Initialize form with item data
+ this.configForm.patchValue({
+ charttitle: item.charttitle || '',
+ connection: item.connection || '',
+ table: item.table || '',
+ xAxis: item.xAxis || '',
+ yAxis: item.yAxis || [],
+ chartlegend: item.chartlegend !== undefined ? item.chartlegend : true,
+ showlabel: item.showlabel !== undefined ? item.showlabel : true
+ });
+
+ console.log('Form values after patch:', this.configForm.value);
+
+ // Load columns if table is set
+ if (item.table) {
+ this.getColumns(item.connection, item.table);
+ }
+
+ // Load drilldown columns if drilldown API URL is set
+ if (item.drilldownApiUrl) {
+ this.refreshDrilldownColumns();
+ }
+ }
+
+ // Save configuration changes
+ saveConfiguration() {
+ if (this.selectedItem) {
+ const formData = this.configForm.value;
+ console.log('Saving configuration:', formData);
+
+ // Update the selected item with form data
+ Object.assign(this.selectedItem, formData);
+
+ // Close the modal
+ this.modeledit = false; // Use modeledit instead of configModalOpen
+ this.selectedItem = null;
+ }
+ }
+
+ // Cancel configuration changes
+ cancelConfiguration() {
+ console.log('Canceling configuration');
+ this.modeledit = false; // Use modeledit instead of configModalOpen
+ this.selectedItem = null;
+ }
+
+ // Get tables from datastore
+ getTables(id: string) {
+ this.alertService.getTablefromstore(parseInt(id, 10)).subscribe(gateway => {
+ console.log(gateway);
+ // Handle table data
+ }, (error) => {
+ console.log(error);
+ });
+ }
+
+ // Get columns from API
+ getColumns(connectionId: string, table: string) {
+ const connId = connectionId ? parseInt(connectionId, 10) : undefined;
+ this.alertService.getColumnfromurl(table, connId).subscribe(data => {
+ console.log('Column data:', data);
+ this.columnData = data;
+ }, (error) => {
+ console.log(error);
+ this.columnData = [];
+ });
+ }
+
+ // Refresh drilldown columns
+ refreshDrilldownColumns() {
+ if (this.selectedItem && this.selectedItem.drilldownApiUrl) {
+ const connId = this.selectedItem.connection ? parseInt(this.selectedItem.connection, 10) : undefined;
+ this.alertService.getColumnfromurl(this.selectedItem.drilldownApiUrl, connId).subscribe(data => {
+ console.log('Drilldown column data:', data);
+ this.drilldownColumnData = data;
+ }, (error) => {
+ console.log('Error fetching drilldown columns:', error);
+ this.drilldownColumnData = [];
+ });
+ }
+ }
+
+ // Add base filter
+ addBaseFilter() {
+ if (this.selectedItem) {
+ if (!this.selectedItem.baseFilters) {
+ this.selectedItem.baseFilters = [];
+ }
+ this.selectedItem.baseFilters.push({ field: '', value: '' });
+ }
+ }
+
+ // Remove base filter
+ removeBaseFilter(index: number) {
+ if (this.selectedItem && this.selectedItem.baseFilters) {
+ this.selectedItem.baseFilters.splice(index, 1);
+ }
+ }
+
+ // Add drilldown filter
+ addDrilldownFilter() {
+ if (this.selectedItem) {
+ if (!this.selectedItem.drilldownFilters) {
+ this.selectedItem.drilldownFilters = [];
+ }
+ this.selectedItem.drilldownFilters.push({ field: '', value: '' });
+ }
+ }
+
+ // Remove drilldown filter
+ removeDrilldownFilter(index: number) {
+ if (this.selectedItem && this.selectedItem.drilldownFilters) {
+ this.selectedItem.drilldownFilters.splice(index, 1);
+ }
+ }
+
+ // Add drilldown layer
+ addDrilldownLayer() {
+ if (this.selectedItem) {
+ if (!this.selectedItem.drilldownLayers) {
+ this.selectedItem.drilldownLayers = [];
+ }
+ this.selectedItem.drilldownLayers.push({
+ enabled: false,
+ apiUrl: '',
+ xAxis: '',
+ yAxis: '',
+ parameter: '',
+ filters: []
+ });
+ }
+ }
+
+ // Remove drilldown layer
+ removeDrilldownLayer(index: number) {
+ if (this.selectedItem && this.selectedItem.drilldownLayers) {
+ this.selectedItem.drilldownLayers.splice(index, 1);
+ }
+ }
+
+ // Add layer filter
+ addLayerFilter(layerIndex: number) {
+ if (this.selectedItem && this.selectedItem.drilldownLayers) {
+ const layer = this.selectedItem.drilldownLayers[layerIndex];
+ if (layer) {
+ if (!layer.filters) {
+ layer.filters = [];
+ }
+ layer.filters.push({ field: '', value: '' });
+ }
+ }
+ }
+
+ // Refresh drilldown columns for a specific layer
+ refreshDrilldownLayerColumns(layerIndex: number) {
+ if (this.selectedItem && this.selectedItem.drilldownLayers && this.selectedItem.drilldownLayers[layerIndex]) {
+ const layer = this.selectedItem.drilldownLayers[layerIndex];
+ if (layer && layer.apiUrl) {
+ const connId = this.selectedItem.connection ? parseInt(this.selectedItem.connection, 10) : undefined;
+ this.alertService.getColumnfromurl(layer.apiUrl, connId).subscribe(data => {
+ console.log(`Drilldown layer ${layerIndex} column data:`, data);
+ // Store layer column data in the layerColumnData property
+ this.layerColumnData[layerIndex] = data;
+ }, (error) => {
+ console.log(`Error fetching drilldown layer ${layerIndex} columns:`, error);
+ this.layerColumnData[layerIndex] = [];
+ });
+ }
+ }
+ }
+
+ // Remove layer filter
+ removeLayerFilter(layerIndex: number, filterIndex: number) {
+ if (this.selectedItem && this.selectedItem.drilldownLayers) {
+ const layer = this.selectedItem.drilldownLayers[layerIndex];
+ if (layer && layer.filters) {
+ layer.filters.splice(filterIndex, 1);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.component.ts.fixed b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.component.ts.fixed
new file mode 100644
index 0000000..32eb2b3
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.component.ts.fixed
@@ -0,0 +1,223 @@
+import { Component, OnInit } from '@angular/core';
+import { GridsterConfig, GridsterItem } from 'angular-gridster2';
+
+interface ShieldDashboardItem extends GridsterItem {
+ chartType: string;
+ name: string;
+ id: number;
+ component?: any;
+}
+
+@Component({
+ selector: 'app-shield-dashboard',
+ templateUrl: './shield-dashboard.component.html',
+ styleUrls: ['./shield-dashboard.component.scss']
+})
+export class ShieldDashboardComponent implements OnInit {
+ options: GridsterConfig;
+ dashboard: Array;
+
+ // Keep track of deleted items
+ deletedItems: Array = [];
+
+ constructor() { }
+
+ ngOnInit(): void {
+ this.options = {
+ gridType: 'fit',
+ enableEmptyCellDrop: true,
+ emptyCellDropCallback: this.onDrop,
+ pushItems: true,
+ swap: true,
+ pushDirections: { north: true, east: true, south: true, west: true },
+ resizable: { enabled: true },
+ itemChangeCallback: this.itemChange.bind(this),
+ draggable: {
+ enabled: true,
+ ignoreContent: true,
+ dropOverItems: true,
+ dragHandleClass: 'drag-handler',
+ ignoreContentClass: 'no-drag',
+ },
+ displayGrid: 'always',
+ minCols: 10,
+ minRows: 10,
+ itemResizeCallback: this.itemResize.bind(this)
+ };
+
+ // Initialize the dashboard with empty canvas
+ this.dashboard = [];
+ }
+
+ onDrop = (event: any) => {
+ // Handle dropping new components onto the dashboard
+ console.log('Item dropped:', event);
+
+ // Get the component identifier from the drag event
+ const componentType = event.dataTransfer ? event.dataTransfer.getData('widgetIdentifier') : '';
+ console.log('Component type dropped:', componentType);
+
+ if (componentType) {
+ this.addComponentToDashboard(componentType);
+ } else {
+ console.log('No component type found in drag data');
+ }
+ }
+
+ // Add a new component to the dashboard
+ addComponentToDashboard(componentType: string) {
+ // Generate a new ID for the component
+ const newId = this.dashboard.length > 0 ? Math.max(...this.dashboard.map(item => item.id), 0) + 1 : 1;
+
+ let newItem: ShieldDashboardItem;
+
+ switch (componentType) {
+ case "bar_chart":
+ newItem = {
+ cols: 5,
+ rows: 6,
+ y: 0,
+ x: 0,
+ chartType: 'bar-chart',
+ name: 'Bar Chart',
+ id: newId
+ };
+ break;
+ case "doughnut_chart":
+ // For doughnut charts, we'll need to determine which one based on existing items
+ const donutCount = this.dashboard.filter(item => item.chartType === 'donut-chart').length;
+ if (donutCount % 2 === 0) {
+ newItem = {
+ cols: 5,
+ rows: 6,
+ y: 0,
+ x: 0,
+ chartType: 'donut-chart',
+ name: 'End Customer Donut',
+ id: newId
+ };
+ } else {
+ newItem = {
+ cols: 5,
+ rows: 6,
+ y: 0,
+ x: 0,
+ chartType: 'donut-chart',
+ name: 'Segment Penetration Donut',
+ id: newId
+ };
+ }
+ break;
+ case "map_chart":
+ newItem = {
+ cols: 5,
+ rows: 6,
+ y: 0,
+ x: 0,
+ chartType: 'map-chart',
+ name: 'Map Chart',
+ id: newId
+ };
+ break;
+ case "grid_view":
+ newItem = {
+ cols: 10,
+ rows: 6,
+ y: 0,
+ x: 0,
+ chartType: 'data-table',
+ name: 'Data Table',
+ id: newId
+ };
+ break;
+ case "to_do_chart":
+ newItem = {
+ cols: 5,
+ rows: 6,
+ y: 0,
+ x: 0,
+ chartType: 'deal-details',
+ name: 'Deal Details',
+ id: newId
+ };
+ break;
+ case "line_chart":
+ newItem = {
+ cols: 5,
+ rows: 6,
+ y: 0,
+ x: 0,
+ chartType: 'quarterwise-flow',
+ name: 'Quarterwise Flow',
+ id: newId
+ };
+ break;
+ default:
+ newItem = {
+ cols: 5,
+ rows: 6,
+ y: 0,
+ x: 0,
+ chartType: componentType,
+ name: componentType,
+ id: newId
+ };
+ }
+
+ // Add the new item to the dashboard
+ this.dashboard.push(newItem);
+ }
+
+ removeItem(item: ShieldDashboardItem) {
+ // Add the item to deleted items list before removing
+ this.deletedItems.push({...item});
+
+ // Remove the item from the dashboard
+ this.dashboard.splice(this.dashboard.indexOf(item), 1);
+ }
+
+ // Restore a deleted item
+ restoreItem(item: ShieldDashboardItem) {
+ // Remove from deleted items
+ this.deletedItems.splice(this.deletedItems.indexOf(item), 1);
+
+ // Add back to dashboard
+ this.dashboard.push(item);
+ }
+
+ // Clear all deleted items
+ clearDeletedItems() {
+ this.deletedItems = [];
+ }
+
+ itemChange() {
+ console.log('Item changed:', this.dashboard);
+ }
+
+ itemResize(item: any, itemComponent: any) {
+ console.log('Item resized:', item);
+ // Trigger a window resize event to notify charts to resize
+ window.dispatchEvent(new Event('resize'));
+ }
+
+ /**
+ * Extract only the relevant chart configuration properties to pass to chart components
+ * This prevents errors when trying to set properties that don't exist on the components
+ */
+ getChartInputs(item: any): any {
+ // Only pass properties that are relevant to chart components
+ const chartInputs = {
+ chartType: item.chartType,
+ name: item.name
+ };
+
+ // Remove undefined properties to avoid passing unnecessary data
+ Object.keys(chartInputs).forEach(key => {
+ if (chartInputs[key] === undefined) {
+ delete chartInputs[key];
+ }
+ });
+
+ return chartInputs;
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.module.ts b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.module.ts
new file mode 100644
index 0000000..9a048b8
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.module.ts
@@ -0,0 +1,44 @@
+import { NgModule, CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { ClarityModule } from '@clr/angular';
+import { GridsterModule } from 'angular-gridster2';
+import { NgChartsModule } from 'ng2-charts';
+import { DynamicModule } from 'ng-dynamic-component';
+
+import { ShieldDashboardRoutingModule } from './shield-dashboard-routing.module';
+import { ShieldDashboardComponent } from './shield-dashboard.component';
+import { SidebarFiltersComponent } from './components/sidebar-filters/sidebar-filters.component';
+import { BarChartComponent } from './components/bar-chart/bar-chart.component';
+import { DonutChartComponent } from './components/donut-chart/donut-chart.component';
+import { MapChartComponent } from './components/map-chart/map-chart.component';
+import { DataTableComponent } from './components/data-table/data-table.component';
+import { DealDetailsCardComponent } from './components/deal-details-card/deal-details-card.component';
+import { QuarterwiseFlowComponent } from './components/quarterwise-flow/quarterwise-flow.component';
+import { LoadingShimmerComponent } from './components/loading-shimmer/loading-shimmer.component';
+
+@NgModule({
+ declarations: [
+ ShieldDashboardComponent,
+ SidebarFiltersComponent,
+ BarChartComponent,
+ DonutChartComponent,
+ MapChartComponent,
+ DataTableComponent,
+ DealDetailsCardComponent,
+ QuarterwiseFlowComponent,
+ LoadingShimmerComponent
+ ],
+ imports: [
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ ClarityModule,
+ GridsterModule,
+ NgChartsModule,
+ DynamicModule,
+ ShieldDashboardRoutingModule
+ ],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
+})
+export class ShieldDashboardModule { }
\ 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..16a2e1d 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,307 @@
-
+
+
+
+
+
0">
+
Base Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
0 && currentDrilldownLevel > 0">
+
Drilldown Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+
Layer Filters
+
+
+
{{ filter.field }} ({{ filter.type || 'text' }})
+
+
+
+
+
+
+
+
+
+ Select {{ filter.field }}
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.field }}
+
+
+
+
+
+
+
+ Clear All Filters
+
+
+
+
+
+
+
+
0">
+
+
+
+
+
+
+
+
+ Drilldown Level: {{currentDrilldownLevel}}
+ 0">
+ (Clicked on: {{drilldownStack[drilldownStack.length - 1].clickedLabel}})
+
+
+
+
+
+
+
+
+
+
+
+
+ #
+ Item
+
+
+
+ {{i + 1}}
+ {{todo}}
+
+
+
+
+
+
+
+
+
+
+
+ Add
+
+
+
\ 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..598a4bc 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,289 @@
+// To Do Chart specific styles
+.to-do-chart-container {
+ padding: 20px;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.todo-table-container {
+ flex: 1;
+ overflow-y: auto;
+ max-height: 400px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ margin-bottom: 20px; // Add margin at the bottom for spacing
+
+ .todo-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 0;
+
+ th, td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid #ddd;
+ }
+
+ th {
+ background-color: #f2f2f2;
+ font-weight: bold;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ }
+
+ tr:hover {
+ background-color: #f5f5f5;
+ }
+
+ .c-col {
+ width: 50px;
+ }
+
+ // Add padding at the bottom of the table body
+ tbody {
+ tr:last-child td {
+ padding-bottom: 20px; // Extra padding for the last row
+ }
+ }
+ }
+}
+
+.add-todo-section {
+ display: flex;
+ gap: 10px;
+ margin-top: 15px;
+ padding: 20px; // Increased padding all around
+ border-top: 1px solid #eee;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ background-color: #f9f9f9;
+
+ .todo-input {
+ flex: 1;
+ padding: 12px; // Increased padding for better touch targets
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ font-size: 14px;
+ }
+
+ .add-button {
+ white-space: nowrap;
+ padding: 12px 20px; // Increased padding for better touch targets
+ }
+}
+
+.remove-button {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 8px; // Increased padding for better touch targets
+ border-radius: 3px;
+ color: #dc3545;
+
+ &:hover {
+ background-color: #e0e0e0;
+ }
+}
+
+.filter-section {
+ margin-bottom: 20px;
+ padding: 15px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ background-color: #f9f9f9;
+}
+
+.filter-group {
+ margin-bottom: 15px;
+
+ h4 {
+ margin-top: 0;
+ margin-bottom: 10px;
+ color: #333;
+ font-weight: 600;
+ }
+}
+
+.filter-controls {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 15px;
+}
+
+.filter-item {
+ flex: 1 1 300px;
+ min-width: 250px;
+ padding: 10px;
+ background: white;
+ border: 1px solid #e0e0e0;
+ border-radius: 4px;
+}
+
+.filter-label {
+ font-weight: 500;
+ margin-bottom: 8px;
+ color: #555;
+ font-size: 14px;
+}
+
+.filter-input {
+ width: 100%;
+
+ .filter-text-input,
+ .filter-select,
+ .filter-date {
+ width: 100%;
+ padding: 6px 12px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ font-size: 14px;
+ }
+
+ .filter-select {
+ height: 34px;
+ }
+}
+
+.multiselect-container {
+ position: relative;
+}
+
+.multiselect-display {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 6px 12px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ background: white;
+ cursor: pointer;
+ min-height: 34px;
+
+ .multiselect-label {
+ flex: 1;
+ font-size: 14px;
+ }
+
+ .multiselect-value {
+ color: #666;
+ font-size: 12px;
+ margin-right: 8px;
+ }
+
+ .dropdown-icon {
+ flex-shrink: 0;
+ transition: transform 0.2s ease;
+ }
+
+ &:hover {
+ border-color: #999;
+ }
+}
+
+.multiselect-dropdown {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ z-index: 1000;
+ background: white;
+ border: 1px solid #ccc;
+ border-top: none;
+ border-radius: 0 0 4px 4px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ max-height: 200px;
+ overflow-y: auto;
+
+ .checkbox-group {
+ padding: 8px;
+
+ .checkbox-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 0;
+
+ .checkbox-label {
+ margin: 0;
+ font-size: 14px;
+ cursor: pointer;
+ }
+ }
+ }
+}
+
+.date-range {
+ .date-input-group {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .date-separator {
+ margin: 0 5px;
+ color: #777;
+ }
+}
+
+.toggle {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .toggle-label {
+ margin: 0;
+ font-size: 14px;
+ cursor: pointer;
+ }
+}
+
+.filter-actions {
+ margin-top: 15px;
+ padding-top: 15px;
+ border-top: 1px solid #eee;
+
+ .btn {
+ font-size: 13px;
+ }
+}
+
+// New header row styling
+.header-row {
+ margin-bottom: 15px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #eee;
+
+ .chart-title {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: #333;
+ }
+}
+
+// Responsive design
+@media (max-width: 768px) {
+ .filter-controls {
+ flex-direction: column;
+ }
+
+ .filter-item {
+ min-width: 100%;
+ }
+
+ .header-row {
+ .chart-title {
+ font-size: 16px;
+ }
+ }
+
+ .add-todo-section {
+ flex-direction: column;
+ }
+
+ .to-do-chart-container {
+ padding: 15px; // Adjust padding for mobile
+ }
+}
\ 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 481fcca..1866f1d 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,27 +1,555 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { Dashboard3Service } from 'src/app/services/builder/dashboard3.service';
+import { FilterService } from '../../common-filter/filter.service';
+import { Subscription } from 'rxjs';
@Component({
selector: 'app-to-do-chart',
templateUrl: './to-do-chart.component.html',
styleUrls: ['./to-do-chart.component.scss']
})
-export class ToDoChartComponent implements OnInit {
-
- constructor() { }
+export class ToDoChartComponent implements OnInit, OnChanges {
+ @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; // 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(
+ 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.fetchToDoData();
+ })
+ );
+
+ // Initialize with default data
+ this.fetchToDoData();
}
+
+ 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;
+ const tableChanged = changes.table && !changes.table.firstChange;
+ const connectionChanged = changes.connection && !changes.connection.firstChange; // Add connection change detection
+
+ // Respond to input changes
+ if (xAxisChanged || yAxisChanged || tableChanged || connectionChanged) {
+ console.log('X or Y axis or table or connection changed, fetching new data');
+ // Only fetch data if xAxis, yAxis, table, or connection has changed (and it's not the first change)
+ this.fetchToDoData();
+ }
+ }
+
data: any;
todo: string;
- todoList = ['todo 1'];
+ todoList: string[] = [];
+
+ // 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 && this.xAxis) {
+ console.log('Fetching to-do data for:', { table: this.table, xAxis: this.xAxis, connection: this.connection });
+
+ // 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, 'todo', this.xAxis, '', this.connection, '', '', filterParams).subscribe(
+ (data: any) => {
+ console.log('Received to-do chart data:', data);
+ if (data === null) {
+ console.warn('To-do chart API returned null data. Check if the API endpoint is working correctly.');
+ this.todoList = [];
+ return;
+ }
+
+ // Handle the actual data structure returned by the API
+ if (data && data.chartLabels) {
+ // Use chartLabels as the todo items
+ this.todoList = data.chartLabels;
+ } else if (data && data.labels) {
+ // Fallback to labels if chartLabels is not available
+ this.todoList = data.labels;
+ } else {
+ console.warn('To-do chart received data does not have expected structure', data);
+ this.todoList = [];
+ }
+ },
+ (error) => {
+ console.error('Error fetching to-do chart data:', error);
+ this.todoList = [];
+ }
+ );
+ } else {
+ console.log('Missing required data for to-do chart:', { table: this.table, xAxis: this.xAxis });
+ // Initialize with default data if no table is specified
+ if (this.todoList.length === 0) {
+ this.todoList = ['Sample Task 1', 'Sample Task 2', 'Sample Task 3'];
+ }
+ }
+ }
public addTodo(todo: string) {
- this.todoList.push(todo);
-}
+ if (todo && todo.trim() !== '') {
+ this.todoList.push(todo.trim());
+ this.todo = ''; // Clear the input field
+ }
+ }
-public removeTodo(todoIx: number) {
+ public removeTodo(todoIx: number) {
if (this.todoList.length) {
this.todoList.splice(todoIx, 1);
}
-}
-}
+ }
+
+ // Navigate back to previous drilldown level
+ navigateBack(): void {
+ console.log('Navigating back, current stack:', this.drilldownStack);
+ console.log('Current level:', this.currentDrilldownLevel);
+
+ if (this.drilldownStack.length > 0) {
+ // Remove the last entry from the stack
+ const removedEntry = this.drilldownStack.pop();
+ console.log('Removed entry from stack:', removedEntry);
+
+ // Update the current drilldown level
+ this.currentDrilldownLevel = this.drilldownStack.length;
+ console.log('New level after pop:', this.currentDrilldownLevel);
+ console.log('Stack after pop:', this.drilldownStack);
+
+ if (this.drilldownStack.length > 0) {
+ // Fetch data for the previous level
+ console.log('Fetching data for previous level');
+ this.fetchToDoData();
+ } else {
+ // Back to base level
+ console.log('Back to base level, resetting to original data');
+ this.todoList = [...this.originalTodoList];
+ }
+ } else {
+ // Already at base level, reset to original data
+ console.log('Already at base level, resetting to original data');
+ this.todoList = [...this.originalTodoList];
+ }
+ }
+
+ // Initialize filter values with proper default values based on type
+ private initializeFilterValues(): void {
+ console.log('Initializing filter values');
+
+ // Initialize base filters
+ if (this.baseFilters) {
+ this.baseFilters.forEach(filter => {
+ if (filter.value === undefined || filter.value === null) {
+ switch (filter.type) {
+ case 'multiselect':
+ filter.value = [];
+ break;
+ case 'date-range':
+ filter.value = { start: null, end: null };
+ break;
+ case 'toggle':
+ filter.value = false;
+ break;
+ default:
+ filter.value = '';
+ }
+ }
+ });
+ }
+
+ // Initialize drilldown filters
+ if (this.drilldownFilters) {
+ this.drilldownFilters.forEach(filter => {
+ if (filter.value === undefined || filter.value === null) {
+ switch (filter.type) {
+ case 'multiselect':
+ filter.value = [];
+ break;
+ case 'date-range':
+ filter.value = { start: null, end: null };
+ break;
+ case 'toggle':
+ filter.value = false;
+ break;
+ default:
+ filter.value = '';
+ }
+ }
+ });
+ }
+
+ // Initialize layer filters
+ if (this.drilldownLayers) {
+ this.drilldownLayers.forEach(layer => {
+ if (layer.filters) {
+ layer.filters.forEach((filter: any) => {
+ if (filter.value === undefined || filter.value === null) {
+ switch (filter.type) {
+ case 'multiselect':
+ filter.value = [];
+ break;
+ case 'date-range':
+ filter.value = { start: null, end: null };
+ break;
+ case 'toggle':
+ filter.value = false;
+ break;
+ default:
+ filter.value = '';
+ }
+ }
+ });
+ }
+ });
+ }
+
+ console.log('Filter values initialized:', {
+ baseFilters: this.baseFilters,
+ drilldownFilters: this.drilldownFilters,
+ drilldownLayers: this.drilldownLayers
+ });
+ }
+
+ // Check if there are active filters
+ hasActiveFilters(): boolean {
+ return (this.baseFilters && this.baseFilters.length > 0) ||
+ (this.drilldownFilters && this.drilldownFilters.length > 0) ||
+ this.hasActiveLayerFilters();
+ }
+
+ // Check if there are active layer filters for current drilldown level
+ hasActiveLayerFilters(): boolean {
+ if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
+ const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ return layerIndex < this.drilldownLayers.length &&
+ this.drilldownLayers[layerIndex].filters &&
+ this.drilldownLayers[layerIndex].filters.length > 0;
+ }
+ return false;
+ }
+
+ // Get active layer filters for current drilldown level
+ getActiveLayerFilters(): any[] {
+ if (this.currentDrilldownLevel > 1 && this.drilldownLayers && this.drilldownLayers.length > 0) {
+ const layerIndex = this.currentDrilldownLevel - 2; // -2 because level 1 is base drilldown
+ if (layerIndex < this.drilldownLayers.length &&
+ this.drilldownLayers[layerIndex].filters) {
+ return this.drilldownLayers[layerIndex].filters;
+ }
+ }
+ return [];
+ }
+
+ // Get filter options for dropdown/multiselect filters
+ getFilterOptions(filter: any): string[] {
+ if (filter.options) {
+ if (Array.isArray(filter.options)) {
+ return filter.options;
+ } else if (typeof filter.options === 'string') {
+ return filter.options.split(',').map(opt => opt.trim()).filter(opt => opt);
+ }
+ }
+ return [];
+ }
+
+ // Check if an option is selected for multiselect filters
+ isOptionSelected(filter: any, option: string): boolean {
+ if (!filter.value) {
+ return false;
+ }
+
+ if (Array.isArray(filter.value)) {
+ return filter.value.includes(option);
+ }
+
+ return filter.value === option;
+ }
+
+ // Handle base filter changes
+ onBaseFilterChange(filter: any): void {
+ console.log('Base filter changed:', filter);
+ // Refresh data when filter changes
+ this.fetchToDoData();
+ }
+
+ // Handle drilldown filter changes
+ onDrilldownFilterChange(filter: any): void {
+ console.log('Drilldown filter changed:', filter);
+ // Refresh data when filter changes
+ this.fetchToDoData();
+ }
+
+ // Handle layer filter changes
+ onLayerFilterChange(filter: any): void {
+ console.log('Layer filter changed:', filter);
+ // Refresh data when filter changes
+ this.fetchToDoData();
+ }
+
+ // Handle multiselect changes
+ onMultiSelectChange(filter: any, option: string, event: any): void {
+ const checked = event.target.checked;
+
+ // Initialize filter.value as array if it's not already
+ if (!Array.isArray(filter.value)) {
+ filter.value = [];
+ }
+
+ if (checked) {
+ // Add option to array if not already present
+ if (!filter.value.includes(option)) {
+ filter.value.push(option);
+ }
+ } else {
+ // Remove option from array
+ filter.value = filter.value.filter((item: string) => item !== option);
+ }
+
+ // Refresh data when filter changes
+ this.fetchToDoData();
+ }
+
+ // Handle date range changes
+ onDateRangeChange(filter: any, dateRange: { start: string | null, end: string | null }): void {
+ filter.value = dateRange;
+ // Refresh data when filter changes
+ this.fetchToDoData();
+ }
+
+ // Handle toggle changes
+ onToggleChange(filter: any, checked: boolean): void {
+ filter.value = checked;
+ // Refresh data when filter changes
+ this.fetchToDoData();
+ }
+
+ // Toggle multiselect dropdown visibility
+ toggleMultiselect(filter: any, context: string): void {
+ const filterId = `${context}-${filter.field}`;
+ if (this.isMultiselectOpen(filter, context)) {
+ this.openMultiselects.delete(filterId);
+ } else {
+ // Close all other multiselects first
+ this.openMultiselects.clear();
+ this.openMultiselects.set(filterId, context);
+
+ // Add document click handler to close dropdown when clicking outside
+ this.addDocumentClickHandler();
+ }
+ }
+
+ // Add document click handler to close dropdowns when clicking outside
+ private addDocumentClickHandler(): void {
+ if (!this.documentClickHandler) {
+ this.documentClickHandler = (event: MouseEvent) => {
+ const target = event.target as HTMLElement;
+ // Check if click is outside any multiselect dropdown
+ if (!target.closest('.multiselect-container')) {
+ this.openMultiselects.clear();
+ this.removeDocumentClickHandler();
+ }
+ };
+
+ // Use setTimeout to ensure the click event that opened the dropdown doesn't immediately close it
+ setTimeout(() => {
+ document.addEventListener('click', this.documentClickHandler!);
+ }, 0);
+ }
+ }
+
+ // Remove document click handler
+ private removeDocumentClickHandler(): void {
+ if (this.documentClickHandler) {
+ document.removeEventListener('click', this.documentClickHandler);
+ this.documentClickHandler = null;
+ }
+ }
+
+ // Check if multiselect dropdown is open
+ isMultiselectOpen(filter: any, context: string): boolean {
+ const filterId = `${context}-${filter.field}`;
+ return this.openMultiselects.has(filterId);
+ }
+
+ // Get count of selected options for a multiselect filter
+ getSelectedOptionsCount(filter: any): number {
+ if (!filter.value) {
+ return 0;
+ }
+
+ if (Array.isArray(filter.value)) {
+ return filter.value.length;
+ }
+
+ return 0;
+ }
+
+ // Clear all filters
+ clearAllFilters(): void {
+ // Clear base filters
+ if (this.baseFilters) {
+ this.baseFilters.forEach(filter => {
+ if (filter.type === 'multiselect') {
+ filter.value = [];
+ } else if (filter.type === 'date-range') {
+ filter.value = { start: null, end: null };
+ } else if (filter.type === 'toggle') {
+ filter.value = false;
+ } else {
+ filter.value = '';
+ }
+ });
+ }
+
+ // Clear drilldown filters
+ if (this.drilldownFilters) {
+ this.drilldownFilters.forEach(filter => {
+ if (filter.type === 'multiselect') {
+ filter.value = [];
+ } else if (filter.type === 'date-range') {
+ filter.value = { start: null, end: null };
+ } else if (filter.type === 'toggle') {
+ filter.value = false;
+ } else {
+ filter.value = '';
+ }
+ });
+ }
+
+ // Clear layer filters
+ if (this.drilldownLayers) {
+ this.drilldownLayers.forEach(layer => {
+ if (layer.filters) {
+ layer.filters.forEach((filter: any) => {
+ if (filter.type === 'multiselect') {
+ filter.value = [];
+ } else if (filter.type === 'date-range') {
+ filter.value = { start: null, end: null };
+ } else if (filter.type === 'toggle') {
+ filter.value = false;
+ } else {
+ filter.value = '';
+ }
+ });
+ }
+ });
+ }
+
+ // Close all multiselect dropdowns
+ this.openMultiselects.clear();
+
+ // Refresh data
+ this.fetchToDoData();
+ }
+
+ ngOnDestroy(): void {
+ // Unsubscribe from all subscriptions to prevent memory leaks
+ this.subscriptions.forEach(subscription => subscription.unsubscribe());
+
+ // Remove document click handler if it exists
+ this.removeDocumentClickHandler();
+ }
+}
\ No newline at end of file
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..cf50dfa
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.html
@@ -0,0 +1,296 @@
+
+
+
0">
+
+
+ Back
+
+ Level: {{ currentDrilldownLevel }}
+
+
+
+
+
{{ charttitle }}
+
+
+
+
+
+
+
+ {{ showFilters ? 'Hide Filters' : 'Show Filters' }}
+
+
+
+
+
+
+
+
0 && showFilters">
+
Filters
+
+
+
+
+ {{ filter.field || 'Filter ' + (i + 1) }}
+
+
+
+
+
+ {{ filter.field || 'Filter ' + (i + 1) }}
+
+ Select {{ filter.field || 'value' }}
+
+ {{ option }}
+
+
+
+
+
+
+
{{ filter.field || 'Filter ' + (i + 1) }}
+
+
+ Select {{ filter.field || 'options' }}
+
+ {{ filter.value }}
+
+ 0">
+ {{ filter.value.length }} selected
+
+
+
+
+
+
+
+
+
+
+
+
{{ filter.field || 'Filter ' + (i + 1) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
0 && currentDrilldownLevel > 0 && showFilters">
+
Drilldown Filters
+
+
+
+
+ {{ filter.field || 'Filter ' + (i + 1) }}
+
+
+
+
+
+ {{ filter.field || 'Filter ' + (i + 1) }}
+
+ Select {{ filter.field || 'value' }}
+
+ {{ option }}
+
+
+
+
+
+
+
{{ filter.field || 'Filter ' + (i + 1) }}
+
+
+ Select {{ filter.field || 'options' }}
+
+ {{ filter.value }}
+
+ 0">
+ {{ filter.value.length }} selected
+
+
+
+
+
+
+
+
+
+
+
+
{{ filter.field || 'Filter ' + (i + 1) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Layer Filters
+
+
+
+
+ {{ filter.field || 'Filter ' + (i + 1) }}
+
+
+
+
+
+ {{ filter.field || 'Filter ' + (i + 1) }}
+
+ Select {{ filter.field || 'value' }}
+
+ {{ option }}
+
+
+
+
+
+
+
{{ filter.field || 'Filter ' + (i + 1) }}
+
+
+ Select {{ filter.field || 'options' }}
+
+ {{ filter.value }}
+
+ 0">
+ {{ filter.value.length }} selected
+
+
+
+
+
+
+
+
+
+
+
+
{{ filter.field || 'Filter ' + (i + 1) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Clear All Filters
+
+
+
+
+
+
+
No data available for the selected filters.
+
Retry
+
+
+
+
+
+
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..4cd6c2e
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.scss
@@ -0,0 +1,307 @@
+.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: 400px;
+ padding: 10px;
+}
+
+.chart-canvas-container {
+ position: relative;
+ height: 100%;
+ width: 100%;
+ padding: 15px;
+ box-sizing: border-box;
+
+ canvas {
+ display: block;
+ max-width: 100%;
+ max-height: 100%;
+ }
+}
+
+.filter-toggle-icon {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ padding: 5px;
+ cursor: pointer;
+
+ clr-icon {
+ transition: color 0.3s ease;
+
+ &:hover {
+ color: #007cba !important;
+ }
+ }
+
+ span {
+ margin-left: 5px;
+ font-size: 12px;
+ color: #666;
+ transition: color 0.3s ease;
+ }
+
+ &:hover span {
+ color: #007cba;
+ }
+}
+
+.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); }
+}
+
+@media (max-width: 768px) {
+ .filters-container {
+ flex-direction: column;
+ }
+
+ .filter-item {
+ min-width: 100%;
+ }
+
+ .chart-wrapper {
+ min-height: 250px;
+ }
+
+ .chart-canvas-container {
+ padding: 10px;
+ }
+}
\ 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..b877eb8
--- /dev/null
+++ b/frontend/angular-clarity-master/src/app/modules/main/builder/dashboardnew/gadgets/unified-chart/unified-chart.component.ts
@@ -0,0 +1,2096 @@
+import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, ViewChild, ElementRef, Renderer2 } 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';
+import { DynamicChartLoaderService } from '../../chart-config/dynamic-chart-loader.service';
+
+@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 = {};
+
+ // Filter visibility toggle
+ showFilters: boolean = false;
+
+ // 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;
+
+ // Dynamic template properties
+ dynamicTemplate: string = '';
+ dynamicStyles: string = '';
+ dynamicOptions: any = null;
+
+ // Properties to hold extracted values from dynamic template
+ extractedChartType: string = '';
+ extractedDatasetsBinding: string = '';
+ extractedLabelsBinding: string = '';
+ extractedOptionsBinding: string = '';
+ extractedLegendBinding: string = '';
+ extractedChartClickBinding: string = '';
+ extractedChartHoverBinding: string = '';
+
+ // Add setter to log when dynamicTemplate changes
+ setDynamicTemplate(value: string) {
+ console.log('Setting dynamic template:', value);
+ this.dynamicTemplate = value;
+
+ // Extract values from the dynamic template
+ this.extractTemplateValues(value);
+
+ // Apply dynamic options if they were extracted
+ if (this.dynamicOptions) {
+ this.mergeDynamicOptions();
+ }
+
+ // Apply dynamic styles if they were extracted
+ if (this.dynamicStyles) {
+ this.applyDynamicStyles();
+ }
+
+ // Trigger change detection to ensure the template is rendered
+ setTimeout(() => {
+ console.log('Dynamic template updated in DOM');
+ // Check if the dynamic template container exists
+ const dynamicContainer = this.el.nativeElement.querySelector('.dynamic-template-container');
+ console.log('Dynamic template container:', dynamicContainer);
+ if (dynamicContainer) {
+ console.log('Dynamic container innerHTML:', dynamicContainer.innerHTML);
+ }
+ // Check if the canvas element exists in the DOM
+ const canvasElements = this.el.nativeElement.querySelectorAll('canvas');
+ console.log('Canvas elements found in DOM:', canvasElements.length);
+ if (canvasElements.length > 0) {
+ console.log('First canvas element:', canvasElements[0]);
+ // Check if it has the baseChart directive processed
+ const firstCanvas = canvasElements[0];
+ console.log('Canvas has baseChart directive processed:', firstCanvas.classList.contains('chartjs-render-monitor'));
+ } else {
+ console.log('No canvas elements found - checking if template was inserted but not processed');
+ // Check if there's HTML content in the dynamic container
+ if (dynamicContainer) {
+ const htmlContent = dynamicContainer.innerHTML;
+ console.log('HTML content in dynamic container:', htmlContent);
+ if (htmlContent && htmlContent.includes('canvas')) {
+ console.log('Canvas tag found in HTML but not processed by Angular');
+ }
+ }
+ }
+ }, 100);
+ }
+
+ // Extract values from dynamic template HTML
+ private extractTemplateValues(template: string): void {
+ console.log('Extracting values from template:', template);
+
+ // Reset extracted values
+ this.extractedChartType = this.chartType || '';
+ this.extractedDatasetsBinding = 'chartData';
+ this.extractedLabelsBinding = 'chartLabels';
+ this.extractedOptionsBinding = 'chartOptions';
+ this.extractedLegendBinding = 'chartLegend';
+ this.extractedChartClickBinding = 'chartClicked($event)';
+ this.extractedChartHoverBinding = 'chartHovered($event)';
+
+ if (!template) {
+ console.log('No template to extract values from');
+ return;
+ }
+
+ // Parse the template to extract bindings
+ // Look for [chartType] binding
+ const chartTypeMatch = template.match(/\[chartType\]="([^"]+)"/);
+ if (chartTypeMatch && chartTypeMatch[1]) {
+ this.extractedChartType = chartTypeMatch[1];
+ console.log('Extracted chartType binding:', this.extractedChartType);
+ }
+
+ // Look for [datasets] binding
+ const datasetsMatch = template.match(/\[datasets\]="([^"]+)"/);
+ if (datasetsMatch && datasetsMatch[1]) {
+ this.extractedDatasetsBinding = datasetsMatch[1];
+ console.log('Extracted datasets binding:', this.extractedDatasetsBinding);
+ }
+
+ // Look for [labels] binding
+ const labelsMatch = template.match(/\[labels\]="([^"]+)"/);
+ if (labelsMatch && labelsMatch[1]) {
+ this.extractedLabelsBinding = labelsMatch[1];
+ console.log('Extracted labels binding:', this.extractedLabelsBinding);
+ }
+
+ // Look for [options] binding
+ const optionsMatch = template.match(/\[options\]="([^"]+)"/);
+ if (optionsMatch && optionsMatch[1]) {
+ this.extractedOptionsBinding = optionsMatch[1];
+ console.log('Extracted options binding:', this.extractedOptionsBinding);
+ }
+
+ // Look for [legend] binding
+ const legendMatch = template.match(/\[legend\]="([^"]+)"/);
+ if (legendMatch && legendMatch[1]) {
+ this.extractedLegendBinding = legendMatch[1];
+ console.log('Extracted legend binding:', this.extractedLegendBinding);
+ }
+
+ // Look for (chartClick) binding
+ const chartClickMatch = template.match(/\(chartClick\)="([^"]+)"/);
+ if (chartClickMatch && chartClickMatch[1]) {
+ this.extractedChartClickBinding = chartClickMatch[1];
+ console.log('Extracted chartClick binding:', this.extractedChartClickBinding);
+ }
+
+ // Look for (chartHover) binding
+ const chartHoverMatch = template.match(/\(chartHover\)="([^"]+)"/);
+ if (chartHoverMatch && chartHoverMatch[1]) {
+ this.extractedChartHoverBinding = chartHoverMatch[1];
+ console.log('Extracted chartHover binding:', this.extractedChartHoverBinding);
+ }
+
+ // Extract CSS styles if present in the template
+ const styleMatch = template.match(/