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="Year" key="year" [data]="data.Years"></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="School" key="school" [data]="data.Schools"></fbi-select>
|
||||
</div>
|
||||
<div>
|
||||
<button (click)="onReset($event)">Reset</button>
|
||||
</div> -->
|
||||
<!-- <div>
|
||||
<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 { Header } from '../components/table/header';
|
||||
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({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [RouterOutlet, ChartComponent, SelectComponent, TableComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterOutlet,
|
||||
ChartComponent,
|
||||
SelectComponent,
|
||||
TableComponent,
|
||||
ScoresComponent,
|
||||
CohortSectionComponent,
|
||||
],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss',
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
title = 'Georgia Milestones';
|
||||
|
||||
county = '644';
|
||||
school = ['3067', '0605'];
|
||||
grades = [3, 4, 5];
|
||||
cohorts: string[] = [];
|
||||
|
||||
data = inject(DataService);
|
||||
filter = inject(FilterService);
|
||||
|
||||
header = [new Header({ label: 'School', source: 'school' })];
|
||||
rows: Record<string, any>[] = [];
|
||||
header = [
|
||||
{ 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 {
|
||||
this.data.Cohorts.subscribe((kv: KeyValue[]) => {
|
||||
this.cohorts = kv.map((i: KeyValue) => i.key);
|
||||
});
|
||||
// this.filter.filters$.subscribe((filters: KeyValue[]) => {
|
||||
// this.data.Data.pipe(take(1)).subscribe((data: 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',
|
||||
}
|
||||
|
||||
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>[]>;
|
||||
}
|
||||
|
||||
@@ -48,10 +48,14 @@ export class DataService {
|
||||
download: true,
|
||||
header: false,
|
||||
complete: (results: ParseResult<Record<string, any>>) => {
|
||||
const headers = this.getHeaders((results?.data ?? []).slice(1, 3));
|
||||
const lookup = (x: string, record: Record<string, any>): any => {
|
||||
const idx = headers.findIndex((h: string) => x === h);
|
||||
return (idx > -1 ? record[idx] : '') as any;
|
||||
const h = this.getHeaders((results?.data ?? []).slice(1, 3));
|
||||
const fs = (x: string, r: Record<string, any>): string => {
|
||||
const idx = h.findIndex((h: string) => x === h);
|
||||
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;
|
||||
this._cohorts.set(`${cohort}`, `Class of ${cohort}`);
|
||||
@@ -59,7 +63,7 @@ export class DataService {
|
||||
const data: 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
|
||||
if (isNaN(code)) return;
|
||||
|
||||
@@ -68,44 +72,39 @@ export class DataService {
|
||||
ms[Milestone.Year] = year;
|
||||
ms[Milestone.Grade] = grade;
|
||||
ms[Milestone.Cohort] = cohort;
|
||||
ms[Milestone.County] = lookup(Csv.County_Code, record);
|
||||
ms[Milestone.School] = lookup(Csv.School_Code, record);
|
||||
ms[Milestone.ELACount] = lookup(Csv.ELA_Count, record);
|
||||
ms[Milestone.ELAMean] = lookup(Csv.ELA_Mean, record);
|
||||
ms[Milestone.ELA0] = lookup(Csv.ELA_L0, record);
|
||||
ms[Milestone.ELA1] = lookup(Csv.ELA_L1, record);
|
||||
ms[Milestone.ELA2] = lookup(Csv.ELA_L2, record);
|
||||
ms[Milestone.ELA3] = lookup(Csv.ELA_L3, record);
|
||||
ms[Milestone.MathCount] = lookup(Csv.Math_Count, record);
|
||||
ms[Milestone.MathMean] = lookup(Csv.Math_Mean, record);
|
||||
ms[Milestone.Math0] = lookup(Csv.Math_L0, record);
|
||||
ms[Milestone.Math1] = lookup(Csv.Math_L1, record);
|
||||
ms[Milestone.Math2] = lookup(Csv.Math_L2, record);
|
||||
ms[Milestone.Math3] = lookup(Csv.Math_L3, record);
|
||||
ms[Milestone.SciCount] = lookup(Csv.Science_Count, record);
|
||||
ms[Milestone.SciMean] = lookup(Csv.Science_Mean, record);
|
||||
ms[Milestone.Sci0] = lookup(Csv.Science_L0, record);
|
||||
ms[Milestone.Sci1] = lookup(Csv.Science_L1, record);
|
||||
ms[Milestone.Sci2] = lookup(Csv.Science_L2, record);
|
||||
ms[Milestone.Sci3] = lookup(Csv.Science_L3, record);
|
||||
ms[Milestone.SocCount] = lookup(Csv.Social_Count, record);
|
||||
ms[Milestone.SocMean] = lookup(Csv.Social_Mean, record);
|
||||
ms[Milestone.Soc0] = lookup(Csv.Social_L0, record);
|
||||
ms[Milestone.Soc1] = lookup(Csv.Social_L1, record);
|
||||
ms[Milestone.Soc2] = lookup(Csv.Social_L2, record);
|
||||
ms[Milestone.Soc3] = lookup(Csv.Social_L3, record);
|
||||
ms[Milestone.County] = fs(Csv.County_Code, record);
|
||||
ms[Milestone.School] = fs(Csv.School_Code, record);
|
||||
ms[Milestone.ELACount] = fn(Csv.ELA_Count, record);
|
||||
ms[Milestone.ELAMean] = fn(Csv.ELA_Mean, record);
|
||||
ms[Milestone.ELA0] = fn(Csv.ELA_L0, record);
|
||||
ms[Milestone.ELA1] = fn(Csv.ELA_L1, record);
|
||||
ms[Milestone.ELA2] = fn(Csv.ELA_L2, record);
|
||||
ms[Milestone.ELA3] = fn(Csv.ELA_L3, record);
|
||||
ms[Milestone.MathCount] = fn(Csv.Math_Count, record);
|
||||
ms[Milestone.MathMean] = fn(Csv.Math_Mean, record);
|
||||
ms[Milestone.Math0] = fn(Csv.Math_L0, record);
|
||||
ms[Milestone.Math1] = fn(Csv.Math_L1, record);
|
||||
ms[Milestone.Math2] = fn(Csv.Math_L2, record);
|
||||
ms[Milestone.Math3] = fn(Csv.Math_L3, record);
|
||||
ms[Milestone.SciCount] = fn(Csv.Science_Count, record);
|
||||
ms[Milestone.SciMean] = fn(Csv.Science_Mean, record);
|
||||
ms[Milestone.Sci0] = fn(Csv.Science_L0, record);
|
||||
ms[Milestone.Sci1] = fn(Csv.Science_L1, record);
|
||||
ms[Milestone.Sci2] = fn(Csv.Science_L2, record);
|
||||
ms[Milestone.Sci3] = fn(Csv.Science_L3, record);
|
||||
ms[Milestone.SocCount] = fn(Csv.Social_Count, record);
|
||||
ms[Milestone.SocMean] = fn(Csv.Social_Mean, record);
|
||||
ms[Milestone.Soc0] = fn(Csv.Social_L0, record);
|
||||
ms[Milestone.Soc1] = fn(Csv.Social_L1, record);
|
||||
ms[Milestone.Soc2] = fn(Csv.Social_L2, record);
|
||||
ms[Milestone.Soc3] = fn(Csv.Social_L3, record);
|
||||
data.push(ms);
|
||||
|
||||
this._counties.set(
|
||||
lookup(Csv.County_Code, record),
|
||||
lookup(Csv.County_Label, record)
|
||||
);
|
||||
const county = fs(Csv.County_Code, record);
|
||||
this._counties.set(county, fs(Csv.County_Label, record));
|
||||
this._schools.set(
|
||||
`${lookup(Csv.County_Code, record)}-${lookup(
|
||||
Csv.School_Code,
|
||||
record
|
||||
)}`,
|
||||
lookup(Csv.School_Label, record)
|
||||
`${county}-${fs(Csv.School_Code, record)}`,
|
||||
fs(Csv.School_Label, record)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -22,4 +22,8 @@ export class FilterService {
|
||||
if (!!value) update.push(new KeyValue({ key: key, value: value }));
|
||||
this.filters = update;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.filters = [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user