diff --git a/libs/portal-integration-angular/assets/i18n/de.json b/libs/portal-integration-angular/assets/i18n/de.json index 6700ddf1..c09617e1 100644 --- a/libs/portal-integration-angular/assets/i18n/de.json +++ b/libs/portal-integration-angular/assets/i18n/de.json @@ -83,7 +83,12 @@ }, "OCX_DIAGRAM": { "SUM": "Gesamtanzahl", - "NO_DATA": "Es sind keine Daten vorhanden" + "NO_DATA": "Es sind keine Daten vorhanden", + "SWITCH_DIAGRAM_TYPE": { + "PIE": "Zu Tortendiagramm wechseln", + "HORIZONTAL_BAR": "Zu horizontalem Balkendiagramm wechseln", + "VERTICAL_BAR": "Zu vertikalem Balkendiagramm wechseln" + } }, "OCX_PORTAL_VIEWPORT": { "SUCCESS": "Erfolg!", diff --git a/libs/portal-integration-angular/assets/i18n/en.json b/libs/portal-integration-angular/assets/i18n/en.json index 357b85a7..9b3d87ec 100644 --- a/libs/portal-integration-angular/assets/i18n/en.json +++ b/libs/portal-integration-angular/assets/i18n/en.json @@ -83,7 +83,12 @@ }, "OCX_DIAGRAM": { "SUM": "Total", - "NO_DATA": "There is no data available" + "NO_DATA": "There is no data available", + "SWITCH_DIAGRAM_TYPE": { + "PIE": "Switch to pie chart", + "HORIZONTAL_BAR": "Switch to horizontal bar chart", + "VERTICAL_BAR": "Switch to vertical bar chart" + } }, "OCX_PORTAL_VIEWPORT": { "SUCCESS": "Success!", diff --git a/libs/portal-integration-angular/src/lib/core/components/diagram/diagram.component.html b/libs/portal-integration-angular/src/lib/core/components/diagram/diagram.component.html index 69a3f032..ca850eb0 100644 --- a/libs/portal-integration-angular/src/lib/core/components/diagram/diagram.component.html +++ b/libs/portal-integration-angular/src/lib/core/components/diagram/diagram.component.html @@ -1,4 +1,17 @@ +
+ + + + + +
{ let translateService: TranslateService @@ -38,6 +41,8 @@ describe('DiagramComponent', () => { ChartModule, MessageModule, MockAuthModule, + SelectButtonModule, + FormsModule, TranslateTestingModule.withTranslations({ en: require('./../../../../../assets/i18n/en.json'), de: require('./../../../../../assets/i18n/de.json'), @@ -103,4 +108,110 @@ describe('DiagramComponent', () => { const chartType = await chartHarness.getType() expect(chartType).toEqual('bar') }) + + it('should not display a diagramType select button by default', async () => { + expect(component.supportedDiagramTypes).toEqual([]) + expect(component.shownDiagramTypes).toEqual([]) + + const diagram = await TestbedHarnessEnvironment.harnessForFixture(fixture, DiagramHarness) + const diagramTypeSelectButton = await diagram.getDiagramTypeSelectButton() + + expect(diagramTypeSelectButton).toBe(null) + }) + + it('should render a diagramType select button if supportedDiagramTypes is specified', async () => { + const expectedDiagramLayouts: DiagramLayouts[] = [ + { icon: PrimeIcons.CHART_PIE, layout: DiagramType.PIE, titleKey: 'OCX_DIAGRAM.SWITCH_DIAGRAM_TYPE.PIE' }, + { + icon: PrimeIcons.BARS, + layout: DiagramType.HORIZONTAL_BAR, + titleKey: 'OCX_DIAGRAM.SWITCH_DIAGRAM_TYPE.HORIZONTAL_BAR', + }, + ] + + component.supportedDiagramTypes = [DiagramType.PIE, DiagramType.HORIZONTAL_BAR] + const diagram = await TestbedHarnessEnvironment.harnessForFixture(fixture, DiagramHarness) + const diagramTypeSelectButton = await diagram.getDiagramTypeSelectButton() + const diagramTypeSelectButtonOptions = await diagram.getAllSelectionButtons() + + expect(component.shownDiagramTypes).toEqual(expectedDiagramLayouts) + expect(diagramTypeSelectButton).toBeTruthy() + expect(diagramTypeSelectButtonOptions.length).toBe(2) + }) + + it('should change the rendered diagram whenever the select button is used to change the diagramType', async () => { + component.supportedDiagramTypes = [DiagramType.PIE, DiagramType.HORIZONTAL_BAR] + + const diagram = await TestbedHarnessEnvironment.harnessForFixture(fixture, DiagramHarness) + const diagramTypeSelectButton = await diagram.getDiagramTypeSelectButton() + const diagramTypeSelectButtonOptions = await diagram.getAllSelectionButtons() + + let diagramTypeChangedEvent: DiagramType | undefined + component.diagramTypeChanged.subscribe((event) => (diagramTypeChangedEvent = event)) + + expect(diagramTypeSelectButton).toBeTruthy() + expect(component.diagramType).toBe(DiagramType.PIE) + let chartHarness = await diagram.getChart() + let chartType = await chartHarness.getType() + expect(chartType).toEqual('pie') + + await diagramTypeSelectButtonOptions[1].click() + expect(component.diagramType).toBe(DiagramType.HORIZONTAL_BAR) + chartHarness = await diagram.getChart() + chartType = await chartHarness.getType() + expect(chartType).toEqual('bar') + expect(diagramTypeChangedEvent).toBe(DiagramType.HORIZONTAL_BAR) + + await diagramTypeSelectButtonOptions[0].click() + expect(component.diagramType).toBe(DiagramType.PIE) + chartHarness = await diagram.getChart() + chartType = await chartHarness.getType() + expect(chartType).toEqual('pie') + expect(diagramTypeChangedEvent).toBe(DiagramType.PIE) + }) + + it('should dynamically add/remove options to/from the diagramType select button', async () => { + const allDiagramLayouts: DiagramLayouts[] = [ + { icon: PrimeIcons.CHART_PIE, layout: DiagramType.PIE, titleKey: 'OCX_DIAGRAM.SWITCH_DIAGRAM_TYPE.PIE' }, + { + icon: PrimeIcons.BARS, + layout: DiagramType.HORIZONTAL_BAR, + titleKey: 'OCX_DIAGRAM.SWITCH_DIAGRAM_TYPE.HORIZONTAL_BAR', + }, + { + icon: PrimeIcons.CHART_BAR, + layout: DiagramType.VERTICAL_BAR, + titleKey: 'OCX_DIAGRAM.SWITCH_DIAGRAM_TYPE.VERTICAL_BAR', + }, + ] + + expect(component.shownDiagramTypes).toEqual([]) + + component.supportedDiagramTypes = [DiagramType.PIE, DiagramType.HORIZONTAL_BAR] + const diagram = await TestbedHarnessEnvironment.harnessForFixture(fixture, DiagramHarness) + const diagramTypeSelectButton = await diagram.getDiagramTypeSelectButton() + + expect(diagramTypeSelectButton).toBeTruthy() + expect(component.shownDiagramTypes).toEqual(allDiagramLayouts.slice(0, 2)) + const diagramTypeSelectButtonOptions = await diagram.getAllSelectionButtons() + expect(diagramTypeSelectButtonOptions.length).toBe(2) + + component.supportedDiagramTypes = [DiagramType.PIE, DiagramType.HORIZONTAL_BAR, DiagramType.VERTICAL_BAR] + const diagramTypeSelectButtonAfterUpdate = await diagram.getDiagramTypeSelectButton() + const diagramTypeSelectButtonOptionsAfterUpdate = await diagram.getAllSelectionButtons() + expect(diagramTypeSelectButtonAfterUpdate).toBeTruthy() + expect(component.shownDiagramTypes).toEqual(allDiagramLayouts) + expect(diagramTypeSelectButtonOptionsAfterUpdate.length).toBe(3) + }) + + it('should automatically select the button for the currently displayed diagram', async () => { + component.supportedDiagramTypes = [DiagramType.PIE, DiagramType.HORIZONTAL_BAR] + component.diagramType = DiagramType.HORIZONTAL_BAR + + const diagram = await TestbedHarnessEnvironment.harnessForFixture(fixture, DiagramHarness) + const diagramTypeSelectButtonOptions = await diagram.getAllSelectionButtons() + + expect(await diagramTypeSelectButtonOptions[0].hasClass('p-highlight')).toBe(false) + expect(await diagramTypeSelectButtonOptions[1].hasClass('p-highlight')).toBe(true) + }) }) diff --git a/libs/portal-integration-angular/src/lib/core/components/diagram/diagram.component.stories.ts b/libs/portal-integration-angular/src/lib/core/components/diagram/diagram.component.stories.ts new file mode 100644 index 00000000..d1b3f86f --- /dev/null +++ b/libs/portal-integration-angular/src/lib/core/components/diagram/diagram.component.stories.ts @@ -0,0 +1,91 @@ +import { importProvidersFrom } from '@angular/core' +import { BrowserModule } from '@angular/platform-browser' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { Meta, StoryFn, applicationConfig, moduleMetadata } from '@storybook/angular' +import { BreadcrumbModule } from 'primeng/breadcrumb' +import { ButtonModule } from 'primeng/button' +import { MenuModule } from 'primeng/menu' +import { SkeletonModule } from 'primeng/skeleton' +import { DynamicPipe } from '../../pipes/dynamic.pipe' +import { StorybookTranslateModule } from '../../storybook-translate.module' +import { DiagramComponent } from './diagram.component' +import { DiagramType } from '../../../model/diagram-type' +import { DiagramData } from '../../../model/diagram-data' +import { ChartModule } from 'primeng/chart' +import { SelectButtonModule } from 'primeng/selectbutton' +import { FormsModule } from '@angular/forms' + +export default { + title: 'DiagramComponent', + component: DiagramComponent, + argTypes: { + diagramType: { + options: [DiagramType.HORIZONTAL_BAR, DiagramType.VERTICAL_BAR, DiagramType.PIE], + control: { type: 'select' }, + }, + }, + decorators: [ + applicationConfig({ + providers: [importProvidersFrom(BrowserModule), importProvidersFrom(BrowserAnimationsModule)], + }), + moduleMetadata({ + declarations: [DiagramComponent, DynamicPipe], + imports: [MenuModule, BreadcrumbModule, ButtonModule, SkeletonModule, StorybookTranslateModule, ChartModule, SelectButtonModule, FormsModule], + }), + ], +} as Meta + +const Template: StoryFn = (args: DiagramComponent) => ({ + props: args, +}) + +const mockData: DiagramData[] = [ + { + label: 'Apples', + value: 10, + }, + { + label: 'Bananas', + value: 7, + }, + { + label: 'Oranges', + value: 3, + }, +] + +export const PieChart = { + render: Template, + + args: { + diagramType: DiagramType.PIE, + data: mockData, + }, +} + +export const HorizontalBarChart = { + render: Template, + + args: { + diagramType: DiagramType.HORIZONTAL_BAR, + data: mockData, + }, +} + +export const VerticalBarChart = { + render: Template, + + args: { + diagramType: DiagramType.VERTICAL_BAR, + data: mockData, + }, +} + +export const WithDiagramTypeSelection = { + render: Template, + args: { + diagramType: DiagramType.PIE, + data: mockData, + supportedDiagramTypes: [DiagramType.PIE, DiagramType.HORIZONTAL_BAR, DiagramType.VERTICAL_BAR] + } +} diff --git a/libs/portal-integration-angular/src/lib/core/components/diagram/diagram.component.ts b/libs/portal-integration-angular/src/lib/core/components/diagram/diagram.component.ts index 0810eb9e..55577114 100644 --- a/libs/portal-integration-angular/src/lib/core/components/diagram/diagram.component.ts +++ b/libs/portal-integration-angular/src/lib/core/components/diagram/diagram.component.ts @@ -2,18 +2,34 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angu import { TranslateService } from '@ngx-translate/core' import { ChartData, ChartOptions } from 'chart.js' import * as d3 from 'd3-scale-chromatic' -import { ColorUtils } from '../../utils/colorutils' +import { PrimeIcons } from 'primeng/api' import { DiagramData } from '../../../model/diagram-data' import { DiagramType } from '../../../model/diagram-type' +import { ColorUtils } from '../../utils/colorutils' + +export interface DiagramLayouts { + icon: string + layout: DiagramType + title?: string + titleKey: string +} + +const allDiagramTypes: DiagramLayouts[] = [ + { icon: PrimeIcons.CHART_PIE, layout: DiagramType.PIE, titleKey: 'OCX_DIAGRAM.SWITCH_DIAGRAM_TYPE.PIE' }, + { icon: PrimeIcons.BARS, layout: DiagramType.HORIZONTAL_BAR, titleKey: 'OCX_DIAGRAM.SWITCH_DIAGRAM_TYPE.HORIZONTAL_BAR' }, + { icon: PrimeIcons.CHART_BAR, layout: DiagramType.VERTICAL_BAR, titleKey: 'OCX_DIAGRAM.SWITCH_DIAGRAM_TYPE.VERTICAL_BAR' }, +] @Component({ selector: 'ocx-diagram', templateUrl: './diagram.component.html', + styleUrls: ['./diagram.component.scss'] }) export class DiagramComponent implements OnInit, OnChanges { @Input() data: DiagramData[] | undefined @Input() sumKey = 'OCX_DIAGRAM.SUM' private _diagramType: DiagramType = DiagramType.PIE + selectedDiagramType: DiagramLayouts | undefined public chartType = 'pie' @Input() get diagramType(): DiagramType { @@ -21,12 +37,24 @@ export class DiagramComponent implements OnInit, OnChanges { } set diagramType(value: DiagramType) { this._diagramType = value + this.selectedDiagramType = allDiagramTypes.find((v) => v.layout === value) this.chartType = this.diagramTypeToChartType(value) } + private _supportedDiagramTypes: DiagramType[] = [] + @Input() + get supportedDiagramTypes(): DiagramType[] { + return this._supportedDiagramTypes + } + set supportedDiagramTypes(value: DiagramType[]) { + this._supportedDiagramTypes = value + this.shownDiagramTypes = allDiagramTypes.filter((vl) => this.supportedDiagramTypes.includes(vl.layout)) + } @Output() dataSelected: EventEmitter = new EventEmitter() + @Output() diagramTypeChanged: EventEmitter = new EventEmitter() chartOptions: ChartOptions | undefined chartData: ChartData | undefined amountOfData: number | undefined | null + shownDiagramTypes: DiagramLayouts[] = [] // Changing the colorRangeInfo, will change the range of the color palette of the diagram. private colorRangeInfo = { colorStart: 0, @@ -90,6 +118,12 @@ export class DiagramComponent implements OnInit, OnChanges { dataClicked(event: []) { this.dataSelected.emit(event.length) } + + onDiagramTypeChanged(event: any) { + this.diagramType = event.value.layout + this.generateChart(this.colorScale, this.colorRangeInfo) + this.diagramTypeChanged.emit(event.value.layout) + } } function interpolateColors(amountOfData: number, colorScale: any, colorRangeInfo: any) { return ColorUtils.interpolateColors(amountOfData, colorScale, colorRangeInfo) diff --git a/libs/portal-integration-angular/src/lib/core/components/group-by-count-diagram/group-by-count-diagram.component.html b/libs/portal-integration-angular/src/lib/core/components/group-by-count-diagram/group-by-count-diagram.component.html index 55d6de89..56631dba 100644 --- a/libs/portal-integration-angular/src/lib/core/components/group-by-count-diagram/group-by-count-diagram.component.html +++ b/libs/portal-integration-angular/src/lib/core/components/group-by-count-diagram/group-by-count-diagram.component.html @@ -1 +1,8 @@ - + diff --git a/libs/portal-integration-angular/src/lib/core/components/group-by-count-diagram/group-by-count-diagram.component.spec.ts b/libs/portal-integration-angular/src/lib/core/components/group-by-count-diagram/group-by-count-diagram.component.spec.ts index 4fc6300f..cd83e044 100644 --- a/libs/portal-integration-angular/src/lib/core/components/group-by-count-diagram/group-by-count-diagram.component.spec.ts +++ b/libs/portal-integration-angular/src/lib/core/components/group-by-count-diagram/group-by-count-diagram.component.spec.ts @@ -13,6 +13,7 @@ import { MockAuthModule } from '../../../mock-auth/mock-auth.module' import { ColumnType } from '../../../model/column-type.model' import { DiagramComponent } from '../diagram/diagram.component' import { GroupByCountDiagramComponent } from './group-by-count-diagram.component' +import { DiagramType } from '../../../model/diagram-type' describe('GroupByCountDiagramComponent', () => { let translateService: TranslateService @@ -195,4 +196,22 @@ describe('GroupByCountDiagramComponent', () => { const definedSumKeyTranslation = translateService.instant(definedSumKey) expect(displayedText).toEqual(definedSumKeyTranslation) }) + + it('should not display a selectButton on the diagram by default', async () => { + expect(component.supportedDiagramTypes).toEqual([]) + + const diagram = await loader.getHarness(DiagramHarness) + const diagramTypeSelectButton = await diagram.getDiagramTypeSelectButton() + + expect(diagramTypeSelectButton).toBe(null) + }) + + it('should display a selectButton on the diagram if supportedDiagramTypes is specified', async () => { + component.supportedDiagramTypes = [DiagramType.PIE, DiagramType.HORIZONTAL_BAR] + + const diagram = await loader.getHarness(DiagramHarness) + const diagramTypeSelectButton = await diagram.getDiagramTypeSelectButton() + + expect(diagramTypeSelectButton).toBeTruthy() + }) }) diff --git a/libs/portal-integration-angular/src/lib/core/components/group-by-count-diagram/group-by-count-diagram.component.stories.ts b/libs/portal-integration-angular/src/lib/core/components/group-by-count-diagram/group-by-count-diagram.component.stories.ts new file mode 100644 index 00000000..f941de54 --- /dev/null +++ b/libs/portal-integration-angular/src/lib/core/components/group-by-count-diagram/group-by-count-diagram.component.stories.ts @@ -0,0 +1,124 @@ +import { importProvidersFrom } from '@angular/core' +import { BrowserModule } from '@angular/platform-browser' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { Meta, StoryFn, applicationConfig, moduleMetadata } from '@storybook/angular' +import { BreadcrumbModule } from 'primeng/breadcrumb' +import { ButtonModule } from 'primeng/button' +import { MenuModule } from 'primeng/menu' +import { SkeletonModule } from 'primeng/skeleton' +import { DynamicPipe } from '../../pipes/dynamic.pipe' +import { StorybookTranslateModule } from '../../storybook-translate.module' +import { DiagramType } from '../../../model/diagram-type' +import { ChartModule } from 'primeng/chart' +import { SelectButtonModule } from 'primeng/selectbutton' +import { GroupByCountDiagramComponent } from './group-by-count-diagram.component' +import { DiagramComponent } from '../diagram/diagram.component' +import { ColumnType } from '../../../model/column-type.model' + +export default { + title: 'GroupByCountDiagramComponent', + component: GroupByCountDiagramComponent, + argTypes: { + diagramType: { + options: [DiagramType.HORIZONTAL_BAR, DiagramType.VERTICAL_BAR, DiagramType.PIE], + control: { type: 'select' }, + }, + }, + decorators: [ + applicationConfig({ + providers: [importProvidersFrom(BrowserModule), importProvidersFrom(BrowserAnimationsModule)], + }), + moduleMetadata({ + declarations: [GroupByCountDiagramComponent, DiagramComponent, DynamicPipe], + imports: [MenuModule, BreadcrumbModule, ButtonModule, SkeletonModule, StorybookTranslateModule, ChartModule, SelectButtonModule], + }), + ], +} as Meta + +const Template: StoryFn = (args: GroupByCountDiagramComponent) => ({ + props: args, +}) + +const mockData = [ + { + id: 1, + fruitType: 'Apple', + name: 'Apple1' + }, + { + id: 2, + fruitType: 'Apple', + name: 'Apple2' + }, + { + id: 3, + fruitType: 'Apple', + name: 'Apple3' + }, + { + id: 4, + fruitType: 'Banana', + name: 'Banana1' + }, + { + id: 5, + fruitType: 'Banana', + name: 'Banana2' + } +] + +export const PieChart = { + render: Template, + + args: { + diagramType: DiagramType.PIE, + data: mockData, + column: { + id: 'fruitType', + type: ColumnType.STRING + }, + sumKey: 'Total' + }, +} + +export const HorizontalBarChart = { + render: Template, + + args: { + diagramType: DiagramType.HORIZONTAL_BAR, + data: mockData, + column: { + id: 'fruitType', + type: ColumnType.STRING + }, + sumKey: 'Total' + }, +} + +export const VerticalBarChart = { + render: Template, + + args: { + diagramType: DiagramType.VERTICAL_BAR, + data: mockData, + column: { + id: 'fruitType', + type: ColumnType.STRING + }, + sumKey: 'Total' + }, +} + +export const WithDiagramTypeSelection = { + render: Template, + args: { + diagramType: DiagramType.PIE, + data: mockData, + supportedDiagramTypes: [DiagramType.PIE, DiagramType.HORIZONTAL_BAR, DiagramType.VERTICAL_BAR], + column: { + id: 'fruitType', + type: ColumnType.STRING + }, + sumKey: 'Total' + } +} diff --git a/libs/portal-integration-angular/src/lib/core/components/group-by-count-diagram/group-by-count-diagram.component.ts b/libs/portal-integration-angular/src/lib/core/components/group-by-count-diagram/group-by-count-diagram.component.ts index 1d0a047f..fa5208e0 100644 --- a/libs/portal-integration-angular/src/lib/core/components/group-by-count-diagram/group-by-count-diagram.component.ts +++ b/libs/portal-integration-angular/src/lib/core/components/group-by-count-diagram/group-by-count-diagram.component.ts @@ -13,7 +13,18 @@ import { DiagramType } from '../../../model/diagram-type' }) export class GroupByCountDiagramComponent implements OnInit { @Input() sumKey = 'SEARCH.SUMMARY_TITLE' - @Input() type = DiagramType.PIE + @Input() diagramType = DiagramType.PIE + /** + * @deprecated Will be replaced by diagramType + */ + @Input() + get type(): DiagramType { + return this.diagramType + } + set type(value: DiagramType) { + this.diagramType = value + } + @Input() supportedDiagramTypes: DiagramType[] = [] private _data$ = new BehaviorSubject([]) @Input() get data(): unknown[] { @@ -52,6 +63,7 @@ export class GroupByCountDiagramComponent implements OnInit { } @Output() dataSelected: EventEmitter = new EventEmitter() + @Output() diagramTypeChanged: EventEmitter = new EventEmitter() constructor(private translateService: TranslateService) {} @@ -85,4 +97,9 @@ export class GroupByCountDiagramComponent implements OnInit { dataClicked(event: any) { this.dataSelected.emit(event) } + + onDiagramTypeChanged(newDiagramType: DiagramType) { + this.diagramType = newDiagramType + this.diagramTypeChanged.emit(newDiagramType) + } } diff --git a/libs/portal-integration-angular/testing/diagram.harness.ts b/libs/portal-integration-angular/testing/diagram.harness.ts index 1eb0f43b..8dfe3d22 100644 --- a/libs/portal-integration-angular/testing/diagram.harness.ts +++ b/libs/portal-integration-angular/testing/diagram.harness.ts @@ -1,5 +1,6 @@ import { ComponentHarness } from '@angular/cdk/testing' import { PChartHarness } from './primeng/p-chart.harness' +import { PSelectButtonHarness } from './primeng/p-selectButton.harness' export class DiagramHarness extends ComponentHarness { static hostSelector = 'ocx-diagram' @@ -13,4 +14,12 @@ export class DiagramHarness extends ComponentHarness { async getSumLabel(): Promise { return (await this.locatorForOptional('.sumKey span[name="sumLabel"]')())?.text() } + + async getDiagramTypeSelectButton() { + return (await this.locatorForOptional('p-selectButton[name="diagram-type-select-button"]')()) + } + + async getAllSelectionButtons() { + return await (await this.locatorFor(PSelectButtonHarness)()).getAllButtons() + } }