first implementation of x-chart component ...

This commit is contained in:
2026-08-20 15:48:57 +03:30
parent 8ab1322222
commit 203c09a0e8
10 changed files with 570 additions and 4 deletions
+2
View File
@@ -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",
@@ -2,14 +2,12 @@ import {
Input,
Inject,
NgZone,
Output,
Component,
Renderer2,
ElementRef,
Injectable,
EventEmitter,
ChangeDetectorRef,
TemplateRef,
ChangeDetectorRef,
} from '@angular/core';
import {
XStandardType,
@@ -0,0 +1,44 @@
<!-- Component Template -->
<ng-container *ngIf="getValue(wrapWithCard) | async; else componentsRef">
<x-card
class="x-card"
[showActions]="true"
[color]="getValue(cardColor) | async"
[ngClass]="getValue(cssClass) | async"
[isInPage]="getValue(isInPage) | async"
[showHeader]="
(notEmptyValue(cardTitle) | async) ||
(notEmptyValue(cardSubTitle) | async)
"
[subTitle]="getResourceValue(cardSubTitle)"
[title]="getResourceValue(cardTitle) | async"
[showFooter]="getValue(cardShowFooter) | async"
[showActions]="getValue(cardShowActions) | async"
[foregroundColor]="getValue(cardForegroundColor) | async"
>
<!-- Content -->
<ng-container *ngTemplateOutlet="componentsRef"></ng-container>
<!-- Actions -->
<div class="x-actions" [ngClass]="getValue(cssClass) | async" actions>
<ng-content select="[actions]"></ng-content>
</div>
<!-- Footer -->
<div footer *ngIf="getValue(cardShowFooter) | async">
<ng-content select="[footer]"></ng-content>
</div>
</x-card>
</ng-container>
<!-- Component's itSelf Template -->
<ng-template #componentsRef>
<!-- Component Box ... -->
<div
class="x-chart x-wrapper"
[class.mobile]="isMobileUi$ | async"
[ngClass]="getValue(cssClass) | async"
>
<div #chartContainer class="x-chart-container"></div>
</div>
</ng-template>
@@ -0,0 +1,10 @@
.x-chart-wrapper {
width: 100%;
height: 100%;
min-height: 300px; // ارتفاع پیش‌فرض
}
.x-chart-container {
width: 100%;
height: 100%;
}
@@ -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<XChartComponent>('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<XChartComponent>('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<XChartComponent>('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<XChartComponent>('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
}
@@ -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 {}
@@ -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<XChartActionIdentifier> {}
export interface IXChartContainer extends IXComponentCardContainer {
//
// Commons ...
actionProvider: Subject<XChartActionModel>;
cssClass: XStandardType<XOneOrManyType<string>>;
//
// Chart Props ...
autoUpdate: XStandardType<boolean>;
options: XStandardType<ApexOptions>;
series: XStandardType<ApexAxisChartSeries | ApexNonAxisChartSeries>;
//
// Events ...
chartRendered: EventEmitter<void>;
dataPointSelected: EventEmitter<any>;
zoomed: EventEmitter<any>;
}
@Injectable({
providedIn: 'root',
})
export class XChartContainer
extends XComponentCardContainer
implements Readonly<IXChartContainer>
{
//
readonly chartId: XStandardType<string>;
readonly options: XStandardType<ApexOptions>;
readonly autoUpdate: XStandardType<boolean> = true;
readonly cssClass: XStandardType<XOneOrManyType<string>>;
readonly actionProvider = new Subject<XChartActionModel>();
readonly series: XStandardType<ApexAxisChartSeries | ApexNonAxisChartSeries>;
//
readonly zoomed = new EventEmitter<any>();
readonly chartRendered = new EventEmitter<void>();
readonly dataPointSelected = new EventEmitter<any>();
}
@Injectable()
@Component({ template: `` })
export abstract class XChartContainerComponent
extends XComponentCardContainerComponent
implements IXChartContainer
{
//
//#region Props ...
@Input()
actionProvider: Subject<XChartActionModel> =
this.propertyProvider.actionProvider;
@Input()
cssClass: XStandardType<XOneOrManyType<string>> =
this.propertyProvider.cssClass;
@Input()
options: XStandardType<ApexOptions> = this.propertyProvider.options;
@Input()
series: XStandardType<ApexAxisChartSeries | ApexNonAxisChartSeries> =
this.propertyProvider.series;
@Input()
autoUpdate: XStandardType<boolean> = this.propertyProvider.autoUpdate;
//
// Events ...
@Output()
chartRendered: EventEmitter<void> = new EventEmitter();
@Output()
dataPointSelected: EventEmitter<any> = new EventEmitter();
@Output()
zoomed: EventEmitter<any> = 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<IXChartContainer> {
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
}
@@ -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,
@@ -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';
+1
View File
@@ -19,6 +19,7 @@
"moduleResolution": "node",
"experimentalDecorators": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true,
"suppressImplicitAnyIndexErrors": true,
"forceConsistentCasingInFileNames": true,
"lib": [