Compare commits
2 Commits
e88499cbaf
...
1e3dc2938b
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e3dc2938b | |||
| 224e6388c1 |
@@ -25,7 +25,8 @@
|
|||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": ["src/favicon.ico", "src/assets"],
|
"assets": ["src/favicon.ico", "src/assets"],
|
||||||
"styles": ["src/styles.scss"],
|
"styles": ["src/styles.scss"],
|
||||||
"scripts": []
|
"scripts": [],
|
||||||
|
"allowedCommonJsDependencies": ["papaparse"]
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
|
|||||||
49
package-lock.json
generated
49
package-lock.json
generated
@@ -16,6 +16,8 @@
|
|||||||
"@angular/platform-browser": "^17.3.0",
|
"@angular/platform-browser": "^17.3.0",
|
||||||
"@angular/platform-browser-dynamic": "^17.3.0",
|
"@angular/platform-browser-dynamic": "^17.3.0",
|
||||||
"@angular/router": "^17.3.0",
|
"@angular/router": "^17.3.0",
|
||||||
|
"@fortawesome/angular-fontawesome": "^0.14.1",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||||
"chart.js": "^4.4.3",
|
"chart.js": "^4.4.3",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
@@ -2683,6 +2685,53 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fortawesome/angular-fontawesome": {
|
||||||
|
"version": "0.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.14.1.tgz",
|
||||||
|
"integrity": "sha512-Yb5HLiEOAxjSLEcaOM51CKIrzdfvoDafXVJERm9vufxfZkVZPZJgrZRgqwLVpejgq4/Ez6TqHZ6SqmJwdtRF6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/core": "^17.0.0",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "~1.2.27 || ~1.3.0-beta2 || ^6.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||||
|
"version": "6.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
|
||||||
|
"integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||||
|
"version": "6.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
|
||||||
|
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||||
|
"version": "6.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
|
||||||
|
"integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
|
||||||
|
"license": "(CC-BY-4.0 AND MIT)",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
"@angular/platform-browser": "^17.3.0",
|
"@angular/platform-browser": "^17.3.0",
|
||||||
"@angular/platform-browser-dynamic": "^17.3.0",
|
"@angular/platform-browser-dynamic": "^17.3.0",
|
||||||
"@angular/router": "^17.3.0",
|
"@angular/router": "^17.3.0",
|
||||||
|
"@fortawesome/angular-fontawesome": "^0.14.1",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||||
"chart.js": "^4.4.3",
|
"chart.js": "^4.4.3",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
|
|||||||
@@ -16,9 +16,17 @@
|
|||||||
[school]="school"
|
[school]="school"
|
||||||
></fbi-scores> -->
|
></fbi-scores> -->
|
||||||
|
|
||||||
<fbi-cohort-section
|
<!-- <fbi-cohort-section
|
||||||
*ngFor="let cohort of cohorts"
|
*ngFor="let cohort of cohorts"
|
||||||
[cohort]="cohort"
|
[cohort]="cohort"
|
||||||
[county]="county"
|
[county]="county"
|
||||||
[school]="school"
|
[school]="school"
|
||||||
></fbi-cohort-section>
|
></fbi-cohort-section> -->
|
||||||
|
|
||||||
|
<div class="menu">
|
||||||
|
<fbi-query class="query"></fbi-query>
|
||||||
|
<fbi-metadata class="metadata"></fbi-metadata>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<fbi-result class="result"></fbi-result>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,29 @@
|
|||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
width: 200px;
|
||||||
|
height: calc(100vh - 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.query {
|
||||||
|
height: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ChartComponent } from '../components/chart/chart.component';
|
|||||||
import { SelectComponent } from '../components/select/select.component';
|
import { SelectComponent } from '../components/select/select.component';
|
||||||
import { DataService } from '../services/data.service';
|
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 '../models/header';
|
||||||
import { FilterService } from '../services/filters.service';
|
import { FilterService } from '../services/filters.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Milestone } from '../enums/milestone';
|
import { Milestone } from '../enums/milestone';
|
||||||
@@ -12,6 +12,9 @@ import { ScoresComponent } from './scores/scores.component';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CohortSectionComponent } from './cohort-section/cohort-section.component';
|
import { CohortSectionComponent } from './cohort-section/cohort-section.component';
|
||||||
import { KeyValue } from '../models/key-value';
|
import { KeyValue } from '../models/key-value';
|
||||||
|
import { QueryComponent } from '../components/query/query.component';
|
||||||
|
import { MetadataComponent } from '../components/metadata/metadata.component';
|
||||||
|
import { ResultComponent } from '../components/result/result.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -24,6 +27,9 @@ import { KeyValue } from '../models/key-value';
|
|||||||
TableComponent,
|
TableComponent,
|
||||||
ScoresComponent,
|
ScoresComponent,
|
||||||
CohortSectionComponent,
|
CohortSectionComponent,
|
||||||
|
QueryComponent,
|
||||||
|
MetadataComponent,
|
||||||
|
ResultComponent,
|
||||||
],
|
],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss',
|
styleUrl: './app.component.scss',
|
||||||
@@ -43,10 +49,10 @@ export class AppComponent implements OnInit {
|
|||||||
{ label: 'School', source: Milestone.School },
|
{ label: 'School', source: Milestone.School },
|
||||||
{ label: 'Grade', source: Milestone.Grade },
|
{ label: 'Grade', source: Milestone.Grade },
|
||||||
].map((data) => new Header(data));
|
].map((data) => new Header(data));
|
||||||
rows: Observable<Record<string, any>[]> = this.data.Data;
|
rows: Observable<Record<string, any>[]> = this.data.Data$;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.data.Cohorts.subscribe((kv: KeyValue[]) => {
|
this.data.Cohorts$.subscribe((kv: KeyValue[]) => {
|
||||||
this.cohorts = kv.map((i: KeyValue) => i.key);
|
this.cohorts = kv.map((i: KeyValue) => i.key);
|
||||||
});
|
});
|
||||||
// this.filter.filters$.subscribe((filters: KeyValue[]) => {
|
// this.filter.filters$.subscribe((filters: KeyValue[]) => {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<fbi-chart [config]="chart" [data]="data"></fbi-chart>
|
<fbi-chart [config]="chart" [data]="data" class="chart"></fbi-chart>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.chart {
|
||||||
|
height: 400px;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from '../../components/chart/chart-type';
|
} from '../../components/chart/chart-type';
|
||||||
import { ChartConfig } from '../../components/chart/chart-config';
|
import { ChartConfig } from '../../components/chart/chart-config';
|
||||||
import { DataService } from '../../services/data.service';
|
import { DataService } from '../../services/data.service';
|
||||||
|
import { KeyValue } from '../../models/key-value';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'fbi-cohort-section',
|
selector: 'fbi-cohort-section',
|
||||||
@@ -63,7 +64,14 @@ export class CohortSectionComponent {
|
|||||||
Array.isArray(this.cohort) ? this.cohort : [this.cohort]
|
Array.isArray(this.cohort) ? this.cohort : [this.cohort]
|
||||||
).map((v: string) => parseInt(v));
|
).map((v: string) => parseInt(v));
|
||||||
|
|
||||||
this.dataService.Data.subscribe((data: Record<string, any>[]) => {
|
this.dataService.Cohorts$.subscribe(
|
||||||
|
(cohorts: KeyValue[]) =>
|
||||||
|
(this.chart.title = cohorts
|
||||||
|
.map((cohort: KeyValue) => cohort.value)
|
||||||
|
.join(', '))
|
||||||
|
);
|
||||||
|
|
||||||
|
this.dataService.Data$.subscribe((data: Record<string, any>[]) => {
|
||||||
this.data = data
|
this.data = data
|
||||||
.filter(
|
.filter(
|
||||||
(d: Record<string, any>) =>
|
(d: Record<string, any>) =>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export class ScoresComponent implements OnInit {
|
|||||||
const school = this.school;
|
const school = this.school;
|
||||||
const grade = this.grade;
|
const grade = this.grade;
|
||||||
|
|
||||||
this.dataService.Data.subscribe((data: Record<string, any>[]) => {
|
this.dataService.Data$.subscribe((data: Record<string, any>[]) => {
|
||||||
this.data = data.filter(
|
this.data = data.filter(
|
||||||
(d: Record<string, any>) =>
|
(d: Record<string, any>) =>
|
||||||
d[Milestone.County] === county &&
|
d[Milestone.County] === county &&
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ import { ChartAxis } from './chart-type';
|
|||||||
|
|
||||||
export interface ChartConfig {
|
export interface ChartConfig {
|
||||||
type: ChartType;
|
type: ChartType;
|
||||||
|
title?: string;
|
||||||
axis: ChartAxis[];
|
axis: ChartAxis[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export interface IDataset {
|
|||||||
xAxisKey?: string;
|
xAxisKey?: string;
|
||||||
yAxisKey?: string;
|
yAxisKey?: string;
|
||||||
};
|
};
|
||||||
|
spanGaps?: boolean;
|
||||||
stack?: string;
|
stack?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<canvas #chart></canvas>
|
<canvas #chart class="fit"></canvas>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.fit {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export class DatasetService {
|
|||||||
if (axis.fill) {
|
if (axis.fill) {
|
||||||
entry.fill = axis.fill;
|
entry.fill = axis.fill;
|
||||||
}
|
}
|
||||||
|
entry.spanGaps = true;
|
||||||
result.push(entry);
|
result.push(entry);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,18 @@ export class OptionsService {
|
|||||||
result.maintainAspectRatio = false;
|
result.maintainAspectRatio = false;
|
||||||
result.scales = this.scaleService.scales(chart, data);
|
result.scales = this.scaleService.scales(chart, data);
|
||||||
|
|
||||||
|
if (chart.title) {
|
||||||
|
result.plugins = result.plugins ?? {};
|
||||||
|
result.plugins.title = result.plugins.title ?? {};
|
||||||
|
result.plugins.title.text = chart.title;
|
||||||
|
}
|
||||||
|
|
||||||
const fill =
|
const fill =
|
||||||
(chart?.axis ?? []).filter((axis: ChartAxis) => axis.fill).length > 0;
|
(chart?.axis ?? []).filter((axis: ChartAxis) => axis.fill).length > 0;
|
||||||
if (fill) {
|
if (fill) {
|
||||||
result.plugins = result.plugins ?? {};
|
result.plugins = result.plugins ?? {};
|
||||||
result.plugins.filler = { propagate: true };
|
result.plugins.filler = result.plugins.filler ?? {};
|
||||||
|
result.plugins.filler.propagate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
1
src/components/context-menu/context-menu.component.html
Normal file
1
src/components/context-menu/context-menu.component.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<p>context-menu works!</p>
|
||||||
23
src/components/context-menu/context-menu.component.spec.ts
Normal file
23
src/components/context-menu/context-menu.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ContextMenuComponent } from './context-menu.component';
|
||||||
|
|
||||||
|
describe('ContextMenuComponent', () => {
|
||||||
|
let component: ContextMenuComponent;
|
||||||
|
let fixture: ComponentFixture<ContextMenuComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ContextMenuComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ContextMenuComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
12
src/components/context-menu/context-menu.component.ts
Normal file
12
src/components/context-menu/context-menu.component.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'fbi-context-menu',
|
||||||
|
standalone: true,
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './context-menu.component.html',
|
||||||
|
styleUrl: './context-menu.component.scss'
|
||||||
|
})
|
||||||
|
export class ContextMenuComponent {
|
||||||
|
|
||||||
|
}
|
||||||
6
src/components/metadata/metadata.component.html
Normal file
6
src/components/metadata/metadata.component.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<div class="header">Fields!</div>
|
||||||
|
<fbi-tree
|
||||||
|
class="tree"
|
||||||
|
[node]="node"
|
||||||
|
(actionClick)="onActionClick($event)"
|
||||||
|
></fbi-tree>
|
||||||
17
src/components/metadata/metadata.component.scss
Normal file
17
src/components/metadata/metadata.component.scss
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0px;
|
||||||
|
border: 1px solid black;
|
||||||
|
padding-left: 4px;
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree {
|
||||||
|
flex: 1;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
23
src/components/metadata/metadata.component.spec.ts
Normal file
23
src/components/metadata/metadata.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MetadataComponent } from './metadata.component';
|
||||||
|
|
||||||
|
describe('MetadataComponent', () => {
|
||||||
|
let component: MetadataComponent;
|
||||||
|
let fixture: ComponentFixture<MetadataComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [MetadataComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(MetadataComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
75
src/components/metadata/metadata.component.ts
Normal file
75
src/components/metadata/metadata.component.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { TreeComponent } from '../tree/tree.component';
|
||||||
|
import { TreeNode } from '../../models/tree-node';
|
||||||
|
import { MetaService } from '../../services/meta.service';
|
||||||
|
import { Action } from '../../models/action';
|
||||||
|
import { faAdd } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { QueryService } from '../../services/query.service';
|
||||||
|
import { combineLatest } from 'rxjs';
|
||||||
|
import { Query } from '../../models/query';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'fbi-metadata',
|
||||||
|
standalone: true,
|
||||||
|
imports: [TreeComponent],
|
||||||
|
templateUrl: './metadata.component.html',
|
||||||
|
styleUrl: './metadata.component.scss',
|
||||||
|
})
|
||||||
|
export class MetadataComponent {
|
||||||
|
node: TreeNode = new TreeNode({});
|
||||||
|
|
||||||
|
constructor(metaService: MetaService, private queryService: QueryService) {
|
||||||
|
combineLatest({
|
||||||
|
meta: metaService.Data,
|
||||||
|
query: queryService.Query,
|
||||||
|
}).subscribe((d: { meta: Partial<TreeNode>[]; query: Query }) => {
|
||||||
|
const inuse = d.query.fields;
|
||||||
|
const expanded = this.getExpanded(this.node);
|
||||||
|
|
||||||
|
const recurse = (node: Partial<TreeNode>) => {
|
||||||
|
node.hidden = inuse.includes(node.data);
|
||||||
|
node.expanded = expanded.includes(node.data);
|
||||||
|
|
||||||
|
const children = node.children ?? [];
|
||||||
|
children.forEach((child: Partial<TreeNode>) => recurse(child));
|
||||||
|
if (children.length === 0) {
|
||||||
|
const actions = [{ label: 'Add', icon: faAdd, data: ACTIONS.ADD }];
|
||||||
|
node.actions = actions as Action[];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(d.meta ?? []).forEach((node: Partial<TreeNode>) => recurse(node));
|
||||||
|
|
||||||
|
this.node = new TreeNode({
|
||||||
|
hidden: true,
|
||||||
|
expanded: true,
|
||||||
|
children: d.meta as TreeNode[],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onActionClick(event: { action: Action; node: TreeNode }): void {
|
||||||
|
switch (event.action.data) {
|
||||||
|
case ACTIONS.ADD:
|
||||||
|
this.queryService.add(event.node.data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getExpanded(node: TreeNode): string[] {
|
||||||
|
const result: string[] = [];
|
||||||
|
|
||||||
|
if (node.expanded) {
|
||||||
|
result.push(node.data);
|
||||||
|
}
|
||||||
|
(node.children ?? []).forEach((child: TreeNode) =>
|
||||||
|
result.push(...this.getExpanded(child))
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ACTIONS {
|
||||||
|
ADD = 'add',
|
||||||
|
}
|
||||||
6
src/components/query/query.component.html
Normal file
6
src/components/query/query.component.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<div class="header">Query!</div>
|
||||||
|
<fbi-tree
|
||||||
|
class="tree"
|
||||||
|
[node]="node"
|
||||||
|
(actionClick)="onActionClick($event)"
|
||||||
|
></fbi-tree>
|
||||||
17
src/components/query/query.component.scss
Normal file
17
src/components/query/query.component.scss
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0px;
|
||||||
|
border: 1px solid black;
|
||||||
|
padding-left: 4px;
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree {
|
||||||
|
flex: 1;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
23
src/components/query/query.component.spec.ts
Normal file
23
src/components/query/query.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { QueryComponent } from './query.component';
|
||||||
|
|
||||||
|
describe('QueryComponent', () => {
|
||||||
|
let component: QueryComponent;
|
||||||
|
let fixture: ComponentFixture<QueryComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [QueryComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(QueryComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
106
src/components/query/query.component.ts
Normal file
106
src/components/query/query.component.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { TreeComponent } from '../tree/tree.component';
|
||||||
|
import { TreeNode } from '../../models/tree-node';
|
||||||
|
import { QueryService } from '../../services/query.service';
|
||||||
|
import { Query } from '../../models/query';
|
||||||
|
import { Action } from '../../models/action';
|
||||||
|
import {
|
||||||
|
faArrowDown,
|
||||||
|
faArrowUp,
|
||||||
|
faRemove,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { ExecuteService } from '../../services/execute.service';
|
||||||
|
import { Header } from '../../models/header';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'fbi-query',
|
||||||
|
standalone: true,
|
||||||
|
imports: [TreeComponent],
|
||||||
|
templateUrl: './query.component.html',
|
||||||
|
styleUrl: './query.component.scss',
|
||||||
|
})
|
||||||
|
export class QueryComponent {
|
||||||
|
node: TreeNode = new TreeNode({});
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private queryService: QueryService,
|
||||||
|
executeService: ExecuteService
|
||||||
|
) {
|
||||||
|
queryService.Query.subscribe((query: Query) => {
|
||||||
|
executeService.headers(query).subscribe((headers: Header[]) => {
|
||||||
|
this.node = new TreeNode({
|
||||||
|
hidden: true,
|
||||||
|
expanded: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: 'Fields',
|
||||||
|
leaf: false,
|
||||||
|
expanded: true,
|
||||||
|
children: (query?.fields ?? []).map(
|
||||||
|
(field: string, index: number, array: string[]) => {
|
||||||
|
const actions = [];
|
||||||
|
if (index !== 0) {
|
||||||
|
actions.push({
|
||||||
|
label: 'Move Up',
|
||||||
|
icon: faArrowUp,
|
||||||
|
data: { cmd: ACTIONS.UP, data: index - 1 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (index !== array.length - 1) {
|
||||||
|
actions.push({
|
||||||
|
label: 'Move Down',
|
||||||
|
icon: faArrowDown,
|
||||||
|
data: { cmd: ACTIONS.DOWN, data: index + 1 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
actions.push({
|
||||||
|
label: 'Remove',
|
||||||
|
icon: faRemove,
|
||||||
|
data: { cmd: ACTIONS.REMOVE },
|
||||||
|
});
|
||||||
|
|
||||||
|
const label =
|
||||||
|
headers.find((header: Header) => header.source === field)
|
||||||
|
?.label ?? 'unknown';
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: label,
|
||||||
|
data: field,
|
||||||
|
actions: actions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ label: 'Filters', expanded: true, leaf: false },
|
||||||
|
] as TreeNode[],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onActionClick(event: { action: Action; node: TreeNode }): void {
|
||||||
|
const data = event.action.data as ActionData;
|
||||||
|
switch (data.cmd) {
|
||||||
|
case ACTIONS.REMOVE:
|
||||||
|
this.queryService.remove(event.node.data);
|
||||||
|
break;
|
||||||
|
case ACTIONS.UP:
|
||||||
|
this.queryService.add(event.node.data, data.data);
|
||||||
|
break;
|
||||||
|
case ACTIONS.DOWN:
|
||||||
|
this.queryService.add(event.node.data, data.data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ACTIONS {
|
||||||
|
REMOVE = 'del',
|
||||||
|
UP = 'up',
|
||||||
|
DOWN = 'down',
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionData = {
|
||||||
|
cmd: ACTIONS;
|
||||||
|
data: any;
|
||||||
|
};
|
||||||
1
src/components/result/result.component.html
Normal file
1
src/components/result/result.component.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<fbi-table [headers]="headers" [rows]="rows"></fbi-table>
|
||||||
3
src/components/result/result.component.scss
Normal file
3
src/components/result/result.component.scss
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
23
src/components/result/result.component.spec.ts
Normal file
23
src/components/result/result.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ResultComponent } from './result.component';
|
||||||
|
|
||||||
|
describe('ResultComponent', () => {
|
||||||
|
let component: ResultComponent;
|
||||||
|
let fixture: ComponentFixture<ResultComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ResultComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ResultComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
49
src/components/result/result.component.ts
Normal file
49
src/components/result/result.component.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { TreeComponent } from '../tree/tree.component';
|
||||||
|
import { QueryService } from '../../services/query.service';
|
||||||
|
import { Query } from '../../models/query';
|
||||||
|
import { ExecuteService } from '../../services/execute.service';
|
||||||
|
import { forkJoin } from 'rxjs';
|
||||||
|
import { Header } from '../../models/header';
|
||||||
|
import { TableComponent } from '../table/table.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'fbi-result',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, TreeComponent, TableComponent],
|
||||||
|
templateUrl: './result.component.html',
|
||||||
|
styleUrl: './result.component.scss',
|
||||||
|
})
|
||||||
|
export class ResultComponent {
|
||||||
|
headers: Header[] = [];
|
||||||
|
rows: Record<string, any>[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
queryService: QueryService,
|
||||||
|
private executeService: ExecuteService
|
||||||
|
) {
|
||||||
|
let last: string = '';
|
||||||
|
queryService.Query.subscribe((query: Query) => {
|
||||||
|
if (query.isValid()) {
|
||||||
|
const current = query.toString();
|
||||||
|
if (last !== current) {
|
||||||
|
this.load(query);
|
||||||
|
last = current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private load(query: Query): void {
|
||||||
|
forkJoin({
|
||||||
|
headers: this.executeService.headers(query),
|
||||||
|
data: this.executeService.data(query),
|
||||||
|
}).subscribe(
|
||||||
|
(result: { headers: Header[]; data: Record<string, any>[] }) => {
|
||||||
|
this.headers = result.headers;
|
||||||
|
this.rows = result.data;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th *ngFor="let h of header">
|
<th></th>
|
||||||
|
<th *ngFor="let h of headers">
|
||||||
{{ h.label }}
|
{{ h.label }}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngFor="let row of rows | async; let index">
|
<tr *ngFor="let row of rows; index as i">
|
||||||
<td>{{ index }}</td>
|
<td>{{ i }}</td>
|
||||||
<td *ngFor="let h of header">
|
<td *ngFor="let h of headers">
|
||||||
{{ row[h.source] }}
|
{{ row[h.source] }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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 '../../models/header';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'fbi-table',
|
selector: 'fbi-table',
|
||||||
@@ -11,6 +10,6 @@ import { Observable } from 'rxjs';
|
|||||||
styleUrl: './table.component.scss',
|
styleUrl: './table.component.scss',
|
||||||
})
|
})
|
||||||
export class TableComponent {
|
export class TableComponent {
|
||||||
@Input() header!: Header[];
|
@Input() headers!: Header[];
|
||||||
@Input() rows!: Observable<Record<string, any>[]>;
|
@Input() rows!: Record<string, any>[];
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/components/tree/tree.component.html
Normal file
32
src/components/tree/tree.component.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<div
|
||||||
|
*ngIf="!node?.hidden"
|
||||||
|
class="treenode"
|
||||||
|
[class.disabled]="node.disabled"
|
||||||
|
[class.clickable]="!node.leaf"
|
||||||
|
(click)="toggle($event)"
|
||||||
|
>
|
||||||
|
<span *ngIf="!node.leaf" class="icon">
|
||||||
|
<fa-icon *ngIf="node.expanded" [icon]="faCaretDown"></fa-icon>
|
||||||
|
<fa-icon *ngIf="!node.expanded" [icon]="faCaretRight"></fa-icon>
|
||||||
|
</span>
|
||||||
|
<span [title]="node.label" class="fill ellipsis">{{ node.label }}</span>
|
||||||
|
<ng-container *ngIf="node.leaf && !node.disabled" class="actions">
|
||||||
|
<fa-icon
|
||||||
|
*ngFor="let action of node.actions"
|
||||||
|
class="action clickable"
|
||||||
|
[icon]="action.icon"
|
||||||
|
(click)="onActionClick($event, action)"
|
||||||
|
></fa-icon>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
*ngIf="node && node.children && node.expanded"
|
||||||
|
[class.branch]="node.label !== ''"
|
||||||
|
>
|
||||||
|
<fbi-tree
|
||||||
|
*ngFor="let child of node.children"
|
||||||
|
[node]="child"
|
||||||
|
(actionClick)="passActionClick($event)"
|
||||||
|
></fbi-tree>
|
||||||
|
</div>
|
||||||
56
src/components/tree/tree.component.scss
Normal file
56
src/components/tree/tree.component.scss
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
.branch {
|
||||||
|
margin-left: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treenode {
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treenode:hover {
|
||||||
|
background-color: #edf8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
color: dimgray;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treenode:hover > .action {
|
||||||
|
visibility: visible;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0px;
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
22
src/components/tree/tree.component.spec.ts
Normal file
22
src/components/tree/tree.component.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TreeComponent } from './tree.component';
|
||||||
|
|
||||||
|
describe('TreeComponent', () => {
|
||||||
|
let component: TreeComponent;
|
||||||
|
let fixture: ComponentFixture<TreeComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [TreeComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(TreeComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
40
src/components/tree/tree.component.ts
Normal file
40
src/components/tree/tree.component.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { TreeNode } from '../../models/tree-node';
|
||||||
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
|
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { Action } from '../../models/action';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'fbi-tree',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FontAwesomeModule],
|
||||||
|
templateUrl: './tree.component.html',
|
||||||
|
styleUrl: './tree.component.scss',
|
||||||
|
})
|
||||||
|
export class TreeComponent {
|
||||||
|
@Input() node!: TreeNode;
|
||||||
|
@Output() actionClick = new EventEmitter<{
|
||||||
|
action: Action;
|
||||||
|
node: TreeNode;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
faCaretDown = faCaretDown;
|
||||||
|
faCaretRight = faCaretRight;
|
||||||
|
|
||||||
|
toggle(event: MouseEvent): void {
|
||||||
|
if (!this.node.leaf) {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.node.expanded = !this.node.expanded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
passActionClick(event: { action: Action; node: TreeNode }): void {
|
||||||
|
this.actionClick.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onActionClick(event: MouseEvent, action: Action): void {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.actionClick.emit({ action: action, node: this.node });
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/models/action.ts
Normal file
13
src/models/action.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { faDumbbell, IconDefinition } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
export class Action {
|
||||||
|
label: string;
|
||||||
|
icon: IconDefinition;
|
||||||
|
data: any;
|
||||||
|
|
||||||
|
constructor(data: Partial<Action>) {
|
||||||
|
this.label = data?.label ?? '';
|
||||||
|
this.icon = data?.icon ?? faDumbbell;
|
||||||
|
this.data = data?.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/models/page.ts
Normal file
13
src/models/page.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export class Page {
|
||||||
|
start: number;
|
||||||
|
limit: number;
|
||||||
|
|
||||||
|
constructor(data: Partial<Page>) {
|
||||||
|
this.start = data?.start ?? 1;
|
||||||
|
this.limit = data?.limit ?? 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `(${this.start}:${this.limit})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/models/query.ts
Normal file
23
src/models/query.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Page } from './page';
|
||||||
|
|
||||||
|
export class Query {
|
||||||
|
fields: string[];
|
||||||
|
filter: string[];
|
||||||
|
page: Page;
|
||||||
|
|
||||||
|
constructor(data: Partial<Query>) {
|
||||||
|
this.fields = data?.fields ?? [];
|
||||||
|
this.filter = data?.filter ?? [];
|
||||||
|
this.page = new Page(data?.page ?? {});
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
const fields = (this.fields ?? []).join(', ');
|
||||||
|
const filters = '';
|
||||||
|
return `${fields}${filters}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/models/tree-node.ts
Normal file
38
src/models/tree-node.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Action } from './action';
|
||||||
|
|
||||||
|
export class TreeNode {
|
||||||
|
label: string;
|
||||||
|
data: any;
|
||||||
|
children: TreeNode[];
|
||||||
|
leaf: boolean;
|
||||||
|
expanded: boolean;
|
||||||
|
hidden: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
selected: boolean;
|
||||||
|
actions: Action[];
|
||||||
|
|
||||||
|
index: number;
|
||||||
|
isFirst: boolean;
|
||||||
|
isLast: boolean;
|
||||||
|
|
||||||
|
constructor(data: Partial<TreeNode>) {
|
||||||
|
this.label = data?.label ?? '';
|
||||||
|
this.data = data?.data ?? null;
|
||||||
|
this.children = (data?.children ?? []).map(
|
||||||
|
(i: Partial<TreeNode>, index: number) =>
|
||||||
|
new TreeNode({ ...i, ...{ index: index } })
|
||||||
|
);
|
||||||
|
this.leaf = data?.leaf ?? this.children.length === 0;
|
||||||
|
this.expanded = data?.expanded ?? false;
|
||||||
|
this.hidden = data?.hidden ?? false;
|
||||||
|
this.disabled = data?.disabled ?? false;
|
||||||
|
this.selected = data?.selected ?? false;
|
||||||
|
this.actions = (data?.actions ?? []).map(
|
||||||
|
(i: Partial<Action>) => new Action(i)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.index = data?.index ?? 0;
|
||||||
|
this.isFirst = data?.isFirst ?? false;
|
||||||
|
this.isLast = data?.isLast ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,33 +8,38 @@ import { Milestone } from '../enums/milestone';
|
|||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class DataService {
|
export class DataService {
|
||||||
private _data: Record<string, any>[] = [];
|
private _data: Record<string, any>[] = [];
|
||||||
private data = new ReplaySubject<Record<string, any>[]>();
|
private data = new ReplaySubject<Record<string, any>[]>(1);
|
||||||
readonly Data = this.data.asObservable();
|
readonly Data$ = this.data.asObservable();
|
||||||
|
readonly Data = () => this._data;
|
||||||
|
|
||||||
private _counties = new Map<string, string>();
|
private _counties = new Map<string, string>();
|
||||||
private counties = new ReplaySubject<KeyValue[]>();
|
private counties = new ReplaySubject<KeyValue[]>(1);
|
||||||
readonly Counties = this.counties.asObservable();
|
readonly Counties$ = this.counties.asObservable();
|
||||||
|
readonly Counties = () => this._counties;
|
||||||
|
|
||||||
private _schools = new Map<string, string>();
|
private _schools = new Map<string, string>();
|
||||||
private schools = new ReplaySubject<KeyValue[]>();
|
private schools = new ReplaySubject<KeyValue[]>(1);
|
||||||
readonly Schools = this.schools.asObservable();
|
readonly Schools$ = this.schools.asObservable();
|
||||||
|
readonly Schools = () => this._schools;
|
||||||
|
|
||||||
private _years = Array(2023 - 2015 + 1)
|
private _years = Array(2023 - 2015 + 1)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map((_, index) => 2015 + index)
|
.map((_, index) => 2015 + index);
|
||||||
.filter((y) => y !== 2020);
|
private years = new ReplaySubject<KeyValue[]>(1);
|
||||||
private years = new ReplaySubject<KeyValue[]>();
|
readonly Years$ = this.years.asObservable();
|
||||||
readonly Years = this.years.asObservable();
|
readonly Years = () => this._years;
|
||||||
|
|
||||||
private _grades = Array(8 - 3 + 1)
|
private _grades = Array(8 - 3 + 1)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map((_, index) => 3 + index);
|
.map((_, index) => 3 + index);
|
||||||
private grades = new ReplaySubject<KeyValue[]>();
|
private grades = new ReplaySubject<KeyValue[]>(1);
|
||||||
readonly Grades = this.grades.asObservable();
|
readonly Grades$ = this.grades.asObservable();
|
||||||
|
readonly Grades = () => this._grades;
|
||||||
|
|
||||||
private _cohorts = new Map<string, string>();
|
private _cohorts = new Map<string, string>();
|
||||||
private cohorts = new ReplaySubject<KeyValue[]>();
|
private cohorts = new ReplaySubject<KeyValue[]>(1);
|
||||||
readonly Cohorts = this.cohorts.asObservable();
|
readonly Cohorts$ = this.cohorts.asObservable();
|
||||||
|
readonly Cohorts = () => this._cohorts;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.load();
|
this.load();
|
||||||
@@ -44,7 +49,7 @@ export class DataService {
|
|||||||
let count = this._years.length * this._grades.length;
|
let count = this._years.length * this._grades.length;
|
||||||
this._years.forEach((year: number) => {
|
this._years.forEach((year: number) => {
|
||||||
this._grades.forEach((grade: number) => {
|
this._grades.forEach((grade: number) => {
|
||||||
Papa.parse(`assets/data/${year}-${grade}.csv`, {
|
Papa.parse(`assets/data/${year === 2020 ? 2019 : year}-${grade}.csv`, {
|
||||||
download: true,
|
download: true,
|
||||||
header: false,
|
header: false,
|
||||||
complete: (results: ParseResult<Record<string, any>>) => {
|
complete: (results: ParseResult<Record<string, any>>) => {
|
||||||
@@ -68,12 +73,12 @@ export class DataService {
|
|||||||
if (isNaN(code)) return;
|
if (isNaN(code)) return;
|
||||||
|
|
||||||
const ms = {} as Record<string, any>;
|
const ms = {} as Record<string, any>;
|
||||||
// const ms = new Milestone({});
|
|
||||||
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] = fs(Csv.County_Code, record);
|
ms[Milestone.County] = fs(Csv.County_Code, record);
|
||||||
ms[Milestone.School] = fs(Csv.School_Code, record);
|
ms[Milestone.School] = fs(Csv.School_Code, record);
|
||||||
|
if (year !== 2020) {
|
||||||
ms[Milestone.ELACount] = fn(Csv.ELA_Count, record);
|
ms[Milestone.ELACount] = fn(Csv.ELA_Count, record);
|
||||||
ms[Milestone.ELAMean] = fn(Csv.ELA_Mean, record);
|
ms[Milestone.ELAMean] = fn(Csv.ELA_Mean, record);
|
||||||
ms[Milestone.ELA0] = fn(Csv.ELA_L0, record);
|
ms[Milestone.ELA0] = fn(Csv.ELA_L0, record);
|
||||||
@@ -98,6 +103,7 @@ export class DataService {
|
|||||||
ms[Milestone.Soc1] = fn(Csv.Social_L1, record);
|
ms[Milestone.Soc1] = fn(Csv.Social_L1, record);
|
||||||
ms[Milestone.Soc2] = fn(Csv.Social_L2, record);
|
ms[Milestone.Soc2] = fn(Csv.Social_L2, record);
|
||||||
ms[Milestone.Soc3] = fn(Csv.Social_L3, record);
|
ms[Milestone.Soc3] = fn(Csv.Social_L3, record);
|
||||||
|
}
|
||||||
data.push(ms);
|
data.push(ms);
|
||||||
|
|
||||||
const county = fs(Csv.County_Code, record);
|
const county = fs(Csv.County_Code, record);
|
||||||
@@ -108,6 +114,7 @@ export class DataService {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (year !== 2020) {
|
||||||
[
|
[
|
||||||
{ sort: Milestone.ELAMean, rank: Milestone.ELARank },
|
{ sort: Milestone.ELAMean, rank: Milestone.ELARank },
|
||||||
{ sort: Milestone.MathMean, rank: Milestone.MathRank },
|
{ sort: Milestone.MathMean, rank: Milestone.MathRank },
|
||||||
@@ -120,9 +127,11 @@ export class DataService {
|
|||||||
+a[m.sort] - b[m.sort]
|
+a[m.sort] - b[m.sort]
|
||||||
)
|
)
|
||||||
.forEach(
|
.forEach(
|
||||||
(a: Record<string, any>, index: number) => (a[m.rank] = index)
|
(a: Record<string, any>, index: number) =>
|
||||||
|
(a[m.rank] = index)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this._data.push(...data);
|
this._data.push(...data);
|
||||||
|
|
||||||
|
|||||||
81
src/services/execute.service.ts
Normal file
81
src/services/execute.service.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DataService } from './data.service';
|
||||||
|
import { Query } from '../models/query';
|
||||||
|
import { Observable, of, map, take } from 'rxjs';
|
||||||
|
import { Header } from '../models/header';
|
||||||
|
import { MetaService } from './meta.service';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class ExecuteService {
|
||||||
|
constructor(
|
||||||
|
private dataService: DataService,
|
||||||
|
private metaService: MetaService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
headers(query: Query): Observable<Header[]> {
|
||||||
|
if (!query?.isValid()) return of([]);
|
||||||
|
|
||||||
|
const fields = query.fields;
|
||||||
|
return this.metaService.Flat.pipe(
|
||||||
|
map((items: Header[]) =>
|
||||||
|
items
|
||||||
|
.filter((item: Header) => fields.includes(item.source))
|
||||||
|
.sort(
|
||||||
|
(a: Header, b: Header) =>
|
||||||
|
fields.indexOf(a.source) - fields.indexOf(b.source)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
take(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
data(query: Query): Observable<Record<string, any>[]> {
|
||||||
|
if (!query?.isValid()) return of([]);
|
||||||
|
|
||||||
|
const fields = query.fields;
|
||||||
|
|
||||||
|
return this.dataService.Data$.pipe(
|
||||||
|
// apply filter
|
||||||
|
map((data: Record<string, any>[]) => {
|
||||||
|
const filter = query.filter;
|
||||||
|
return data.filter((i: Record<string, any>) => {
|
||||||
|
// apply filter to i to determine if it should be returned
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
// apply fields
|
||||||
|
map((data: Record<string, any>[]) =>
|
||||||
|
data.map((i: Record<string, any>) => {
|
||||||
|
const r = {} as Record<string, any>;
|
||||||
|
fields.forEach((field: string) => (r[field] = i[field]));
|
||||||
|
return r;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
// make the data unique and apply paging
|
||||||
|
map((data: Record<string, any>[]) => {
|
||||||
|
let i = 0;
|
||||||
|
const result: Record<string, any>[] = [];
|
||||||
|
const page = query.page;
|
||||||
|
|
||||||
|
data.some((d: Record<string, any>) => {
|
||||||
|
const idx = result.findIndex((r: Record<string, any>) =>
|
||||||
|
fields
|
||||||
|
.map((field: string) => d[field] === r[field])
|
||||||
|
.reduce((acc, cur) => acc && cur, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
// if this element has not been seen before, add it to the results we care about.
|
||||||
|
if (idx < 0) {
|
||||||
|
result.push(d);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
return i >= page.start + page.limit;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.slice(page.start, page.start + page.limit);
|
||||||
|
}),
|
||||||
|
// force it to close
|
||||||
|
take(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/services/meta.service.ts
Normal file
110
src/services/meta.service.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Milestone } from '../enums/milestone';
|
||||||
|
import { map, ReplaySubject } from 'rxjs';
|
||||||
|
import { TreeNode } from '../models/tree-node';
|
||||||
|
import { Header } from '../models/header';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class MetaService {
|
||||||
|
private data = [
|
||||||
|
{
|
||||||
|
label: 'School',
|
||||||
|
data: 'node.school',
|
||||||
|
children: [
|
||||||
|
{ data: Milestone.County, label: 'County' },
|
||||||
|
{ data: Milestone.School, label: 'Name' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Class',
|
||||||
|
data: 'node.class',
|
||||||
|
children: [
|
||||||
|
{ data: Milestone.Cohort, label: 'Cohort' },
|
||||||
|
{ data: Milestone.Year, label: 'Year' },
|
||||||
|
{ data: Milestone.Grade, label: 'Grade' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'English Language Arts',
|
||||||
|
data: 'node.ela',
|
||||||
|
children: [
|
||||||
|
{ data: Milestone.ELACount, label: 'Count' },
|
||||||
|
{ data: Milestone.ELAMean, label: 'Mean' },
|
||||||
|
{ data: Milestone.ELARank, label: 'Rank' },
|
||||||
|
{ data: Milestone.ELA0, label: 'Beginning Learner %' },
|
||||||
|
{ data: Milestone.ELA1, label: 'Developing Learner %' },
|
||||||
|
{ data: Milestone.ELA2, label: 'Proficient Learner %' },
|
||||||
|
{ data: Milestone.ELA3, label: 'Advanced Learner %' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Mathematics',
|
||||||
|
data: 'node.math',
|
||||||
|
children: [
|
||||||
|
{ data: Milestone.MathCount, label: 'Count' },
|
||||||
|
{ data: Milestone.MathMean, label: 'Mean' },
|
||||||
|
{ data: Milestone.MathRank, label: 'Rank' },
|
||||||
|
{ data: Milestone.Math0, label: 'Beginning Learner %' },
|
||||||
|
{ data: Milestone.Math1, label: 'Developing Learner %' },
|
||||||
|
{ data: Milestone.Math2, label: 'Proficient Learner %' },
|
||||||
|
{ data: Milestone.Math3, label: 'Advanced Learner %' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Science',
|
||||||
|
data: 'node.sci',
|
||||||
|
children: [
|
||||||
|
{ data: Milestone.SciCount, label: 'Count' },
|
||||||
|
{ data: Milestone.SciMean, label: 'Mean' },
|
||||||
|
{ data: Milestone.SciRank, label: 'Rank' },
|
||||||
|
{ data: Milestone.Sci0, label: 'Beginning Learner %' },
|
||||||
|
{ data: Milestone.Sci1, label: 'Developing Learner %' },
|
||||||
|
{ data: Milestone.Sci2, label: 'Proficient Learner %' },
|
||||||
|
{ data: Milestone.Sci3, label: 'Advanced Learner %' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Social Studies',
|
||||||
|
data: 'node.soc',
|
||||||
|
children: [
|
||||||
|
{ data: Milestone.SocCount, label: 'Count' },
|
||||||
|
{ data: Milestone.SocMean, label: 'Mean' },
|
||||||
|
{ data: Milestone.SocRank, label: 'Rank' },
|
||||||
|
{ data: Milestone.Soc0, label: 'Beginning Learner %' },
|
||||||
|
{ data: Milestone.Soc1, label: 'Developing Learner %' },
|
||||||
|
{ data: Milestone.Soc2, label: 'Proficient Learner %' },
|
||||||
|
{ data: Milestone.Soc3, label: 'Advanced Learner %' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
private subject = new ReplaySubject<Partial<TreeNode>[]>(1);
|
||||||
|
readonly Data = this.subject.asObservable();
|
||||||
|
readonly Flat = this.subject.asObservable().pipe(
|
||||||
|
map((data: Partial<TreeNode>[]) => {
|
||||||
|
const recurse = (path: string, item: Partial<TreeNode>): Header[] => {
|
||||||
|
const result = [];
|
||||||
|
const children = item?.children ?? [];
|
||||||
|
if (children.length > 0) {
|
||||||
|
children.forEach((child: Partial<TreeNode>) => {
|
||||||
|
result.push(...recurse(`${path}${item.label}`, child));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result.push(
|
||||||
|
new Header({ source: item.data, label: `${path} - ${item.label}` })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const result: Header[] = [];
|
||||||
|
data.forEach((item: Partial<TreeNode>) =>
|
||||||
|
result.push(...recurse('', item))
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.subject.next(this.data as Partial<TreeNode>[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/services/query.service.ts
Normal file
24
src/services/query.service.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { Query } from '../models/query';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class QueryService {
|
||||||
|
private querySubject = new BehaviorSubject<Query>(new Query({}));
|
||||||
|
readonly Query = this.querySubject.asObservable();
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
add(field: string, index: number = -1): void {
|
||||||
|
const q = new Query(this.querySubject.value);
|
||||||
|
q.fields = q.fields.filter((f: string) => f !== field);
|
||||||
|
q.fields.splice(index < 0 ? q.fields.length : index, 0, field);
|
||||||
|
this.querySubject.next(q);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(field: string): void {
|
||||||
|
const q = new Query(this.querySubject.value);
|
||||||
|
q.fields = q.fields.filter((f: string) => f !== field);
|
||||||
|
this.querySubject.next(q);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,5 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
/* You can add global styles to this file, and also import other style files */
|
||||||
|
body {
|
||||||
|
// margin: 0;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user