diff --git a/src/app/app.component.html b/src/app/app.component.html
index 891cfd7..86b775a 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,10 +1,24 @@
-
+
+
+
+
+
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 4803758..b125ba8 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -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[] = [];
+ header = [
+ { label: 'School', source: Milestone.School },
+ { label: 'Grade', source: Milestone.Grade },
+ ].map((data) => new Header(data));
+ rows: Observable[]> = 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();
+ }
}
diff --git a/src/app/cohort-section/cohort-section.component.html b/src/app/cohort-section/cohort-section.component.html
new file mode 100644
index 0000000..a09db69
--- /dev/null
+++ b/src/app/cohort-section/cohort-section.component.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/app/cohort-section/cohort-section.component.scss b/src/app/cohort-section/cohort-section.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/cohort-section/cohort-section.component.spec.ts b/src/app/cohort-section/cohort-section.component.spec.ts
new file mode 100644
index 0000000..a1f5778
--- /dev/null
+++ b/src/app/cohort-section/cohort-section.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [CohortSectionComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(CohortSectionComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/cohort-section/cohort-section.component.ts b/src/app/cohort-section/cohort-section.component.ts
new file mode 100644
index 0000000..a295e84
--- /dev/null
+++ b/src/app/cohort-section/cohort-section.component.ts
@@ -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[] = [];
+
+ 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[]) => {
+ this.data = data
+ .filter(
+ (d: Record) =>
+ county.includes(d[Milestone.County]) &&
+ school.includes(d[Milestone.School]) &&
+ cohort.includes(d[Milestone.Cohort])
+ )
+ .sort(
+ (a: Record, b: Record) =>
+ a['grade'] - b['grade']
+ );
+ });
+ }
+}
diff --git a/src/app/scores/scores.component.html b/src/app/scores/scores.component.html
new file mode 100644
index 0000000..ec198d3
--- /dev/null
+++ b/src/app/scores/scores.component.html
@@ -0,0 +1 @@
+
diff --git a/src/app/scores/scores.component.scss b/src/app/scores/scores.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/scores/scores.component.spec.ts b/src/app/scores/scores.component.spec.ts
new file mode 100644
index 0000000..3c5e6c0
--- /dev/null
+++ b/src/app/scores/scores.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ScoresComponent } from './scores.component';
+
+describe('ScoresComponent', () => {
+ let component: ScoresComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ScoresComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ScoresComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/scores/scores.component.ts b/src/app/scores/scores.component.ts
new file mode 100644
index 0000000..0449387
--- /dev/null
+++ b/src/app/scores/scores.component.ts
@@ -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[] = [];
+
+ private dataService = inject(DataService);
+
+ ngOnInit(): void {
+ const county = this.county;
+ const school = this.school;
+ const grade = this.grade;
+
+ this.dataService.Data.subscribe((data: Record[]) => {
+ this.data = data.filter(
+ (d: Record) =>
+ d[Milestone.County] === county &&
+ d[Milestone.School] === school &&
+ d[Milestone.Grade] === grade
+ );
+ });
+ }
+}
diff --git a/src/components/chart/chart-config.ts b/src/components/chart/chart-config.ts
new file mode 100644
index 0000000..fa35301
--- /dev/null
+++ b/src/components/chart/chart-config.ts
@@ -0,0 +1,7 @@
+import { ChartType } from 'chart.js';
+import { ChartAxis } from './chart-type';
+
+export interface ChartConfig {
+ type: ChartType;
+ axis: ChartAxis[];
+}
diff --git a/src/components/chart/chart-type.ts b/src/components/chart/chart-type.ts
index f9ae496..fa550bf 100644
--- a/src/components/chart/chart-type.ts
+++ b/src/components/chart/chart-type.ts
@@ -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)[];
+ 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;
+}
diff --git a/src/components/chart/chart.component.ts b/src/components/chart/chart.component.ts
index 819f94b..9c3a351 100644
--- a/src/components/chart/chart.component.ts
+++ b/src/components/chart/chart.component.ts
@@ -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[];
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);
}
}
diff --git a/src/components/chart/services/chart-config.service.ts b/src/components/chart/services/chart-config.service.ts
new file mode 100644
index 0000000..14857cd
--- /dev/null
+++ b/src/components/chart/services/chart-config.service.ts
@@ -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[]): any {
+ return {
+ type: chart?.type,
+ data: this.data(chart, data),
+ options: this.optionsService.options(chart, data),
+ } as any;
+ }
+
+ private data(chart: ChartConfig, data: Record[]): IData {
+ return {
+ labels: this.labelService.labels(chart, data),
+ datasets: this.datasetService.datasets(chart, data),
+ } as IData;
+ }
+}
diff --git a/src/components/chart/services/dataset.service.ts b/src/components/chart/services/dataset.service.ts
new file mode 100644
index 0000000..e076972
--- /dev/null
+++ b/src/components/chart/services/dataset.service.ts
@@ -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[]): 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[]): 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) => 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;
+ }
+}
diff --git a/src/components/chart/services/label.service.ts b/src/components/chart/services/label.service.ts
new file mode 100644
index 0000000..d201039
--- /dev/null
+++ b/src/components/chart/services/label.service.ts
@@ -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[] {
+ 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) =>
+ result.push(record[sources[0]])
+ );
+ });
+
+ return result;
+ }
+}
diff --git a/src/components/chart/services/options.service.ts b/src/components/chart/services/options.service.ts
new file mode 100644
index 0000000..8659401
--- /dev/null
+++ b/src/components/chart/services/options.service.ts
@@ -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[]): 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;
+ }
+}
diff --git a/src/components/chart/services/scale.service.ts b/src/components/chart/services/scale.service.ts
new file mode 100644
index 0000000..c1e7ee8
--- /dev/null
+++ b/src/components/chart/services/scale.service.ts
@@ -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[]): 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 {
+ const opts: Record = {};
+
+ 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;
+ }
+}
diff --git a/src/components/select/select.component.html b/src/components/select/select.component.html
index 72e5e0f..80b6c7e 100644
--- a/src/components/select/select.component.html
+++ b/src/components/select/select.component.html
@@ -1,6 +1,8 @@
-