diff --git a/package.json b/package.json index 8984a27..cfbc888 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@types/uuid": "^8.3.1", "ace-builds": "^1.11.2", "angular-gridster2": "^12.1.0", + "apexcharts": "^6.10.0", "crypto-js": "^4.0.0", "dom-to-image": "^2.6.0", "howler": "^2.2.3", @@ -72,6 +73,7 @@ "libphonenumber-js": "^1.9.22", "md5-typescript": "^1.0.5", "moment": "^2.29.1", + "ng-apexcharts": "^3.0.0", "ng-packagr": "^12.0.0", "ngx-device-detector": "^2.1.1", "ngx-markdown": "^12.0.1", diff --git a/projects/x-framework-components/src/lib/x-card/x-card.typings.ts b/projects/x-framework-components/src/lib/x-card/x-card.typings.ts index 3f46076..13887b8 100644 --- a/projects/x-framework-components/src/lib/x-card/x-card.typings.ts +++ b/projects/x-framework-components/src/lib/x-card/x-card.typings.ts @@ -2,14 +2,12 @@ import { Input, Inject, NgZone, - Output, Component, Renderer2, ElementRef, Injectable, - EventEmitter, - ChangeDetectorRef, TemplateRef, + ChangeDetectorRef, } from '@angular/core'; import { XStandardType, diff --git a/projects/x-framework-components/src/lib/x-chart/x-chart.component.html b/projects/x-framework-components/src/lib/x-chart/x-chart.component.html new file mode 100644 index 0000000..8e7d5c3 --- /dev/null +++ b/projects/x-framework-components/src/lib/x-chart/x-chart.component.html @@ -0,0 +1,44 @@ + + + + + + + +
+ +
+ + +
+ +
+
+
+ + + + +
+
+
+
diff --git a/projects/x-framework-components/src/lib/x-chart/x-chart.component.scss b/projects/x-framework-components/src/lib/x-chart/x-chart.component.scss new file mode 100644 index 0000000..ddc4b4d --- /dev/null +++ b/projects/x-framework-components/src/lib/x-chart/x-chart.component.scss @@ -0,0 +1,10 @@ +.x-chart-wrapper { + width: 100%; + height: 100%; + min-height: 300px; // ارتفاع پیش‌فرض +} + +.x-chart-container { + width: 100%; + height: 100%; +} diff --git a/projects/x-framework-components/src/lib/x-chart/x-chart.component.ts b/projects/x-framework-components/src/lib/x-chart/x-chart.component.ts new file mode 100644 index 0000000..6280a34 --- /dev/null +++ b/projects/x-framework-components/src/lib/x-chart/x-chart.component.ts @@ -0,0 +1,282 @@ +import { + Component, + ViewChild, + ElementRef, + ChangeDetectionStrategy, +} from '@angular/core'; +import { Subscription } from 'rxjs'; +import { isNullOrUndefined, nameof } from 'x-framework-core'; +import ApexCharts, { ApexAxisChartSeries, ApexOptions } from 'apexcharts'; +import { XChartAction, XChartContainerComponent } from './x-chart.typings'; + +@Component({ + selector: 'x-chart', + templateUrl: './x-chart.component.html', + styleUrls: ['./x-chart.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class XChartComponent extends XChartContainerComponent { + // + //#region Props ... + // + @ViewChild('chartContainer', { static: true }) + chartContainer!: ElementRef; + + // + private chartInstance: ApexCharts | null = null; + + // + private ACTION_PROVIDER_SUBSCRIPTION: Subscription; + //#endregion + + // + //#region LifeCycles ... + afterViewInit() { + super.afterViewInit(); + + // + this.initializeComponent(); + } + + async onChange(changeKeys: string[]) { + super.onChange(changeKeys); + + // + // Auto Update ... + const isAutoUpdateChanged = changeKeys.includes( + nameof('autoUpdate'), + ); + if (isAutoUpdateChanged) { + // + if (isNullOrUndefined(this.autoUpdate)) { + this.autoUpdate = this.propertyProvider.autoUpdate; + } + } + + // + // Check Auto Update ... + var autoUpdate = await this.getValueAsync(this.autoUpdate); + if (!autoUpdate || isNullOrUndefined(this.chartInstance)) { + return; + } + + // + // Options Changed ... + const isOptionsChanged = changeKeys.includes( + nameof('options'), + ); + if (isOptionsChanged) { + // + if (isNullOrUndefined(this.options)) { + this.options = this.propertyProvider.options; + } + + // + const options = await this.getValueAsync(this.options); + await this.chartInstance.updateOptions(options); + } + + // + // Series Changed ... + const isSeriesChanged = changeKeys.includes( + nameof('series'), + ); + if (isSeriesChanged) { + // + if (isNullOrUndefined(this.series)) { + this.series = this.propertyProvider.series; + } + + // + const series = await this.getValueAsync(this.series); + await this.chartInstance.updateSeries(series); + } + + // + // Action Provider Changed ... + const isActionProviderChanged = changeKeys.includes( + nameof('actionProvider'), + ); + if (isActionProviderChanged) { + this.registerActionProvider(); + } + + // + this.detectChanges(); + } + + onDestroy() { + super.onDestroy(); + + // + if (!isNullOrUndefined(this.chartInstance)) { + // + this.chartInstance.destroy(); + this.chartInstance = null; + } + + // + this.unregisterActionProvider(); + } + //#endregion + + // + //#region Private ... + private async initializeComponent() { + // + // Reading Requirement Properties ... + const chartId = await this.getValueAsync(this.uuid); + const series = await this.getValueAsync(this.series); + const options = await this.getValueAsync(this.options); + + // + // Validate Options ... + if (isNullOrUndefined(options)) { + return; + } + + // + const computedStyle = getComputedStyle(document.documentElement); + const primaryColor = + computedStyle.getPropertyValue('--x-color-primary').trim() || '#008ffb'; + const textColor = + computedStyle.getPropertyValue('--x-color-dark-contrast').trim() || + '#373d3f'; + const gridColor = + computedStyle.getPropertyValue('--x-color-light-shade').trim() || + '#e0e0e0'; + const finalOptions: ApexOptions = { + ...options, + series: series as any, + chart: { + ...options.chart, + id: chartId, + type: options.chart?.type || 'line', + foreColor: textColor, + events: { + ...options.chart?.events, + mounted: (chartContext, config) => { + this.chartRendered.emit(); + options.chart?.events?.mounted?.(chartContext, config); + }, + dataPointSelection: (event, chartContext, config) => { + this.dataPointSelected.emit({ event, chartContext, config }); + options.chart?.events?.dataPointSelection?.( + event, + chartContext, + config, + ); + }, + zoomed: (chartContext, { xaxis, yaxis }) => { + this.zoomed.emit({ xaxis, yaxis }); + options.chart?.events?.zoomed?.(chartContext, { xaxis, yaxis }); + }, + }, + }, + theme: { + ...options.theme, + palette: options.theme?.palette || 'palette1', + }, + colors: options.colors || [primaryColor], + grid: { + ...options.grid, + borderColor: gridColor, + }, + xaxis: { + ...options.xaxis, + labels: { + ...options.xaxis?.labels, + style: { colors: textColor }, + }, + }, + yaxis: { + ...options.yaxis, + labels: { + ...(options.yaxis as any).labels, + style: { colors: textColor }, + }, + }, + }; + + // + this.chartInstance = new ApexCharts( + this.chartContainer.nativeElement, + finalOptions, + ); + + // + await this.chartInstance.render(); + } + + private registerActionProvider() { + // + this.unregisterActionProvider(); + + // + if (!this.actionProvider) { + this.actionProvider = this.propertyProvider.actionProvider; + } + + // + this.ACTION_PROVIDER_SUBSCRIPTION = this.actionProvider + .asObservable() + .subscribe(async (model) => { + // + switch (model.action) { + // + case XChartAction.Refresh: + this.detectChanges(); + break; + + // + case XChartAction.Destroy: + this.onDestroy(); + break; + + // + case XChartAction.UpdateSeries: + // + if ( + !isNullOrUndefined(model.payload) || + !isNullOrUndefined(this.chartInstance) + ) { + // + this.series = await this.getValueAsync( + model.payload as ApexAxisChartSeries, + ); + await this.chartInstance.updateSeries(this.series); + this.detectChanges(); + } + break; + + // + case XChartAction.UpdateOptions: + // + if ( + !isNullOrUndefined(model.payload) || + !isNullOrUndefined(this.chartInstance) + ) { + // + this.options = await this.getValueAsync( + model.payload as ApexOptions, + ); + await this.chartInstance.updateOptions(this.options); + this.detectChanges(); + } + break; + } + }); + } + + private unregisterActionProvider() { + // + if (!this.ACTION_PROVIDER_SUBSCRIPTION) { + return; + } + + // + this.ACTION_PROVIDER_SUBSCRIPTION.unsubscribe(); + this.ACTION_PROVIDER_SUBSCRIPTION = null; + } + //#endregion +} diff --git a/projects/x-framework-components/src/lib/x-chart/x-chart.module.ts b/projects/x-framework-components/src/lib/x-chart/x-chart.module.ts new file mode 100644 index 0000000..c63a6af --- /dev/null +++ b/projects/x-framework-components/src/lib/x-chart/x-chart.module.ts @@ -0,0 +1,29 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { XCardModule } from '../x-card/x-card.module'; +import { XIconModule } from '../x-icon/x-icon.module'; +import { XChartComponent } from './x-chart.component'; +import { XSlotterModule } from '../x-slotter/x-slotter.module'; +import { XBaseComponentModule } from '../modules/x-base-component.module'; + +@NgModule({ + providers: [], + entryComponents: [], + declarations: [XChartComponent], + imports: [ + XCardModule, + XIconModule, + CommonModule, + XSlotterModule, + XBaseComponentModule, + ], + exports: [ + XCardModule, + XIconModule, + CommonModule, + XSlotterModule, + XChartComponent, + XBaseComponentModule, + ], +}) +export class XChartModule {} diff --git a/projects/x-framework-components/src/lib/x-chart/x-chart.typings.ts b/projects/x-framework-components/src/lib/x-chart/x-chart.typings.ts new file mode 100644 index 0000000..fdb9eac --- /dev/null +++ b/projects/x-framework-components/src/lib/x-chart/x-chart.typings.ts @@ -0,0 +1,194 @@ +import { + ApexOptions, + ApexAxisChartSeries, + ApexNonAxisChartSeries, +} from 'apexcharts'; +import { + Input, + Inject, + NgZone, + Output, + Component, + Renderer2, + ElementRef, + Injectable, + EventEmitter, + ChangeDetectorRef, +} from '@angular/core'; +import { + IXComponentCardContainer, + XComponentCardContainer, + XComponentCardContainerComponent, +} from '../x-card/x-card.typings'; +import { concatMap, map } from 'rxjs/operators'; +import { forkJoin, Observable, Subject } from 'rxjs'; +import { ViewportRuler } from '@angular/cdk/overlay'; +import { XManagerService } from 'x-framework-services'; +import { XOneOrManyType, XStandardType } from 'x-framework-core'; +import { XBaseActionModel } from '../classes/x-base-action.model'; +import { X_FRAMEWORK_COMPONENTS_CONFIG } from '../tokens/x-injectable-tokens'; +import { XFrameworkComponentsConfig } from '../config/x-framework-components.config'; + +export enum XChartAction { + Refresh = 'refresh', + Destroy = 'destroy', + UpdateSeries = 'update_series', + UpdateOptions = 'update_options', +} + +export type XChartActionIdentifier = XChartAction | string; + +export interface XChartActionModel extends XBaseActionModel {} + +export interface IXChartContainer extends IXComponentCardContainer { + // + // Commons ... + actionProvider: Subject; + cssClass: XStandardType>; + + // + // Chart Props ... + autoUpdate: XStandardType; + options: XStandardType; + series: XStandardType; + + // + // Events ... + chartRendered: EventEmitter; + dataPointSelected: EventEmitter; + zoomed: EventEmitter; +} + +@Injectable({ + providedIn: 'root', +}) +export class XChartContainer + extends XComponentCardContainer + implements Readonly +{ + // + readonly chartId: XStandardType; + readonly options: XStandardType; + readonly autoUpdate: XStandardType = true; + readonly cssClass: XStandardType>; + readonly actionProvider = new Subject(); + readonly series: XStandardType; + + // + readonly zoomed = new EventEmitter(); + readonly chartRendered = new EventEmitter(); + readonly dataPointSelected = new EventEmitter(); +} + +@Injectable() +@Component({ template: `` }) +export abstract class XChartContainerComponent + extends XComponentCardContainerComponent + implements IXChartContainer +{ + // + //#region Props ... + @Input() + actionProvider: Subject = + this.propertyProvider.actionProvider; + + @Input() + cssClass: XStandardType> = + this.propertyProvider.cssClass; + + @Input() + options: XStandardType = this.propertyProvider.options; + + @Input() + series: XStandardType = + this.propertyProvider.series; + + @Input() + autoUpdate: XStandardType = this.propertyProvider.autoUpdate; + + // + // Events ... + @Output() + chartRendered: EventEmitter = new EventEmitter(); + + @Output() + dataPointSelected: EventEmitter = new EventEmitter(); + + @Output() + zoomed: EventEmitter = new EventEmitter(); + //#endregion + + // + //#region Constructor ... + constructor( + @Inject(X_FRAMEWORK_COMPONENTS_CONFIG) + public config: XFrameworkComponentsConfig, + public managerService: XManagerService, + public element: ElementRef, + public renderer: Renderer2, + public ruler: ViewportRuler, + public zone: NgZone, + public changeDetector: ChangeDetectorRef, + protected propertyProvider: XChartContainer, + ) { + super( + config, + managerService, + element, + renderer, + ruler, + zone, + changeDetector, + propertyProvider, + ); + } + //#endregionّ + + // + //#region Abstract ... + public extractProps( + container?: IXChartContainer, + ): Observable { + return super + .extractProps(container ? container : this) + .pipe( + concatMap((superData) => { + return forkJoin({ + // + cssClass: this.getArrayValue( + container ? container.cssClass : this.cssClass, + ), + + // + options: this.getValue( + container ? container.options : this.options, + ), + series: this.getValue(container ? container.series : this.series), + autoUpdate: this.getValue( + container ? container.autoUpdate : this.autoUpdate, + ), + }).pipe( + map((data) => { + return { + superData, + data, + }; + }), + ); + }), + ) + .pipe( + map((info) => ({ + ...info.data, + ...info.superData, + actionProvider: container + ? container.actionProvider + : this.actionProvider, + chartRendered: this.chartRendered, + dataPointSelected: this.dataPointSelected, + zoomed: this.zoomed, + })), + ); + } + //#endregion +} diff --git a/projects/x-framework-components/src/lib/x-framework-components.module.ts b/projects/x-framework-components/src/lib/x-framework-components.module.ts index ee2ab6c..f8368a7 100644 --- a/projects/x-framework-components/src/lib/x-framework-components.module.ts +++ b/projects/x-framework-components/src/lib/x-framework-components.module.ts @@ -18,6 +18,7 @@ import { XTableModule } from './x-table/x-table.module'; import { XFrameworkCoreModule } from 'x-framework-core'; import { XThemeModule } from './x-theme/x-theme.module'; import { XColorModule } from './x-color/x-color.module'; +import { XChartModule } from './x-chart/x-chart.module'; import { XSliderModule } from './x-slider/x-slider.module'; import { XButtonModule } from './x-button/x-button.module'; import { XDialogsModule } from './dialogs/x-dialog.module'; @@ -77,6 +78,7 @@ import { XDescriptionListModule } from './x-description-list/x-description-list. XListModule, XTabsModule, XGridModule, + XChartModule, XFrameModule, XEmptyModule, XColorModule, @@ -136,6 +138,7 @@ import { XDescriptionListModule } from './x-description-list/x-description-list. XTabsModule, XGridModule, XEmptyModule, + XChartModule, XFrameModule, XColorModule, XTableModule, @@ -166,4 +169,4 @@ import { XDescriptionListModule } from './x-description-list/x-description-list. XDescriptionListModule, ], }) -export class XFrameworkComponentsModule { } +export class XFrameworkComponentsModule {} diff --git a/projects/x-framework-components/src/public-api.ts b/projects/x-framework-components/src/public-api.ts index b0585c3..4c2aa58 100644 --- a/projects/x-framework-components/src/public-api.ts +++ b/projects/x-framework-components/src/public-api.ts @@ -17,6 +17,7 @@ export * from './lib/x-grid/x-grid.module'; export * from './lib/x-page/x-page.module'; export * from './lib/x-tabs/x-tabs.module'; export * from './lib/x-tree/x-tree.module'; +export * from './lib/x-chart/x-chart.module'; export * from './lib/x-theme/x-theme.module'; export * from './lib/x-table/x-table.module'; export * from './lib/x-frame/x-frame.module'; @@ -62,6 +63,7 @@ export * from './lib/x-grid/x-grid.component'; export * from './lib/x-page/x-page.component'; export * from './lib/x-tabs/x-tabs.component'; export * from './lib/x-tree/x-tree.component'; +export * from './lib/x-chart/x-chart.component'; export * from './lib/x-color/x-color.component'; export * from './lib/x-empty/x-empty.component'; export * from './lib/x-frame/x-frame.component'; @@ -123,6 +125,7 @@ export * from './lib/x-tree/x-tree.typings'; export * from './lib/x-empty/x-empty.typings'; export * from './lib/x-frame/x-frame.typings'; export * from './lib/x-page/x-page.container'; +export * from './lib/x-chart/x-chart.typings'; export * from './lib/x-color/x-color.typings'; export * from './lib/x-theme/x-theme-typings'; export * from './lib/x-table/x-table.typings'; diff --git a/tsconfig.json b/tsconfig.json index ca50783..0815ceb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "moduleResolution": "node", "experimentalDecorators": true, "noFallthroughCasesInSwitch": true, + "allowSyntheticDefaultImports": true, "suppressImplicitAnyIndexErrors": true, "forceConsistentCasingInFileNames": true, "lib": [