saving progress, lots of charting decisions made
This commit is contained in:
@@ -1,10 +1,24 @@
|
|||||||
<div>
|
<!-- <div>
|
||||||
<fbi-select label="Class" key="cohort" [data]="data.Cohorts"></fbi-select>
|
<fbi-select label="Class" key="cohort" [data]="data.Cohorts"></fbi-select>
|
||||||
<fbi-select label="Year" key="year" [data]="data.Years"></fbi-select>
|
<fbi-select label="Year" key="year" [data]="data.Years"></fbi-select>
|
||||||
<fbi-select label="Grade" key="grade" [data]="data.Grades"></fbi-select>
|
<fbi-select label="Grade" key="grade" [data]="data.Grades"></fbi-select>
|
||||||
<fbi-select label="County" key="county" [data]="data.Counties"></fbi-select>
|
<fbi-select label="County" key="county" [data]="data.Counties"></fbi-select>
|
||||||
<fbi-select label="School" key="school" [data]="data.Schools"></fbi-select>
|
<fbi-select label="School" key="school" [data]="data.Schools"></fbi-select>
|
||||||
</div>
|
<button (click)="onReset($event)">Reset</button>
|
||||||
<div>
|
</div> -->
|
||||||
|
<!-- <div>
|
||||||
<fbi-table [header]="header" [rows]="rows"></fbi-table>
|
<fbi-table [header]="header" [rows]="rows"></fbi-table>
|
||||||
</div>
|
</div> -->
|
||||||
|
<!-- <fbi-scores
|
||||||
|
*ngFor="let grade of grades"
|
||||||
|
[grade]="grade"
|
||||||
|
[county]="county"
|
||||||
|
[school]="school"
|
||||||
|
></fbi-scores> -->
|
||||||
|
|
||||||
|
<fbi-cohort-section
|
||||||
|
*ngFor="let cohort of cohorts"
|
||||||
|
[cohort]="cohort"
|
||||||
|
[county]="county"
|
||||||
|
[school]="school"
|
||||||
|
></fbi-cohort-section>
|
||||||
|
|||||||
@@ -6,24 +6,49 @@ import { DataService } from '../services/data.service';
|
|||||||
import { TableComponent } from '../components/table/table.component';
|
import { TableComponent } from '../components/table/table.component';
|
||||||
import { Header } from '../components/table/header';
|
import { Header } from '../components/table/header';
|
||||||
import { FilterService } from '../services/filters.service';
|
import { FilterService } from '../services/filters.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { Milestone } from '../enums/milestone';
|
||||||
|
import { ScoresComponent } from './scores/scores.component';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CohortSectionComponent } from './cohort-section/cohort-section.component';
|
||||||
|
import { KeyValue } from '../models/key-value';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterOutlet, ChartComponent, SelectComponent, TableComponent],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
RouterOutlet,
|
||||||
|
ChartComponent,
|
||||||
|
SelectComponent,
|
||||||
|
TableComponent,
|
||||||
|
ScoresComponent,
|
||||||
|
CohortSectionComponent,
|
||||||
|
],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss',
|
styleUrl: './app.component.scss',
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit {
|
export class AppComponent implements OnInit {
|
||||||
title = 'Georgia Milestones';
|
title = 'Georgia Milestones';
|
||||||
|
|
||||||
|
county = '644';
|
||||||
|
school = ['3067', '0605'];
|
||||||
|
grades = [3, 4, 5];
|
||||||
|
cohorts: string[] = [];
|
||||||
|
|
||||||
data = inject(DataService);
|
data = inject(DataService);
|
||||||
filter = inject(FilterService);
|
filter = inject(FilterService);
|
||||||
|
|
||||||
header = [new Header({ label: 'School', source: 'school' })];
|
header = [
|
||||||
rows: Record<string, any>[] = [];
|
{ label: 'School', source: Milestone.School },
|
||||||
|
{ label: 'Grade', source: Milestone.Grade },
|
||||||
|
].map((data) => new Header(data));
|
||||||
|
rows: Observable<Record<string, any>[]> = this.data.Data;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.data.Cohorts.subscribe((kv: KeyValue[]) => {
|
||||||
|
this.cohorts = kv.map((i: KeyValue) => i.key);
|
||||||
|
});
|
||||||
// this.filter.filters$.subscribe((filters: KeyValue[]) => {
|
// this.filter.filters$.subscribe((filters: KeyValue[]) => {
|
||||||
// this.data.Data.pipe(take(1)).subscribe((data: Milestone[]) => {
|
// this.data.Data.pipe(take(1)).subscribe((data: Milestone[]) => {
|
||||||
// const result = data.filter((d: Milestone) => {
|
// const result = data.filter((d: Milestone) => {
|
||||||
@@ -46,4 +71,8 @@ export class AppComponent implements OnInit {
|
|||||||
// });
|
// });
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onReset(event: any): void {
|
||||||
|
this.filter.reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/app/cohort-section/cohort-section.component.html
Normal file
1
src/app/cohort-section/cohort-section.component.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<fbi-chart [config]="chart" [data]="data"></fbi-chart>
|
||||||
23
src/app/cohort-section/cohort-section.component.spec.ts
Normal file
23
src/app/cohort-section/cohort-section.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CohortSectionComponent } from './cohort-section.component';
|
||||||
|
|
||||||
|
describe('CohortSectionComponent', () => {
|
||||||
|
let component: CohortSectionComponent;
|
||||||
|
let fixture: ComponentFixture<CohortSectionComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CohortSectionComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CohortSectionComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
80
src/app/cohort-section/cohort-section.component.ts
Normal file
80
src/app/cohort-section/cohort-section.component.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { Component, Input, inject } from '@angular/core';
|
||||||
|
import { ChartComponent } from '../../components/chart/chart.component';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Milestone } from '../../enums/milestone';
|
||||||
|
import {
|
||||||
|
ChartAxisPosition,
|
||||||
|
ChartType,
|
||||||
|
} from '../../components/chart/chart-type';
|
||||||
|
import { ChartConfig } from '../../components/chart/chart-config';
|
||||||
|
import { DataService } from '../../services/data.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'fbi-cohort-section',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, ChartComponent],
|
||||||
|
templateUrl: './cohort-section.component.html',
|
||||||
|
styleUrl: './cohort-section.component.scss',
|
||||||
|
})
|
||||||
|
export class CohortSectionComponent {
|
||||||
|
@Input() county: string | string[] = '644';
|
||||||
|
@Input() school: string | string[] = '3067';
|
||||||
|
@Input() cohort!: string | string[];
|
||||||
|
|
||||||
|
chart: ChartConfig = {
|
||||||
|
type: ChartType.Line,
|
||||||
|
axis: [
|
||||||
|
{
|
||||||
|
label: 'Grade',
|
||||||
|
source: Milestone.Grade,
|
||||||
|
position: ChartAxisPosition.Bottom,
|
||||||
|
stacked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: [
|
||||||
|
'Beginning Learner',
|
||||||
|
'Developing Learner',
|
||||||
|
'Proficient Learner',
|
||||||
|
'Distinguished Learner',
|
||||||
|
],
|
||||||
|
source: [
|
||||||
|
Milestone.ELA0,
|
||||||
|
Milestone.ELA1,
|
||||||
|
Milestone.ELA2,
|
||||||
|
Milestone.ELA3,
|
||||||
|
],
|
||||||
|
position: ChartAxisPosition.Left,
|
||||||
|
fill: true,
|
||||||
|
stacked: true,
|
||||||
|
stack: 'ELA',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
data: Record<string, any>[] = [];
|
||||||
|
|
||||||
|
private dataService = inject(DataService);
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const county = Array.isArray(this.county) ? this.county : [this.county];
|
||||||
|
const school = Array.isArray(this.school) ? this.school : [this.school];
|
||||||
|
const cohort = (
|
||||||
|
Array.isArray(this.cohort) ? this.cohort : [this.cohort]
|
||||||
|
).map((v: string) => parseInt(v));
|
||||||
|
|
||||||
|
this.dataService.Data.subscribe((data: Record<string, any>[]) => {
|
||||||
|
this.data = data
|
||||||
|
.filter(
|
||||||
|
(d: Record<string, any>) =>
|
||||||
|
county.includes(d[Milestone.County]) &&
|
||||||
|
school.includes(d[Milestone.School]) &&
|
||||||
|
cohort.includes(d[Milestone.Cohort])
|
||||||
|
)
|
||||||
|
.sort(
|
||||||
|
(a: Record<string, any>, b: Record<string, any>) =>
|
||||||
|
a['grade'] - b['grade']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/app/scores/scores.component.html
Normal file
1
src/app/scores/scores.component.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<fbi-chart [config]="chart" [data]="data"></fbi-chart>
|
||||||
0
src/app/scores/scores.component.scss
Normal file
0
src/app/scores/scores.component.scss
Normal file
23
src/app/scores/scores.component.spec.ts
Normal file
23
src/app/scores/scores.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ScoresComponent } from './scores.component';
|
||||||
|
|
||||||
|
describe('ScoresComponent', () => {
|
||||||
|
let component: ScoresComponent;
|
||||||
|
let fixture: ComponentFixture<ScoresComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ScoresComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ScoresComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
92
src/app/scores/scores.component.ts
Normal file
92
src/app/scores/scores.component.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, Input, OnInit, inject } from '@angular/core';
|
||||||
|
import { ChartComponent } from '../../components/chart/chart.component';
|
||||||
|
import {
|
||||||
|
ChartAxisPosition,
|
||||||
|
ChartType,
|
||||||
|
} from '../../components/chart/chart-type';
|
||||||
|
import { ChartConfig } from '../../components/chart/chart-config';
|
||||||
|
import { DataService } from '../../services/data.service';
|
||||||
|
import { Milestone } from '../../enums/milestone';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'fbi-scores',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, ChartComponent],
|
||||||
|
templateUrl: './scores.component.html',
|
||||||
|
styleUrl: './scores.component.scss',
|
||||||
|
})
|
||||||
|
export class ScoresComponent implements OnInit {
|
||||||
|
@Input() county: string = '644';
|
||||||
|
@Input() school: string = '3067';
|
||||||
|
@Input() grade!: number;
|
||||||
|
|
||||||
|
chart: ChartConfig = {
|
||||||
|
type: ChartType.Bar,
|
||||||
|
axis: [
|
||||||
|
{
|
||||||
|
label: 'Year',
|
||||||
|
source: Milestone.Year,
|
||||||
|
position: ChartAxisPosition.Bottom,
|
||||||
|
stacked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: [
|
||||||
|
'Beginning Learner',
|
||||||
|
'Developing Learner',
|
||||||
|
'Proficient Learner',
|
||||||
|
'Distinguished Learner',
|
||||||
|
],
|
||||||
|
source: [
|
||||||
|
Milestone.ELA0,
|
||||||
|
Milestone.ELA1,
|
||||||
|
Milestone.ELA2,
|
||||||
|
Milestone.ELA3,
|
||||||
|
],
|
||||||
|
position: ChartAxisPosition.Left,
|
||||||
|
stacked: true,
|
||||||
|
stack: 'ELA',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: [
|
||||||
|
'Beginning Learner',
|
||||||
|
'Developing Learner',
|
||||||
|
'Proficient Learner',
|
||||||
|
'Distinguished Learner',
|
||||||
|
],
|
||||||
|
source: [
|
||||||
|
Milestone.Math0,
|
||||||
|
Milestone.Math1,
|
||||||
|
Milestone.Math2,
|
||||||
|
Milestone.Math3,
|
||||||
|
],
|
||||||
|
position: ChartAxisPosition.Right,
|
||||||
|
stacked: true,
|
||||||
|
stack: 'Math',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
data: Record<string, any>[] = [];
|
||||||
|
|
||||||
|
private dataService = inject(DataService);
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const county = this.county;
|
||||||
|
const school = this.school;
|
||||||
|
const grade = this.grade;
|
||||||
|
|
||||||
|
this.dataService.Data.subscribe((data: Record<string, any>[]) => {
|
||||||
|
this.data = data.filter(
|
||||||
|
(d: Record<string, any>) =>
|
||||||
|
d[Milestone.County] === county &&
|
||||||
|
d[Milestone.School] === school &&
|
||||||
|
d[Milestone.Grade] === grade
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
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',
|
Radar = 'radar',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ChartAxisPosition {
|
||||||
|
Top = 'top',
|
||||||
|
Right = 'right',
|
||||||
|
Bottom = 'bottom',
|
||||||
|
Left = 'left',
|
||||||
|
Center = 'center',
|
||||||
|
}
|
||||||
|
|
||||||
export interface IDataset {
|
export interface IDataset {
|
||||||
type?: ChartType;
|
type?: ChartType;
|
||||||
borderColor?: string;
|
borderColor?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
data: string[];
|
order?: number;
|
||||||
|
data: (string | Record<string, any>)[];
|
||||||
|
beginAtZero?: boolean;
|
||||||
|
fill?: string | number | boolean;
|
||||||
backgroundColor?: string | string[];
|
backgroundColor?: string | string[];
|
||||||
pointBackgroundColor?: string;
|
pointBackgroundColor?: string;
|
||||||
pointBorderColor?: string;
|
pointBorderColor?: string;
|
||||||
|
parsing?: {
|
||||||
|
xAxisKey?: string;
|
||||||
|
yAxisKey?: string;
|
||||||
|
};
|
||||||
|
stack?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IData {
|
export interface IData {
|
||||||
@@ -24,3 +40,15 @@ export interface IData {
|
|||||||
labelsSource?: string[];
|
labelsSource?: string[];
|
||||||
datasets: IDataset[];
|
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,
|
AfterViewInit,
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
|
SimpleChanges,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { Chart, ChartOptions } from 'chart.js/auto';
|
import { Chart } from 'chart.js/auto';
|
||||||
import { PluginNodata } from './plugin-nodata';
|
import { PluginNodata } from './plugin-nodata';
|
||||||
import { PluginMoreColors } from './plugin-more-colors';
|
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({
|
@Component({
|
||||||
selector: 'fbi-chart',
|
selector: 'fbi-chart',
|
||||||
@@ -16,50 +24,43 @@ import { ChartType, IData } from './chart-type';
|
|||||||
imports: [],
|
imports: [],
|
||||||
templateUrl: './chart.component.html',
|
templateUrl: './chart.component.html',
|
||||||
styleUrl: './chart.component.scss',
|
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;
|
@ViewChild('chart') canvas!: ElementRef;
|
||||||
|
@Input() config!: ChartConfig;
|
||||||
|
@Input() data!: Record<string, any>[];
|
||||||
|
|
||||||
private chart: any = undefined;
|
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 {
|
ngAfterViewInit(): void {
|
||||||
this.initChart();
|
this.initChart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
this.initChart();
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.chart?.destroy?.();
|
this.chart?.destroy?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
initChart() {
|
private initChart() {
|
||||||
|
if (!this.canvas?.nativeElement) return;
|
||||||
|
|
||||||
this.chart?.destroy?.();
|
this.chart?.destroy?.();
|
||||||
|
const config = this.chartConfigService.chart(this.config, this.data);
|
||||||
|
console.log(config);
|
||||||
|
|
||||||
const opts: ChartOptions = {};
|
config.plugins = [PluginNodata.config(), PluginMoreColors.config()];
|
||||||
opts.responsive = true;
|
this.chart = new Chart(this.canvas.nativeElement, config);
|
||||||
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[],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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)">
|
<select name="select" [(ngModel)]="selected" (change)="onChange($event)">
|
||||||
<option selected disabled hidden value="">{{ label }}</option>
|
<option [ngValue]="undefined" selected disabled hidden>
|
||||||
<option *ngFor="let kv of data | async" [value]="kv.key">
|
{{ label }}
|
||||||
|
</option>
|
||||||
|
<option *ngFor="let kv of data | async" [ngValue]="kv.key">
|
||||||
{{ kv.value }}
|
{{ kv.value }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</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 { CommonModule } from '@angular/common';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { KeyValue } from '../../models/key-value';
|
import { KeyValue } from '../../models/key-value';
|
||||||
import { FilterService } from '../../services/filters.service';
|
import { FilterService } from '../../services/filters.service';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'fbi-select',
|
selector: 'fbi-select',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule, FormsModule],
|
||||||
templateUrl: './select.component.html',
|
templateUrl: './select.component.html',
|
||||||
styleUrl: './select.component.scss',
|
styleUrl: './select.component.scss',
|
||||||
})
|
})
|
||||||
export class SelectComponent {
|
export class SelectComponent implements OnInit {
|
||||||
@Input() label!: string;
|
@Input() label!: string;
|
||||||
@Input() key!: string;
|
@Input() key!: string;
|
||||||
@Input() data!: Observable<KeyValue[]>;
|
@Input() data!: Observable<KeyValue[]>;
|
||||||
|
|
||||||
|
selected: any = undefined;
|
||||||
|
|
||||||
private filterService = inject(FilterService);
|
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 {
|
onChange(event: any): void {
|
||||||
this.filterService.set(this.key, event?.target?.value);
|
this.filterService.set(this.key, event?.target?.value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
{{ h.label }}
|
{{ h.label }}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</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">
|
<td *ngFor="let h of header">
|
||||||
{{ row[h.source] }}
|
{{ row[h.source] }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { Header } from './header';
|
import { Header } from './header';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'fbi-table',
|
selector: 'fbi-table',
|
||||||
@@ -11,5 +12,5 @@ import { Header } from './header';
|
|||||||
})
|
})
|
||||||
export class TableComponent {
|
export class TableComponent {
|
||||||
@Input() header!: Header[];
|
@Input() header!: Header[];
|
||||||
@Input() rows!: Record<string, any>[];
|
@Input() rows!: Observable<Record<string, any>[]>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,10 +48,14 @@ export class DataService {
|
|||||||
download: true,
|
download: true,
|
||||||
header: false,
|
header: false,
|
||||||
complete: (results: ParseResult<Record<string, any>>) => {
|
complete: (results: ParseResult<Record<string, any>>) => {
|
||||||
const headers = this.getHeaders((results?.data ?? []).slice(1, 3));
|
const h = this.getHeaders((results?.data ?? []).slice(1, 3));
|
||||||
const lookup = (x: string, record: Record<string, any>): any => {
|
const fs = (x: string, r: Record<string, any>): string => {
|
||||||
const idx = headers.findIndex((h: string) => x === h);
|
const idx = h.findIndex((h: string) => x === h);
|
||||||
return (idx > -1 ? record[idx] : '') as any;
|
return (idx > -1 ? r[idx] : '') as string;
|
||||||
|
};
|
||||||
|
const fn = (x: string, r: Record<string, any>): number => {
|
||||||
|
const idx = h.findIndex((h: string) => x === h);
|
||||||
|
return +(idx > -1 ? r[idx] : '');
|
||||||
};
|
};
|
||||||
const cohort = year + 12 - grade;
|
const cohort = year + 12 - grade;
|
||||||
this._cohorts.set(`${cohort}`, `Class of ${cohort}`);
|
this._cohorts.set(`${cohort}`, `Class of ${cohort}`);
|
||||||
@@ -59,7 +63,7 @@ export class DataService {
|
|||||||
const data: Record<string, any>[] = [];
|
const data: Record<string, any>[] = [];
|
||||||
|
|
||||||
(results?.data ?? []).forEach((record: Record<string, any>) => {
|
(results?.data ?? []).forEach((record: Record<string, any>) => {
|
||||||
let code = parseInt(lookup(Csv.County_Code, record));
|
let code = fn(Csv.County_Code, record);
|
||||||
// skip non-data rows on the csv
|
// skip non-data rows on the csv
|
||||||
if (isNaN(code)) return;
|
if (isNaN(code)) return;
|
||||||
|
|
||||||
@@ -68,44 +72,39 @@ export class DataService {
|
|||||||
ms[Milestone.Year] = year;
|
ms[Milestone.Year] = year;
|
||||||
ms[Milestone.Grade] = grade;
|
ms[Milestone.Grade] = grade;
|
||||||
ms[Milestone.Cohort] = cohort;
|
ms[Milestone.Cohort] = cohort;
|
||||||
ms[Milestone.County] = lookup(Csv.County_Code, record);
|
ms[Milestone.County] = fs(Csv.County_Code, record);
|
||||||
ms[Milestone.School] = lookup(Csv.School_Code, record);
|
ms[Milestone.School] = fs(Csv.School_Code, record);
|
||||||
ms[Milestone.ELACount] = lookup(Csv.ELA_Count, record);
|
ms[Milestone.ELACount] = fn(Csv.ELA_Count, record);
|
||||||
ms[Milestone.ELAMean] = lookup(Csv.ELA_Mean, record);
|
ms[Milestone.ELAMean] = fn(Csv.ELA_Mean, record);
|
||||||
ms[Milestone.ELA0] = lookup(Csv.ELA_L0, record);
|
ms[Milestone.ELA0] = fn(Csv.ELA_L0, record);
|
||||||
ms[Milestone.ELA1] = lookup(Csv.ELA_L1, record);
|
ms[Milestone.ELA1] = fn(Csv.ELA_L1, record);
|
||||||
ms[Milestone.ELA2] = lookup(Csv.ELA_L2, record);
|
ms[Milestone.ELA2] = fn(Csv.ELA_L2, record);
|
||||||
ms[Milestone.ELA3] = lookup(Csv.ELA_L3, record);
|
ms[Milestone.ELA3] = fn(Csv.ELA_L3, record);
|
||||||
ms[Milestone.MathCount] = lookup(Csv.Math_Count, record);
|
ms[Milestone.MathCount] = fn(Csv.Math_Count, record);
|
||||||
ms[Milestone.MathMean] = lookup(Csv.Math_Mean, record);
|
ms[Milestone.MathMean] = fn(Csv.Math_Mean, record);
|
||||||
ms[Milestone.Math0] = lookup(Csv.Math_L0, record);
|
ms[Milestone.Math0] = fn(Csv.Math_L0, record);
|
||||||
ms[Milestone.Math1] = lookup(Csv.Math_L1, record);
|
ms[Milestone.Math1] = fn(Csv.Math_L1, record);
|
||||||
ms[Milestone.Math2] = lookup(Csv.Math_L2, record);
|
ms[Milestone.Math2] = fn(Csv.Math_L2, record);
|
||||||
ms[Milestone.Math3] = lookup(Csv.Math_L3, record);
|
ms[Milestone.Math3] = fn(Csv.Math_L3, record);
|
||||||
ms[Milestone.SciCount] = lookup(Csv.Science_Count, record);
|
ms[Milestone.SciCount] = fn(Csv.Science_Count, record);
|
||||||
ms[Milestone.SciMean] = lookup(Csv.Science_Mean, record);
|
ms[Milestone.SciMean] = fn(Csv.Science_Mean, record);
|
||||||
ms[Milestone.Sci0] = lookup(Csv.Science_L0, record);
|
ms[Milestone.Sci0] = fn(Csv.Science_L0, record);
|
||||||
ms[Milestone.Sci1] = lookup(Csv.Science_L1, record);
|
ms[Milestone.Sci1] = fn(Csv.Science_L1, record);
|
||||||
ms[Milestone.Sci2] = lookup(Csv.Science_L2, record);
|
ms[Milestone.Sci2] = fn(Csv.Science_L2, record);
|
||||||
ms[Milestone.Sci3] = lookup(Csv.Science_L3, record);
|
ms[Milestone.Sci3] = fn(Csv.Science_L3, record);
|
||||||
ms[Milestone.SocCount] = lookup(Csv.Social_Count, record);
|
ms[Milestone.SocCount] = fn(Csv.Social_Count, record);
|
||||||
ms[Milestone.SocMean] = lookup(Csv.Social_Mean, record);
|
ms[Milestone.SocMean] = fn(Csv.Social_Mean, record);
|
||||||
ms[Milestone.Soc0] = lookup(Csv.Social_L0, record);
|
ms[Milestone.Soc0] = fn(Csv.Social_L0, record);
|
||||||
ms[Milestone.Soc1] = lookup(Csv.Social_L1, record);
|
ms[Milestone.Soc1] = fn(Csv.Social_L1, record);
|
||||||
ms[Milestone.Soc2] = lookup(Csv.Social_L2, record);
|
ms[Milestone.Soc2] = fn(Csv.Social_L2, record);
|
||||||
ms[Milestone.Soc3] = lookup(Csv.Social_L3, record);
|
ms[Milestone.Soc3] = fn(Csv.Social_L3, record);
|
||||||
data.push(ms);
|
data.push(ms);
|
||||||
|
|
||||||
this._counties.set(
|
const county = fs(Csv.County_Code, record);
|
||||||
lookup(Csv.County_Code, record),
|
this._counties.set(county, fs(Csv.County_Label, record));
|
||||||
lookup(Csv.County_Label, record)
|
|
||||||
);
|
|
||||||
this._schools.set(
|
this._schools.set(
|
||||||
`${lookup(Csv.County_Code, record)}-${lookup(
|
`${county}-${fs(Csv.School_Code, record)}`,
|
||||||
Csv.School_Code,
|
fs(Csv.School_Label, record)
|
||||||
record
|
|
||||||
)}`,
|
|
||||||
lookup(Csv.School_Label, record)
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -22,4 +22,8 @@ export class FilterService {
|
|||||||
if (!!value) update.push(new KeyValue({ key: key, value: value }));
|
if (!!value) update.push(new KeyValue({ key: key, value: value }));
|
||||||
this.filters = update;
|
this.filters = update;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.filters = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user