Skip to content

Commit

Permalink
Merge pull request #132 from SCAI-BIO/fix-lineplot
Browse files Browse the repository at this point in the history
Fix Lineplot Service
  • Loading branch information
mehmetcanay authored Nov 11, 2024
2 parents 33ad60b + 977a3ff commit aad76b1
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 93 deletions.
66 changes: 36 additions & 30 deletions frontend/src/app/longitudinal/longitudinal.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,22 @@ describe('LongitudinalComponent', () => {
httpMock.verify();
});

it('should create', () => {
it('should create', async () => {
fixture.detectChanges();

const reqMappings = httpMock.expectOne(
'./assets/lower_to_original_case.json'
);
reqMappings.flush({
'feature name': 'Feature Name',
'another feature': 'Another Feature',
});

// Wait for the ngOnInit to complete loading the mappings
await fixture.whenStable();
const reqTables = httpMock.expectOne(`${environment.API_URL}/longitudinal`);
reqTables.flush(['longitudinal_table1', 'longitudinal_table2']);

// Handle the HTTP requests triggered by ngOnInit
const reqMeta = httpMock.expectOne(
`${environment.API_URL}/cohorts/metadata`
Expand Down Expand Up @@ -77,21 +90,10 @@ describe('LongitudinalComponent', () => {
},
});

const reqTables = httpMock.expectOne(`${environment.API_URL}/longitudinal`);
reqTables.flush(['longitudinal_table1', 'longitudinal_table2']);

const reqMappings = httpMock.expectOne(
'./assets/lower_to_original_case.json'
);
reqMappings.flush({
'feature name': 'Feature Name',
'another feature': 'Another Feature',
});

expect(component).toBeTruthy();
});

it('should fetch colors on init', () => {
it('should fetch colors on init', async () => {
const mockMetadata: Metadata = {
cohort1: {
Participants: 100,
Expand Down Expand Up @@ -121,33 +123,45 @@ describe('LongitudinalComponent', () => {

fixture.detectChanges();

const reqMeta = httpMock.expectOne(
`${environment.API_URL}/cohorts/metadata`
);
reqMeta.flush(mockMetadata);

const reqTables = httpMock.expectOne(`${environment.API_URL}/longitudinal`);
reqTables.flush(['longitudinal_table1', 'longitudinal_table2']);

const reqMappings = httpMock.expectOne(
'./assets/lower_to_original_case.json'
);
reqMappings.flush({
'feature name': 'Feature Name',
'another feature': 'Another Feature',
});
// Wait for the ngOnInit to complete loading the mappings
await fixture.whenStable();

const reqTables = httpMock.expectOne(`${environment.API_URL}/longitudinal`);
reqTables.flush(['longitudinal_table1', 'longitudinal_table2']);

const reqMeta = httpMock.expectOne(
`${environment.API_URL}/cohorts/metadata`
);
reqMeta.flush(mockMetadata);

expect(component.colors).toEqual({
cohort1: '#ff0000',
cohort2: '#00ff00',
});
});

it('should fetch longitudinal tables on init', () => {
it('should fetch longitudinal tables on init', async () => {
const mockTables = ['longitudinal_table1', 'longitudinal_table2'];

fixture.detectChanges();

const reqMappings = httpMock.expectOne(
'./assets/lower_to_original_case.json'
);
reqMappings.flush({
'feature name': 'Feature Name',
'another feature': 'Another Feature',
});
// Wait for the ngOnInit to complete loading the mappings
await fixture.whenStable();

const reqMeta = httpMock.expectOne(
`${environment.API_URL}/cohorts/metadata`
);
Expand Down Expand Up @@ -182,14 +196,6 @@ describe('LongitudinalComponent', () => {
expect(reqTables.request.method).toBe('GET');
reqTables.flush(mockTables);

const reqMappings = httpMock.expectOne(
'./assets/lower_to_original_case.json'
);
reqMappings.flush({
'feature name': 'Feature Name',
'another feature': 'Another Feature',
});

expect(component.longitudinalTables).toEqual(['Table1', 'Table2']);
});

Expand Down
52 changes: 29 additions & 23 deletions frontend/src/app/longitudinal/longitudinal.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,36 +132,42 @@ export class LongitudinalComponent implements OnInit, OnDestroy {
);
}

loadOriginalCaseMappings(): void {
this.http
.get<{ [key: string]: string }>('./assets/lower_to_original_case.json')
.subscribe({
next: (data) => {
this.originalVariableNameMappings = data;
console.info(
'Lowercase to original case mappings successfully loaded'
);
},
error: (e) =>
console.error(
'Error loading lowercase to original case mappings:',
e
),
});
loadOriginalCaseMappings(): Promise<void> {
return new Promise((resolve, reject) => {
this.http
.get<{ [key: string]: string }>('./assets/lower_to_original_case.json')
.subscribe({
next: (data) => {
this.originalVariableNameMappings = data;
console.info(
'Lowercase to original case mappings successfully loaded'
);
resolve();
},
error: (e) => {
console.error(
'Error loading lowercase to original case mappings:',
e
);
reject(e);
},
});
});
}

ngOnDestroy(): void {
this.subscriptions.forEach((sub) => sub.unsubscribe());
}

ngOnInit() {
this.loadOriginalCaseMappings();
this.fetchLongitudinalTables();
this.fetchColors();
this.filteredFeatures = this.featureCtrl.valueChanges.pipe(
startWith(''),
map((value) => this._filterTableName(value || ''))
);
this.loadOriginalCaseMappings().then(() => {
this.fetchLongitudinalTables();
this.fetchColors();
this.filteredFeatures = this.featureCtrl.valueChanges.pipe(
startWith(''),
map((value) => this._filterTableName(value || ''))
);
});
}

removeFeature(): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div class="header-container">
<h1>Biomarker-specific Follow-up</h1>
<h1>Variable-specific Follow-up</h1>
<div class="component-container">
<div class="lineplot" #lineplot></div>
</div>
Expand Down
77 changes: 56 additions & 21 deletions frontend/src/app/plot-longitudinal/plot-longitudinal.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import { LineplotService } from '../services/lineplot.service';
})
export class PlotLongitudinalComponent implements OnInit, OnDestroy {
cohort: string = '';
features: string[] = [];
variables: string[] = [];
data: LongitudinalData[] = [];
originalData: LongitudinalData[] = [];
originalVariableNameMappings: { [key: string]: string } = {};
private API_URL = environment.API_URL;
@ViewChild('lineplot') private chartContainer!: ElementRef;
private subscriptions: Subscription[] = [];
Expand Down Expand Up @@ -58,11 +58,17 @@ export class PlotLongitudinalComponent implements OnInit, OnDestroy {
}

generateLineplot(): void {
const features = [];
for (const feature of this.features) {
features.push(this._transformLongitudinalName(feature));
const variables = [];
for (const variable of this.variables) {
variables.push(this._transformLongitudinalName(variable));
}
const features_string = features.map((item) => `${item}`).join(', ');
const features_string =
variables.length > 1
? variables.slice(0, -1).join(', ') +
' and ' +
variables[variables.length - 1]
: variables[0] || ''; // Handle single or empty features case

const title = `Longitudinal follow-ups for ${features_string} in the ${this.cohort} cohort`;
this.lineplotService.createLineplot(
this.chartContainer,
Expand All @@ -72,35 +78,64 @@ export class PlotLongitudinalComponent implements OnInit, OnDestroy {
);
}

loadOriginalCaseMappings(): Promise<void> {
return new Promise((resolve, reject) => {
this.http
.get<{ [key: string]: string }>('./assets/lower_to_original_case.json')
.subscribe({
next: (data) => {
this.originalVariableNameMappings = data;
console.info(
'Lowercase to original case mappings successfully loaded'
);
resolve();
},
error: (e) => {
console.error(
'Error loading lowercase to original case mappings:',
e
);
reject(e);
},
});
});
}

ngOnDestroy(): void {
this.subscriptions.forEach((sub) => sub.unsubscribe());
}

ngOnInit(): void {
const sub = this.route.queryParams.subscribe((params) => {
this.cohort = params['cohort'] || '';
this.features = params['features'] || [];
});
this.subscriptions.push(sub);
this.loadOriginalCaseMappings().then(() => {
const sub = this.route.queryParams.subscribe((params) => {
this.cohort = params['cohort'] || '';
this.variables = params['features'] || [];
});
this.subscriptions.push(sub);

// Ensure features is an array
if (!Array.isArray(this.features)) {
this.features = [this.features];
}
// Ensure features is an array
if (!Array.isArray(this.variables)) {
this.variables = [this.variables];
}

// Set the count of features to fetch data
this.dataFetchCount = this.features.length;
// Set the count of features to fetch data
this.dataFetchCount = this.variables.length;

for (const feature of this.features) {
this.fetchLongitudinalTable(feature);
}
// Fetch data for each variable
for (const variable of this.variables) {
this.fetchLongitudinalTable(variable);
}
});
}

private _transformLongitudinalName(longitudinal: string): string {
if (longitudinal.startsWith('longitudinal_')) {
longitudinal = longitudinal.substring(13);
}
longitudinal = longitudinal.split('_').join(' ');
return longitudinal.charAt(0).toUpperCase() + longitudinal.slice(1);
const mappedValue = this.originalVariableNameMappings[longitudinal];
return mappedValue
? mappedValue
: longitudinal.charAt(0).toUpperCase() + longitudinal.slice(1);
}
}
50 changes: 32 additions & 18 deletions frontend/src/app/services/lineplot.service.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { ElementRef, Injectable } from '@angular/core';
import { ElementRef, Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import * as d3 from 'd3';
import { LongitudinalData } from '../interfaces/longitudinal-data';

@Injectable({
providedIn: 'root',
})
export class LineplotService {
constructor() {}
constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

createLineplot(
element: ElementRef,
data: LongitudinalData[],
colors: { [key: string]: string } = {}, // Default parameter
title: string = '' // New optional title parameter
): void {
if (!isPlatformBrowser(this.platformId)) {
console.warn(
`D3 code skipped because it is running on the ${this.platformId}`
);
return;
}
const margin = { top: 40, right: 150, bottom: 60, left: 60 }; // Increase top margin for the title
const width = 1200 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
Expand All @@ -33,23 +40,22 @@ export class LineplotService {

// Set the domains for the scales
x.domain(d3.extent(data, (d) => d.Months) as [number, number]);
y.domain([0, 100]); // Y-axis from 0 to 100 percent
y.domain([0, 100]);

// Add the title
if (title) {
svg
.append('text')
.attr('x', width / 2)
.attr('y', -margin.top / 2)
.attr('text-anchor', 'middle')
.attr('class', 'title')
.text(title);
}
// Determine the maximum value of x (Months)
const maxMonths = Math.ceil(d3.max(data, (d) => d.Months) || 0);

svg
.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x));
// Generate tick values at intervals of 6
const tickValues = Array.from(
{ length: Math.floor(maxMonths / 6) + 1 },
(_, i) => i * 6
).filter((tick) => tick <= maxMonths);

// Create and configure the x-axis
const xAxis = d3.axisBottom(x).tickValues(tickValues);

// Append the x-axis to the SVG
svg.append('g').attr('transform', `translate(0,${height})`).call(xAxis);

svg.append('g').call(d3.axisLeft(y).ticks(10));

Expand Down Expand Up @@ -77,7 +83,7 @@ export class LineplotService {
.datum(values)
.attr('class', 'line-path')
.attr('fill', 'none')
.attr('stroke', cohortColor) // Use the color from the colors object or the color scale
.attr('stroke', cohortColor)
.attr('stroke-width', 1.5)
.attr('d', line);
});
Expand Down Expand Up @@ -271,5 +277,13 @@ export class LineplotService {
`translate(${width / 2}, ${height + margin.bottom / 1.5})`
)
.text('Months');

// Add the title
svg
.append('text')
.attr('x', width / 2)
.attr('y', -margin.top / 2)
.attr('text-anchor', 'middle')
.text(title);
}
}

0 comments on commit aad76b1

Please sign in to comment.