Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions cypress/e2e/journeys/flows/mixed-node-coloring.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/// <reference types="cypress" />

import type { DatasetProfile } from '../datasets/profile';
import {
ensureBubbleView,
ensureMapView,
goToPhyloTreeView,
launchProfileToTwoD,
openGlobalStylingTab,
} from '../../../support/journey-helpers';

type WinWithMicrobeTrace = Window & {
commonService: any;
cytoscapeInstance?: any;
};

const profile: DatasetProfile = {
id: 'mixed-genotype-node-coloring',
title: 'Mixed genotype node coloring',
tags: ['color-by', 'mixed-node-colors', 'genotype', 'load-to-twod'],
files: [
{
name: 'Cypress_MixedGenotype_Nodes.csv',
datatype: 'node',
field1: 'ID',
field2: 'seq',
},
{
name: 'Cypress_MixedGenotype_Links.csv',
datatype: 'link',
field1: 'source',
field2: 'target',
},
],
preLaunch: {
metric: 'snps',
threshold: 16,
defaultView: '2D Network',
},
expectations: {
afterLaunch: {
nodes: 4,
visibleLinks: 3,
},
},
};

const selectPrimeOption = (selector: string, label: string): void => {
cy.get(selector).click({ force: true });
cy.contains('li[role="option"]', label, { timeout: 15000 }).click({ force: true });
};

const assertMixedStyleSegments = (expectedFirstColor?: string): void => {
cy.window().then((win: unknown) => {
const { commonService } = win as WinWithMicrobeTrace;
const mixedNode = commonService.session.data.nodes.find((node: any) => node._id === 'sample-4');
const singleNode = commonService.session.data.nodes.find((node: any) => node._id === 'sample-2');
const mixedStyle = commonService.getNodeFillStyle(mixedNode);
const singleStyle = commonService.getNodeFillStyle(singleNode);
const color2a = String(commonService.temp.style.nodeColorMap('2a'));
const color3a = String(commonService.temp.style.nodeColorMap('3a'));

expect(mixedStyle.segments?.map((segment: any) => segment.value)).to.deep.equal(['2a', '3a']);
expect(mixedStyle.segments?.map((segment: any) => segment.color)).to.deep.equal([color2a, color3a]);
expect(singleStyle.segments).to.equal(undefined);

if (expectedFirstColor) {
expect(color2a.toLowerCase()).to.equal(expectedFirstColor);
expect(mixedStyle.segments?.[0].color.toLowerCase()).to.equal(expectedFirstColor);
}
});
};

describe('Journey Flow - mixed node coloring', () => {
it('renders mixed genotype nodes with component color segments across node views', () => {
launchProfileToTwoD(profile);

openGlobalStylingTab();
cy.get('#node-mixed-colors-row').should('not.exist');
cy.get('#node-mixed-colors-enabled').should('not.exist');
selectPrimeOption('#node-color-variable', 'Genotype');
cy.get('#node-mixed-colors-enabled').should('be.visible').and('be.enabled').check();
cy.window().its('commonService.session.style.widgets.node-mixed-colors-enabled').should('equal', true);
cy.closeGlobalSettings();

assertMixedStyleSegments();

cy.window().then((win: unknown) => {
const typedWindow = win as WinWithMicrobeTrace;
const cyInstance = typedWindow.cytoscapeInstance;
const mixedNode = cyInstance.getElementById('sample-4');
const singleNode = cyInstance.getElementById('sample-2');

expect(String(mixedNode.data('mixedColorImage') || '')).to.contain('data:image/svg+xml');
expect(singleNode.data('mixedColorImage')).to.equal(undefined);
});

cy.get('#key-tables-node-table td[data-value="2a"]', { timeout: 15000 })
.parents('tr')
.find('input[type="color"]')
.invoke('val', '#00aa00')
.trigger('input')
.trigger('change');

assertMixedStyleSegments('#00aa00');

ensureBubbleView();
cy.window().then((win: unknown) => {
const bubble = (win as WinWithMicrobeTrace).commonService.visuals.bubble;
const mixedBubbleNode = bubble.cy.getElementById('sample-4');
expect(String(mixedBubbleNode.data('mixedColorImage') || '')).to.contain('data:image/svg+xml');
});

ensureMapView();
cy.window().then((win: unknown) => {
const map = (win as WinWithMicrobeTrace).commonService.visuals.gisMap;
const marker = map.mapNodeMarkersById['sample-4'];
const iconUrl = String(marker?.options?.icon?.options?.iconUrl || '');
expect(iconUrl).to.contain('data:image/svg+xml');
expect(decodeURIComponent(iconUrl)).to.contain('#00aa00');
});

goToPhyloTreeView();
cy.get('#phylocanvas image.tidytree-node-shape-overlay', { timeout: 30000 })
.should(($overlays) => {
expect($overlays.length).to.be.greaterThan(0);
});
});
});
18 changes: 17 additions & 1 deletion cypress/e2e/view-state/bubble-view.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { visitAppAndAcceptEula } from '../../support/journey-helpers';
let takeScreenshots = false;
const getCy = () => cy.window().then(win => win.commonService.visuals.bubble.cy)

