Skip to content

Commit

Permalink
perf(charts): revise the d3 charts library
Browse files Browse the repository at this point in the history
- [x] break down the chart examples component into presentational components;
- [x] fix the force directed chart - fix the links count value mapping;
  • Loading branch information
rfprod committed Aug 19, 2023
1 parent f89776d commit 1e5570d
Show file tree
Hide file tree
Showing 26 changed files with 649 additions and 351 deletions.
2 changes: 1 addition & 1 deletion libs/client-d3-charts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rfprodz/client-d3-charts",
"version": "1.3.16",
"version": "1.3.17",
"description": "Angular chart components based on D3JS (https://d3js.org).",
"keywords": [
"angular-charts",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="container" *ngIf="barChartConfig$ | async as config">
<app-bar-chart [chartId]="'bar-example-1'" [data]="config.data" [options]="config.options"></app-bar-chart>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
:host {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;

.container {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
width: 100%;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ComponentFixture, TestBed, TestModuleMetadata } from '@angular/core/testing';
import { firstValueFrom } from 'rxjs';

import { AppChartExamplesBarComponent } from './chart-examples-bar.component';

describe('AppChartExamplesBarComponent', () => {
const testBedConfig: TestModuleMetadata = {
declarations: [AppChartExamplesBarComponent],
};

let fixture: ComponentFixture<AppChartExamplesBarComponent>;
let component: AppChartExamplesBarComponent;

beforeEach(async () => {
await TestBed.configureTestingModule(testBedConfig).compileComponents();
fixture = TestBed.createComponent(AppChartExamplesBarComponent);
component = fixture.debugElement.componentInstance;
fixture.detectChanges();
});

it('should be defined', () => {
expect(component).toBeDefined();
});

it('the chart options should have expected structure', async () => {
const config = await firstValueFrom(component.barChartConfig$);
expect(config.options).toEqual({
chartTitle: 'Example bar chart',
xAxisTitle: 'long x axis title',
yAxisTitle: 'long y axis title',
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { first, map, switchMap, timer } from 'rxjs';

import { IBarChartOptions, TBarChartData } from '../../interfaces/bar-chart.interface';

/**
* BAr chart examples.
*/
@Component({
selector: 'app-chart-examples-bar',
templateUrl: './chart-examples-bar.component.html',
styleUrls: ['./chart-examples-bar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppChartExamplesBarComponent {
/**
* Sample bar chart data.
*/
private get barChartData() {
return <TBarChartData>[
{ title: 'one', value: 1 },
{ title: 'two', value: 2 },
{ title: 'three', value: 3 },
{ title: 'four', value: 4 },
{ title: 'five', value: 5 },
];
}

private readonly breakpoint$ = this.breakpointObserver
.observe([Breakpoints.XSmall, Breakpoints.Small, Breakpoints.Medium, Breakpoints.Large, Breakpoints.XLarge])
.pipe(map(result => Object.keys(result.breakpoints).find(item => result.breakpoints[item]) ?? 'unknown'));

public readonly barChartConfig$ = this.breakpoint$.pipe(
switchMap(() => {
const timeout = 100;
return timer(timeout).pipe(
first(),
map(() => ({ data: this.barChartData, options: this.barChartOptions() })),
);
}),
);

constructor(private readonly breakpointObserver: BreakpointObserver) {}

/**
* Example bar chart options.
*/
private barChartOptions() {
return <Partial<IBarChartOptions>>{
chartTitle: 'Example bar chart',
xAxisTitle: 'long x axis title',
yAxisTitle: 'long y axis title',
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div class="container" *ngIf="forceDirectedChartConfig$ | async as config">
<app-force-directed-chart
[chartId]="'force-directed-example-1'"
[data]="config.data"
[options]="config.options"
></app-force-directed-chart>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
:host {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;

.container {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
width: 100%;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ComponentFixture, TestBed, TestModuleMetadata } from '@angular/core/testing';
import { firstValueFrom } from 'rxjs';

import { AppChartExamplesForceDirectedComponent } from './chart-examples-force-directed.component';

describe('AppChartExamplesForceDirectedComponent', () => {
const testBedConfig: TestModuleMetadata = {
declarations: [AppChartExamplesForceDirectedComponent],
};

let fixture: ComponentFixture<AppChartExamplesForceDirectedComponent>;
let component: AppChartExamplesForceDirectedComponent;

beforeEach(async () => {
await TestBed.configureTestingModule(testBedConfig).compileComponents();
fixture = TestBed.createComponent(AppChartExamplesForceDirectedComponent);
component = fixture.debugElement.componentInstance;
fixture.detectChanges();
});

it('should be defined', () => {
expect(component).toBeDefined();
});

it('the chart options should have expected structure', async () => {
const config = await firstValueFrom(component.forceDirectedChartConfig$);
expect(config.options).toEqual({
chartTitle: 'Example force directed chart',
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { first, map, switchMap, timer } from 'rxjs';

import {
IForceDirectedChartData,
IForceDirectedChartOptions,
IForceDirectedGraphEntity,
} from '../../interfaces/force-directed-chart.interface';

@Component({
selector: 'app-chart-examples-force-directed',
templateUrl: './chart-examples-force-directed.component.html',
styleUrls: ['./chart-examples-force-directed.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppChartExamplesForceDirectedComponent {
/**
* Sample force directed chart data.
*/
private get forceDirectedChartData() {
const input = {
domains: ['first', 'second', 'third'],
entities: [
{ name: 'one', domains: ['first'], img: '' },
{ name: 'two', domains: ['second'], img: '' },
{ name: 'three', domains: ['third'], img: '' },
{ name: 'four', domains: ['first', 'second'], img: '' },
{ name: 'five', domains: ['second'], img: '' },
{ name: 'six', domains: ['third', 'second'], img: '' },
{ name: 'seven', domains: ['second'], img: '' },
{ name: 'eight', domains: ['third'], img: '' },
],
};
const domains: IForceDirectedChartData['domains'] = input.domains.map((name, index) => ({ index, name, value: 1 }));
const entities: IForceDirectedChartData['entities'] = input.entities.map((app, index) => ({
index: index,
name: app.name,
domains: [...app.domains],
img: app.img,
linksCount: app.domains.length,
}));
const nodes: IForceDirectedGraphEntity[] = [...entities];
const links: IForceDirectedChartData['links'] = entities
.map(entity => {
return entity.domains.map(domain => {
const source = domains.find(value => domain === value.name)?.index ?? -1;
const target = entity.index;
return { source, target };
});
})
.reduce((accumulator, item) => (Array.isArray(item) ? [...accumulator, ...item] : [...accumulator, item]), [])
.filter(link => link.source !== -1 && link.target !== -1 && typeof link.target !== 'undefined');
const chartData: IForceDirectedChartData = {
domains,
entities: entities.map(item => ({
...item,
linksCount: links.reduce((acc, link) => (link.target === item.index ? acc + 1 : acc), 0),
})),
links,
nodes,
};
return chartData;
}

private readonly breakpoint$ = this.breakpointObserver
.observe([Breakpoints.XSmall, Breakpoints.Small, Breakpoints.Medium, Breakpoints.Large, Breakpoints.XLarge])
.pipe(map(result => Object.keys(result.breakpoints).find(item => result.breakpoints[item]) ?? 'unknown'));

public readonly forceDirectedChartConfig$ = this.breakpoint$.pipe(
switchMap(() => {
const timeout = 100;
return timer(timeout).pipe(
first(),
map(() => ({ data: this.forceDirectedChartData, options: this.forceDirectedChartOptions() })),
);
}),
);

constructor(private readonly breakpointObserver: BreakpointObserver) {}

/**
* Example force directed chart options.
*/
private forceDirectedChartOptions() {
return <Partial<IForceDirectedChartOptions>>{
chartTitle: 'Example force directed chart',
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div class="container" *ngIf="lineChartConfig$ | async as config">
<app-line-chart [chartId]="'line-example-1'" [data]="[config.data[0]]" [options]="config.options"></app-line-chart>

<app-line-chart [chartId]="'line-example-2'" [data]="[config.data[1]]" [options]="config.optionsDateDdMmYy"></app-line-chart>

<app-line-chart
[chartId]="'line-example-3'"
[data]="[config.data[1], config.data[2]]"
[options]="config.optionsDateDdMmYyyy"
></app-line-chart>

<app-line-chart [chartId]="'line-example-4'" [data]="config.data" [options]="config.optionsDateMmYyyy"></app-line-chart>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
:host {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;

.container {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
width: 100%;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ComponentFixture, TestBed, TestModuleMetadata } from '@angular/core/testing';
import { firstValueFrom } from 'rxjs';

import { AppChartExamplesLineComponent } from './chart-examples-line.component';

describe('AppChartExamplesLineComponent', () => {
const testBedConfig: TestModuleMetadata = {
declarations: [AppChartExamplesLineComponent],
};

let fixture: ComponentFixture<AppChartExamplesLineComponent>;
let component: AppChartExamplesLineComponent;

beforeEach(async () => {
await TestBed.configureTestingModule(testBedConfig).compileComponents();
fixture = TestBed.createComponent(AppChartExamplesLineComponent);
component = fixture.debugElement.componentInstance;
fixture.detectChanges();
});

it('should be defined', () => {
expect(component).toBeDefined();
});

it('the chart options should have expected structure', async () => {
const config = await firstValueFrom(component.lineChartConfig$);
expect(config.options).toEqual({
chartTitle: 'Example line chart, date format default',
dateFormat: 'default',
xAxisTitle: 'Date range',
yAxisTitle: 'Value range',
});
});
});
Loading

0 comments on commit 1e5570d

Please sign in to comment.