saving progress, lots of charting decisions made

This commit is contained in:
2024-06-06 13:53:51 -04:00
parent bf74aeeba3
commit e88499cbaf
24 changed files with 612 additions and 87 deletions

View File

@@ -0,0 +1,7 @@
import { ChartType } from 'chart.js';
import { ChartAxis } from './chart-type';
export interface ChartConfig {
type: ChartType;
axis: ChartAxis[];
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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>[]>;
}