saving progress, lots of charting decisions made
This commit is contained in:
7
src/components/chart/chart-config.ts
Normal file
7
src/components/chart/chart-config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { ChartType } from 'chart.js';
|
||||
import { ChartAxis } from './chart-type';
|
||||
|
||||
export interface ChartConfig {
|
||||
type: ChartType;
|
||||
axis: ChartAxis[];
|
||||
}
|
||||
@@ -9,14 +9,30 @@ export enum ChartType {
|
||||
Radar = 'radar',
|
||||
}
|
||||
|
||||
export enum ChartAxisPosition {
|
||||
Top = 'top',
|
||||
Right = 'right',
|
||||
Bottom = 'bottom',
|
||||
Left = 'left',
|
||||
Center = 'center',
|
||||
}
|
||||
|
||||
export interface IDataset {
|
||||
type?: ChartType;
|
||||
borderColor?: string;
|
||||
label?: string;
|
||||
data: string[];
|
||||
order?: number;
|
||||
data: (string | Record<string, any>)[];
|
||||
beginAtZero?: boolean;
|
||||
fill?: string | number | boolean;
|
||||
backgroundColor?: string | string[];
|
||||
pointBackgroundColor?: string;
|
||||
pointBorderColor?: string;
|
||||
parsing?: {
|
||||
xAxisKey?: string;
|
||||
yAxisKey?: string;
|
||||
};
|
||||
stack?: string;
|
||||
}
|
||||
|
||||
export interface IData {
|
||||
@@ -24,3 +40,15 @@ export interface IData {
|
||||
labelsSource?: string[];
|
||||
datasets: IDataset[];
|
||||
}
|
||||
|
||||
export interface ChartAxis {
|
||||
label: string | string[];
|
||||
source: string | string[];
|
||||
type?: ChartType;
|
||||
fill?: boolean;
|
||||
stacked?: boolean;
|
||||
stack?: string;
|
||||
position: ChartAxisPosition;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,21 @@ import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
SimpleChanges,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { Chart, ChartOptions } from 'chart.js/auto';
|
||||
import { Chart } from 'chart.js/auto';
|
||||
import { PluginNodata } from './plugin-nodata';
|
||||
import { PluginMoreColors } from './plugin-more-colors';
|
||||
import { ChartType, IData } from './chart-type';
|
||||
import { ChartConfig } from './chart-config';
|
||||
import { ChartConfigService } from './services/chart-config.service';
|
||||
import { DatasetService } from './services/dataset.service';
|
||||
import { LabelService } from './services/label.service';
|
||||
import { OptionsService } from './services/options.service';
|
||||
import { ScaleService } from './services/scale.service';
|
||||
|
||||
@Component({
|
||||
selector: 'fbi-chart',
|
||||
@@ -16,50 +24,43 @@ import { ChartType, IData } from './chart-type';
|
||||
imports: [],
|
||||
templateUrl: './chart.component.html',
|
||||
styleUrl: './chart.component.scss',
|
||||
providers: [
|
||||
ChartConfigService,
|
||||
DatasetService,
|
||||
LabelService,
|
||||
OptionsService,
|
||||
ScaleService,
|
||||
],
|
||||
})
|
||||
export class ChartComponent implements AfterViewInit, OnDestroy {
|
||||
export class ChartComponent implements OnChanges, AfterViewInit, OnDestroy {
|
||||
@ViewChild('chart') canvas!: ElementRef;
|
||||
@Input() config!: ChartConfig;
|
||||
@Input() data!: Record<string, any>[];
|
||||
|
||||
private chart: any = undefined;
|
||||
private data: IData = {
|
||||
labels: Array.from(
|
||||
{ length: 10 },
|
||||
() => `${Math.floor(Math.random() * 100)}`
|
||||
),
|
||||
// labelsSource: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'test',
|
||||
data: Array.from(
|
||||
{ length: 10 },
|
||||
() => `${Math.floor(Math.random() * 100)}`
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
constructor() {}
|
||||
constructor(private chartConfigService: ChartConfigService) {}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.initChart();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.initChart();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.chart?.destroy?.();
|
||||
}
|
||||
|
||||
initChart() {
|
||||
private initChart() {
|
||||
if (!this.canvas?.nativeElement) return;
|
||||
|
||||
this.chart?.destroy?.();
|
||||
const config = this.chartConfigService.chart(this.config, this.data);
|
||||
console.log(config);
|
||||
|
||||
const opts: ChartOptions = {};
|
||||
opts.responsive = true;
|
||||
opts.maintainAspectRatio = false;
|
||||
|
||||
this.chart = new Chart(this.canvas.nativeElement, {
|
||||
type: ChartType.Bar,
|
||||
data: this.data,
|
||||
options: opts,
|
||||
plugins: [PluginNodata.config(), PluginMoreColors.config()] as any[],
|
||||
});
|
||||
config.plugins = [PluginNodata.config(), PluginMoreColors.config()];
|
||||
this.chart = new Chart(this.canvas.nativeElement, config);
|
||||
}
|
||||
}
|
||||
|
||||
30
src/components/chart/services/chart-config.service.ts
Normal file
30
src/components/chart/services/chart-config.service.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ChartConfig } from '../chart-config';
|
||||
import { IData } from '../chart-type';
|
||||
import { DatasetService } from './dataset.service';
|
||||
import { LabelService } from './label.service';
|
||||
import { OptionsService } from './options.service';
|
||||
|
||||
@Injectable()
|
||||
export class ChartConfigService {
|
||||
constructor(
|
||||
private datasetService: DatasetService,
|
||||
private labelService: LabelService,
|
||||
private optionsService: OptionsService
|
||||
) {}
|
||||
|
||||
chart(chart: ChartConfig, data: Record<string, any>[]): any {
|
||||
return {
|
||||
type: chart?.type,
|
||||
data: this.data(chart, data),
|
||||
options: this.optionsService.options(chart, data),
|
||||
} as any;
|
||||
}
|
||||
|
||||
private data(chart: ChartConfig, data: Record<string, any>[]): IData {
|
||||
return {
|
||||
labels: this.labelService.labels(chart, data),
|
||||
datasets: this.datasetService.datasets(chart, data),
|
||||
} as IData;
|
||||
}
|
||||
}
|
||||
54
src/components/chart/services/dataset.service.ts
Normal file
54
src/components/chart/services/dataset.service.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ChartConfig } from '../chart-config';
|
||||
import { ChartAxis, ChartAxisPosition, IDataset } from '../chart-type';
|
||||
|
||||
@Injectable()
|
||||
export class DatasetService {
|
||||
constructor() {}
|
||||
|
||||
datasets(chart: ChartConfig, data: Record<string, any>[]): IDataset[] {
|
||||
const result: IDataset[] = [];
|
||||
|
||||
(chart?.axis ?? []).forEach((axis: ChartAxis) => {
|
||||
switch (axis.position) {
|
||||
case ChartAxisPosition.Left:
|
||||
case ChartAxisPosition.Right:
|
||||
const ds = this.dataset(axis, data);
|
||||
result.push(...ds);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private dataset(axis: ChartAxis, data: Record<string, any>[]): IDataset[] {
|
||||
let result: IDataset[] = [];
|
||||
|
||||
const labels = Array.isArray(axis.label) ? axis.label : [axis.label];
|
||||
const sources = Array.isArray(axis.source) ? axis.source : [axis.source];
|
||||
|
||||
if (labels.length !== sources.length) return result;
|
||||
|
||||
labels.forEach((label: string, idx: number) => {
|
||||
const source = sources[idx];
|
||||
const entry: IDataset = {
|
||||
label: label,
|
||||
// order: idx,
|
||||
data: (data ?? []).map((d: Record<string, any>) => d[source]),
|
||||
};
|
||||
if (axis.type) {
|
||||
entry.type = axis.type;
|
||||
}
|
||||
if (axis.stack) {
|
||||
entry.stack = axis.stack;
|
||||
}
|
||||
if (axis.fill) {
|
||||
entry.fill = axis.fill;
|
||||
}
|
||||
result.push(entry);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
33
src/components/chart/services/label.service.ts
Normal file
33
src/components/chart/services/label.service.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ChartConfig } from '../chart-config';
|
||||
import { ChartAxis, ChartAxisPosition } from '../chart-type';
|
||||
|
||||
@Injectable()
|
||||
export class LabelService {
|
||||
constructor() {}
|
||||
|
||||
labels(chart: ChartConfig, data: Record<string, any>[]): string[] {
|
||||
const result: string[] = [];
|
||||
|
||||
(chart?.axis ?? [])
|
||||
.filter((axis: ChartAxis) =>
|
||||
[ChartAxisPosition.Top, ChartAxisPosition.Bottom].includes(
|
||||
axis.position
|
||||
)
|
||||
)
|
||||
.map((axis: ChartAxis, index: number) => {
|
||||
if (index > 0) return;
|
||||
|
||||
const sources = Array.isArray(axis.source)
|
||||
? axis.source
|
||||
: [axis.source];
|
||||
const source = sources[0];
|
||||
|
||||
(data ?? []).map((record: Record<string, any>) =>
|
||||
result.push(record[sources[0]])
|
||||
);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
27
src/components/chart/services/options.service.ts
Normal file
27
src/components/chart/services/options.service.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ChartOptions } from 'chart.js';
|
||||
import { ChartConfig } from '../chart-config';
|
||||
import { ScaleService } from './scale.service';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ChartAxis } from '../chart-type';
|
||||
|
||||
@Injectable()
|
||||
export class OptionsService {
|
||||
constructor(private scaleService: ScaleService) {}
|
||||
|
||||
options(chart: ChartConfig, data: Record<string, any>[]): ChartOptions {
|
||||
const result: ChartOptions = {};
|
||||
|
||||
result.responsive = true;
|
||||
result.maintainAspectRatio = false;
|
||||
result.scales = this.scaleService.scales(chart, data);
|
||||
|
||||
const fill =
|
||||
(chart?.axis ?? []).filter((axis: ChartAxis) => axis.fill).length > 0;
|
||||
if (fill) {
|
||||
result.plugins = result.plugins ?? {};
|
||||
result.plugins.filler = { propagate: true };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
59
src/components/chart/services/scale.service.ts
Normal file
59
src/components/chart/services/scale.service.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ChartConfig } from '../chart-config';
|
||||
import { ChartAxis, ChartAxisPosition } from '../chart-type';
|
||||
|
||||
@Injectable()
|
||||
export class ScaleService {
|
||||
constructor() {}
|
||||
|
||||
scales(chart: ChartConfig, data: Record<string, any>[]): any {
|
||||
const result: any = {};
|
||||
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
(chart?.axis ?? []).forEach((axis: ChartAxis, index: number) => {
|
||||
let scale = '';
|
||||
let count = 0;
|
||||
axis.position = axis.position ?? ChartAxisPosition.Bottom;
|
||||
switch (axis.position) {
|
||||
case ChartAxisPosition.Left:
|
||||
case ChartAxisPosition.Right:
|
||||
scale = 'y';
|
||||
count = y;
|
||||
++y;
|
||||
break;
|
||||
case ChartAxisPosition.Bottom:
|
||||
case ChartAxisPosition.Top:
|
||||
scale = 'x';
|
||||
count = x;
|
||||
++x;
|
||||
break;
|
||||
}
|
||||
result[`${scale}${count === 0 ? '' : count}`] = this.getScale(axis);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getScale(item: ChartAxis): Record<string, any> {
|
||||
const opts: Record<string, any> = {};
|
||||
|
||||
if (item.stacked) {
|
||||
opts['stacked'] = true;
|
||||
}
|
||||
if (item.position) {
|
||||
opts['position'] = item.position;
|
||||
}
|
||||
if (item.stack) {
|
||||
opts['stack'] = item.stack;
|
||||
}
|
||||
if (item.min) {
|
||||
opts['min'] = item.min;
|
||||
}
|
||||
if (item.max) {
|
||||
opts['max'] = item.max;
|
||||
}
|
||||
|
||||
return opts;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
<select name="select" (change)="onChange($event)">
|
||||
<option selected disabled hidden value="">{{ label }}</option>
|
||||
<option *ngFor="let kv of data | async" [value]="kv.key">
|
||||
<select name="select" [(ngModel)]="selected" (change)="onChange($event)">
|
||||
<option [ngValue]="undefined" selected disabled hidden>
|
||||
{{ label }}
|
||||
</option>
|
||||
<option *ngFor="let kv of data | async" [ngValue]="kv.key">
|
||||
{{ kv.value }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
@@ -1,23 +1,39 @@
|
||||
import { Component, Input, inject } from '@angular/core';
|
||||
import { Component, Input, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { KeyValue } from '../../models/key-value';
|
||||
import { FilterService } from '../../services/filters.service';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'fbi-select',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, FormsModule],
|
||||
templateUrl: './select.component.html',
|
||||
styleUrl: './select.component.scss',
|
||||
})
|
||||
export class SelectComponent {
|
||||
export class SelectComponent implements OnInit {
|
||||
@Input() label!: string;
|
||||
@Input() key!: string;
|
||||
@Input() data!: Observable<KeyValue[]>;
|
||||
|
||||
selected: any = undefined;
|
||||
|
||||
private filterService = inject(FilterService);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.filterService.filters$.subscribe((f: KeyValue[]) => {
|
||||
let me = (f ?? [])
|
||||
.filter((i: KeyValue) => i.key === this.key)
|
||||
.find((_) => true);
|
||||
if (me) {
|
||||
this.selected = me.value;
|
||||
} else {
|
||||
this.selected = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onChange(event: any): void {
|
||||
this.filterService.set(this.key, event?.target?.value);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
{{ h.label }}
|
||||
</th>
|
||||
</tr>
|
||||
<tr *ngFor="let row of rows; let index">
|
||||
<tr *ngFor="let row of rows | async; let index">
|
||||
<td>{{ index }}</td>
|
||||
<td *ngFor="let h of header">
|
||||
{{ row[h.source] }}
|
||||
</td>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Header } from './header';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'fbi-table',
|
||||
@@ -11,5 +12,5 @@ import { Header } from './header';
|
||||
})
|
||||
export class TableComponent {
|
||||
@Input() header!: Header[];
|
||||
@Input() rows!: Record<string, any>[];
|
||||
@Input() rows!: Observable<Record<string, any>[]>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user