From 89e63510b5eb111dd058472ebe07b190526b4743 Mon Sep 17 00:00:00 2001 From: Andrei Timar Date: Wed, 8 Jul 2026 22:45:27 +0300 Subject: [PATCH] feat(charts): add bar chart component --- .github/workflows/ui-charts-ci.yml | 2 +- .../federation.config.mjs | 6 +- .../default-scriptbee-charts/src/app/app.html | 7 ++ .../src/app/app.spec.ts | 12 +++- .../default-scriptbee-charts/src/app/app.ts | 64 ++++++++++++++++++- .../app/components/bar-chart/bar-chart.html | 1 + .../app/components/bar-chart/bar-chart.scss | 0 .../src/app/components/bar-chart/bar-chart.ts | 34 ++++++++++ .../src/app/services/theme.service.ts | 40 ++++++++++++ .../src/app/types/ChartInput.ts | 19 ++++++ 10 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 Plugins/UI/default-scriptbee-charts/src/app/components/bar-chart/bar-chart.html create mode 100644 Plugins/UI/default-scriptbee-charts/src/app/components/bar-chart/bar-chart.scss create mode 100644 Plugins/UI/default-scriptbee-charts/src/app/components/bar-chart/bar-chart.ts create mode 100644 Plugins/UI/default-scriptbee-charts/src/app/services/theme.service.ts create mode 100644 Plugins/UI/default-scriptbee-charts/src/app/types/ChartInput.ts diff --git a/.github/workflows/ui-charts-ci.yml b/.github/workflows/ui-charts-ci.yml index 1327cd9f..93cf8f8c 100644 --- a/.github/workflows/ui-charts-ci.yml +++ b/.github/workflows/ui-charts-ci.yml @@ -1,4 +1,4 @@ -name: UI CI +name: UI Charts CI on: push: diff --git a/Plugins/UI/default-scriptbee-charts/federation.config.mjs b/Plugins/UI/default-scriptbee-charts/federation.config.mjs index b550411d..3d6d970d 100644 --- a/Plugins/UI/default-scriptbee-charts/federation.config.mjs +++ b/Plugins/UI/default-scriptbee-charts/federation.config.mjs @@ -2,13 +2,13 @@ import { shareAll, withNativeFederation } from '@angular-architects/native-feder export default withNativeFederation({ name: 'default-scriptbee-charts', - + exposes: { + './BarChart': './src/app/components/bar-chart/bar-chart.ts', + }, shared: { ...shareAll({ singleton: true, strictVersion: false, requiredVersion: 'auto' }), }, - skip: ['@angular-architects/native-federation', 'rxjs/ajax', 'rxjs/fetch', 'rxjs/testing', 'rxjs/webSocket'], - features: { ignoreUnusedDeps: true, }, diff --git a/Plugins/UI/default-scriptbee-charts/src/app/app.html b/Plugins/UI/default-scriptbee-charts/src/app/app.html index 9baf8929..6c35a40f 100644 --- a/Plugins/UI/default-scriptbee-charts/src/app/app.html +++ b/Plugins/UI/default-scriptbee-charts/src/app/app.html @@ -1 +1,8 @@
Hello, default-scriptbee-chart!
+ +
+ Mode: {{ themeService.echartsTheme() }} + +
+ + diff --git a/Plugins/UI/default-scriptbee-charts/src/app/app.spec.ts b/Plugins/UI/default-scriptbee-charts/src/app/app.spec.ts index 75753d68..89efcb0c 100644 --- a/Plugins/UI/default-scriptbee-charts/src/app/app.spec.ts +++ b/Plugins/UI/default-scriptbee-charts/src/app/app.spec.ts @@ -1,10 +1,20 @@ +import { computed, signal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { App } from './app'; +import { ThemeService } from './services/theme.service'; + +describe('AppComponent with Mock Theme', () => { + let mockThemeService: Partial; -describe('App', () => { beforeEach(async () => { + mockThemeService = { + isDark: signal(false), + echartsTheme: computed(() => 'light'), + }; + await TestBed.configureTestingModule({ imports: [App], + providers: [{ provide: ThemeService, useValue: mockThemeService }], }).compileComponents(); }); diff --git a/Plugins/UI/default-scriptbee-charts/src/app/app.ts b/Plugins/UI/default-scriptbee-charts/src/app/app.ts index 66363352..49febb5d 100644 --- a/Plugins/UI/default-scriptbee-charts/src/app/app.ts +++ b/Plugins/UI/default-scriptbee-charts/src/app/app.ts @@ -1,9 +1,67 @@ -import { Component } from '@angular/core'; +import { Component, computed, inject } from '@angular/core'; +import { BarChart } from './components/bar-chart/bar-chart'; +import { ThemeService } from './services/theme.service'; +import { BarChartInput, ChartParameters } from './types/ChartInput'; @Component({ selector: 'app-root', - imports: [], + imports: [BarChart], templateUrl: './app.html', styleUrl: './app.scss', }) -export class App {} +export class App { + readonly themeService = inject(ThemeService); + + barChartParameters = computed>(() => { + const xAxisData = []; + const data1 = []; + const data2 = []; + const data3 = []; + + for (let i = 0; i < 100; i++) { + xAxisData.push('category' + i); + data1.push((Math.sin(i / 5) * (i / 5 - 10) + i / 6) * 5); + data2.push((Math.cos(i / 5) * (i / 5 - 10) + i / 6) * 5); + data3.push((Math.sin(i / 5) * (i / 5 + 10) + i / 6) * 5); + } + + const series: ChartParameters['input']['series'] = [ + { + name: 'bar', + data: data1, + }, + { + name: 'bar2', + data: data2, + }, + { + name: 'bar3', + data: data3, + color: 'red', + }, + ]; + + return { + theme: this.themeService.echartsTheme(), + input: { + xAxis: { + data: xAxisData, + silent: false, + splitLine: { + show: false, + }, + }, + yAxis: {}, + legend: { + data: ['bar', 'bar2', 'bar3'], + align: 'left', + }, + tooltip: {}, + series: series, + options: { + animationEasing: 'elasticOut', + }, + }, + }; + }); +} diff --git a/Plugins/UI/default-scriptbee-charts/src/app/components/bar-chart/bar-chart.html b/Plugins/UI/default-scriptbee-charts/src/app/components/bar-chart/bar-chart.html new file mode 100644 index 00000000..bf3048e2 --- /dev/null +++ b/Plugins/UI/default-scriptbee-charts/src/app/components/bar-chart/bar-chart.html @@ -0,0 +1 @@ +
diff --git a/Plugins/UI/default-scriptbee-charts/src/app/components/bar-chart/bar-chart.scss b/Plugins/UI/default-scriptbee-charts/src/app/components/bar-chart/bar-chart.scss new file mode 100644 index 00000000..e69de29b diff --git a/Plugins/UI/default-scriptbee-charts/src/app/components/bar-chart/bar-chart.ts b/Plugins/UI/default-scriptbee-charts/src/app/components/bar-chart/bar-chart.ts new file mode 100644 index 00000000..fd8a5b5e --- /dev/null +++ b/Plugins/UI/default-scriptbee-charts/src/app/components/bar-chart/bar-chart.ts @@ -0,0 +1,34 @@ +import { Component, computed, input } from '@angular/core'; +import { EChartsCoreOption } from 'echarts'; +import { NgxEchartsDirective, provideEchartsCore } from 'ngx-echarts'; +import * as echarts from 'echarts/core'; +import { BarChart as EchartsBarChart } from 'echarts/charts'; +import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'; +import { CanvasRenderer } from 'echarts/renderers'; +import { BarChartInput, ChartParameters } from '../../types/ChartInput'; + +echarts.use([EchartsBarChart, GridComponent, CanvasRenderer, LegendComponent, TooltipComponent]); + +@Component({ + selector: 'app-bar-chart', + imports: [NgxEchartsDirective], + templateUrl: './bar-chart.html', + styleUrl: './bar-chart.scss', + providers: [provideEchartsCore({ echarts })], +}) +export class BarChart { + parameters = input.required>(); + + options = computed(() => { + const input = this.parameters().input; + + return { + ...(input.options ?? {}), + legend: input.legend, + tooltip: input.tooltip, + xAxis: input.xAxis, + yAxis: input.yAxis, + series: (input.series ?? []).map((s) => ({ ...s, type: 'bar' })), + }; + }); +} diff --git a/Plugins/UI/default-scriptbee-charts/src/app/services/theme.service.ts b/Plugins/UI/default-scriptbee-charts/src/app/services/theme.service.ts new file mode 100644 index 00000000..35820d4f --- /dev/null +++ b/Plugins/UI/default-scriptbee-charts/src/app/services/theme.service.ts @@ -0,0 +1,40 @@ +import { Injectable, signal, computed } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class ThemeService { + private readonly STORAGE_KEY = 'ngx-echarts-theme'; + + readonly isDark = signal(this.loadTheme()); + + readonly echartsTheme = computed(() => (this.isDark() ? 'dark' : 'light')); + + constructor() { + this.applyTheme(this.isDark()); + } + + toggle(): void { + const dark = !this.isDark(); + this.isDark.set(dark); + this.applyTheme(dark); + localStorage.setItem(this.STORAGE_KEY, dark ? 'dark' : 'light'); + } + + private loadTheme(): boolean { + const stored = localStorage.getItem(this.STORAGE_KEY); + if (stored) { + return stored === 'dark'; + } + return window.matchMedia('(prefers-color-scheme: dark)').matches; + } + + private applyTheme(dark: boolean): void { + const body = document.body; + if (dark) { + body.classList.add('dark-theme'); + body.classList.remove('light-theme'); + } else { + body.classList.add('light-theme'); + body.classList.remove('dark-theme'); + } + } +} diff --git a/Plugins/UI/default-scriptbee-charts/src/app/types/ChartInput.ts b/Plugins/UI/default-scriptbee-charts/src/app/types/ChartInput.ts new file mode 100644 index 00000000..4b067498 --- /dev/null +++ b/Plugins/UI/default-scriptbee-charts/src/app/types/ChartInput.ts @@ -0,0 +1,19 @@ +import { EChartsCoreOption } from 'echarts/types/dist/echarts'; +import { EChartsOption } from 'echarts/types/dist/shared'; +import { BarSeriesOption } from 'echarts'; + +export type Theme = 'light' | 'dark'; + +export interface ChartParameters { + theme: Theme; + input: T; +} + +export interface BarChartInput { + xAxis: EChartsOption['xAxis']; + yAxis: EChartsOption['yAxis']; + legend: EChartsOption['legend']; + tooltip: EChartsOption['tooltip']; + series: Omit[]; + options?: EChartsCoreOption; +}