const showFloatingNodeColorTable = (): void => {
cy.get('#node-color-table-row')
.contains('.p-togglebutton-label', 'Show')
.click({ force: true });
cy.window().its('commonService.visuals.microbeTrace.SelectedNodeColorTableTypesVariable').should('equal', 'Show');
};

describe('Bubble View', () => {
const selectors = {
container: '#cyBubble',
Expand Down Expand Up @@ -204,6 +211,7 @@ describe('Bubble View', () => {
cy.openGlobalSettings();
cy.get('#node-color-variable').click()
cy.get('li[role="option"]').contains('Lineage').click()
showFloatingNodeColorTable();
cy.get('#node-color-table td input', { timeout: 10000 }).should('exist');
cy.get('#node-color-table tr').eq(1).find('.transparency-symbol').click({ force: true });
cy.get('#color-transparency').invoke('val', alpha).trigger('change');
Expand All @@ -225,6 +233,7 @@ describe('Bubble View', () => {
cy.openGlobalSettings();
cy.get('#node-color-variable').click()
cy.get('li[role="option"]').contains('Lineage').click()
showFloatingNodeColorTable();
cy.get('#node-color-table td input', { timeout: 10000 }).should('exist');
cy.get('#node-color-table tr').eq(1).find('.transparency-symbol').click({ force: true });
cy.get('#color-transparency').invoke('val', alpha).trigger('change');
Expand Down Expand Up @@ -565,11 +574,18 @@ describe('Bubble View', () => {
})

it('changes color of node and link during timeline and then ensures color is kept after timeline ends', () => {
cy.openGlobalSettings();
cy.contains('#global-settings-modal .nav-link', 'Styling').click({ force: true });
cy.get('#node-color-variable').click();
cy.get('li[role="option"]').contains('State').click();
showFloatingNodeColorTable();
cy.closeGlobalSettings();

cy.get('#timeline-play-button').should('contain', 'Play').click();
cy.wait(7500)
cy.get('#timeline-play-button').should('contain', 'Pause').click();

cy.get('#key-tables-node-table').contains('td', 'Pennsylvania').parent('tr').find('input[type="color"]').first().invoke('val', '#777777').trigger('input').trigger('change');
cy.get('#node-color-table').contains('td', 'Pennsylvania').parent('tr').find('input[type="color"]').first().invoke('val', '#777777').trigger('input').trigger('change');

cy.window().its('commonService.visuals.bubble').then(bubble => {
let penNode = bubble.cy.nodes('[id = "MZ415508"]')[0]
Expand Down
35 changes: 23 additions & 12 deletions cypress/e2e/view-state/map-view.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,25 @@ const searchForFieldValue = (field: string, value: string): void => {
const searchForNode = (nodeId: string): void =>
searchForFieldValue('_id', nodeId);

const showFloatingNodeColorTable = (): void => {
cy.get('#node-color-table-row')
.contains('.p-togglebutton-label', 'Show')
.click({ force: true });
cy.window().its('commonService.visuals.microbeTrace.SelectedNodeColorTableTypesVariable').should('equal', 'Show');
};

const closeFloatingLinkColorTableIfPresent = (): void => {
cy.get('body').then($body => {
const linkColorHeader = $body.find('.p-dialog-header:contains("Link Color Table")');
if (linkColorHeader.length) {
cy.wrap(linkColorHeader)
.parents('.p-dialog')
.find('button.p-dialog-close-button')
.click({ force: true });
}
});
};

/**
* Tests for the Map visualization component.
*/
Expand Down Expand Up @@ -676,10 +695,7 @@ describe('Map View', () => {
cy.wait(200)

cy.closeSettingsPane('Geospatial Settings');
cy.contains('.p-dialog-header', 'Link Color Table')
.parents('.p-dialog')
.find('button.p-dialog-close-button')
.click();
closeFloatingLinkColorTableIfPresent();

let NC_node: any;
cy.window().then((win: any) => {
Expand Down Expand Up @@ -719,10 +735,7 @@ describe('Map View', () => {
cy.wait(200)

cy.closeSettingsPane('Geospatial Settings');
cy.contains('.p-dialog-header', 'Link Color Table')
.parents('.p-dialog')
.find('button.p-dialog-close-button')
.click();
closeFloatingLinkColorTableIfPresent();

let test_link: any;
cy.window().then((win: any) => {
Expand Down Expand Up @@ -758,10 +771,7 @@ describe('Map View', () => {

it('should select a node by clicking on it', () => {
cy.closeSettingsPane('Geospatial Settings');
cy.contains('.p-dialog-header', 'Link Color Table')
.parents('.p-dialog')
.find('button.p-dialog-close-button')
.click();
closeFloatingLinkColorTableIfPresent();

let NC_node: any;
cy.window().then((win: any) => {
Expand Down Expand Up @@ -933,6 +943,7 @@ describe('Map View', () => {

cy.get('#node-color-variable').click()
cy.get('li[role="option"]').contains('Lineage').click()
showFloatingNodeColorTable();
cy.get('#node-color-table td input', { timeout: 10000 }).should('exist');
cy.get('#node-color-table tr').eq(1).find('.transparency-symbol').click({ force: true });
cy.get('#color-transparency').invoke('val', tableAlpha).trigger('change');
Expand Down
8 changes: 8 additions & 0 deletions cypress/e2e/view-state/phylogenetic-view.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ describe('Phylogenetic Tree View', () => {
});
};

const showFloatingNodeColorTable = (): void => {
cy.get('#node-color-table-row')
.contains('.p-togglebutton-label', 'Show')
.click({ force: true });
cy.window().its('commonService.visuals.microbeTrace.SelectedNodeColorTableTypesVariable').should('equal', 'Show');
};

/**
* This block runs before each test. It loads the application,
* continues with the sample dataset, and navigates to the view.
Expand Down Expand Up @@ -99,6 +106,7 @@ describe('Phylogenetic Tree View', () => {
cy.openGlobalSettings();
cy.get('#node-color-variable').click()
cy.get('li[role="option"]').contains('Lineage').click()
showFloatingNodeColorTable();
cy.get('#node-color-table td input', { timeout: 10000 }).should('exist');
cy.get('#node-color-table tr').eq(1).find('.transparency-symbol').click({ force: true });
cy.get('#color-transparency').invoke('val', alpha).trigger('change');
Expand Down
4 changes: 4 additions & 0 deletions cypress/fixtures/Cypress_MixedGenotype_Links.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
source,target,distance
sample-1,sample-2,1
sample-2,sample-3,1
sample-3,sample-4,1
10 changes: 10 additions & 0 deletions cypress/fixtures/Cypress_MixedGenotype_Nodes.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
ID,Genotype,_jlat,_jlon,seq
sample-1,1a,38.1,-86.1,AAAAAAAAAA
sample-2,2a,38.12,-86.12,AAAAAAAATA
sample-3,3a,38.14,-86.14,AAAAAAATAA
sample-4,2a/3a,38.16,-86.16,AAAAAATAAA
sample-5,6/7a,38,86,AAAAAAAAAC
sample-6,,37,85,CAAAAAAAAA
sample-7,null,37,85.2,CAAAAAAAAA
sample-8,N/A,36,86.14,AAAAAACCCC
sample-9,n/a,36.1,86.15,AAAAAACTCC
96 changes: 96 additions & 0 deletions src/app/contactTraceCommonServices/color-mapping.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { ColorMappingService, getMixedNodeColorSegments, parseMixedNodeColorValue } from './color-mapping.service';

describe('mixed node color helpers', () => {
it('splits strings on supported delimiters', () => {
expect(parseMixedNodeColorValue('1a/2a, 3a;4a+5a|6a and 7a')).toEqual([
'1a',
'2a',
'3a',
'4a',
'5a',
'6a',
'7a'
]);
});

it('accepts arrays, trims values, removes duplicates, and ignores null-like values', () => {
expect(parseMixedNodeColorValue([' 2a ', '2a/3a', 'N/A', 'n/a', '2a/N/A', '', null, undefined, NaN, 'nan', 'NULL', 'undefined'])).toEqual([
'2a',
'3a'
]);
});

it('maps mixed segments through the existing color and alpha maps', () => {
const colors = { '2a': '#00aa00', '3a': '#ffff00' };
const alphas = { '2a': 0.4, '3a': 0.8 };

expect(getMixedNodeColorSegments(
'2a/3a',
value => colors[value],
value => alphas[value],
'#000000',
1
)).toEqual([
{ value: '2a', color: '#00aa00', alpha: 0.4, weight: 1 },
{ value: '3a', color: '#ffff00', alpha: 0.8, weight: 1 }
]);
});

it('splits mixed values into component color table rows instead of adding combination rows', () => {
const service = new ColorMappingService();
const result = service.createNodeColorMap(
[
{ visible: true, Genotype: '2a' },
{ visible: true, Genotype: '2a/3a' },
{ visible: true, Genotype: '3a' }
],
'Genotype',
['#00aa00', '#ffff00'],
[1, 1],
{},
{},
{},
false,
true
);

expect(result.aggregates['2a']).toBeCloseTo(1.5);
expect(result.aggregates['3a']).toBeCloseTo(1.5);
expect(result.updatedColorsTableKeys.Genotype).toEqual(['2a', '3a']);
expect(result.updatedColorsTableKeys.Genotype).not.toContain('2a/3a');
});

it('keeps components that only occur inside mixed values in the color table domain', () => {
const service = new ColorMappingService();
const result = service.createNodeColorMap(
[
{ visible: true, Genotype: '1a' },
{ visible: true, Genotype: '2a' },
{ visible: true, Genotype: '3a' },
{ visible: true, Genotype: '2a/3a' },
{ visible: true, Genotype: '6/7a' },
{ visible: true, Genotype: 'N/A' },
{ visible: true, Genotype: null }
],
'Genotype',
['#111111', '#222222', '#333333', '#444444', '#555555', '#666666'],
[1, 1, 1, 1, 1, 1],
{},
{},
{},
false,
true
);

expect(result.aggregates['6']).toBeCloseTo(0.5);
expect(result.aggregates['7a']).toBeCloseTo(0.5);
expect(result.aggregates['null']).toBeCloseTo(2);
expect(result.updatedColorsTableKeys.Genotype).toContain('6');
expect(result.updatedColorsTableKeys.Genotype).toContain('7a');
expect(result.updatedColorsTableKeys.Genotype).toContain('null');
expect(result.updatedColorsTableKeys.Genotype).not.toContain('6/7a');
expect(result.updatedColorsTableKeys.Genotype).not.toContain('N');
expect(result.updatedColorsTableKeys.Genotype).not.toContain('A');
expect(result.updatedColorsTableKeys.Genotype).not.toContain('N/A');
});
});
Loading
Loading