Skip to content

Commit

Permalink
Merge branch 'cde-mvp2' of https://github.com/hubmapconsortium/hra-ui
Browse files Browse the repository at this point in the history
…into cde-create-vis-app-mvp2
  • Loading branch information
edlu77 committed Oct 2, 2024
2 parents 8c34da1 + 73bb515 commit b220efc
Show file tree
Hide file tree
Showing 11 changed files with 658 additions and 190 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,32 @@
</div>

<div class="visualizations">
<cde-node-dist-visualization
[nodes]="loadedNodes()"
[nodeTargetKey]="nodeTypeKey()"
[nodeTargetValue]="selectedNodeTargetValue()"
[edges]="loadedEdges()"
[maxEdgeDistance]="maxEdgeDistance()"
[colorMap]="cellTypesAsColorMap()"
[colorMapKey]="colorMapTypeKey()"
[colorMapValueKey]="colorMapValueKey()"
[cellTypesSelection]="cellTypesSelection()"
(nodeClick)="nodeClick.emit($event)"
(nodeHover)="nodeHover.emit($event)"
(edgesChange)="edges.set($event)"
#visualization
></cde-node-dist-visualization>
<div class="top">
<cde-node-dist-visualization
[nodes]="loadedNodes()"
[nodeTargetKey]="nodeTypeKey()"
[nodeTargetValue]="selectedNodeTargetValue()"
[edges]="loadedEdges()"
[maxEdgeDistance]="maxEdgeDistance()"
[colorMap]="cellTypesAsColorMap()"
[colorMapKey]="colorMapTypeKey()"
[colorMapValueKey]="colorMapValueKey()"
[cellTypesSelection]="cellTypesSelection()"
(nodeClick)="nodeClick.emit($event)"
(nodeHover)="nodeHover.emit($event)"
(edgesChange)="edges.set($event)"
#visualization
></cde-node-dist-visualization>

<cde-violin [colors]="filteredColors()" [data]="filteredDistances()" #violin></cde-violin>
</div>

<cde-histogram
[nodes]="loadedNodes()"
[nodeTargetKey]="nodeTypeKey()"
[edges]="loadedEdges()"
[colors]="filteredColors()"
[data]="filteredDistances()"
[filteredCellTypes]="filteredCellTypes()"
[selectedCellType]="selectedNodeTargetValue()"
[(cellTypes)]="cellTypes"
[cellTypesSelection]="cellTypesSelection()"
(updateColor)="updateColor($event.entry, $event.color)"
#histogram
></cde-histogram>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,23 @@
display: flex;
flex-direction: column;

.top {
display: flex;
height: 0;
flex-grow: 1;
}

cde-node-dist-visualization {
border-color: #d5dbe3;
border-width: 0 0 2px 0;
border-style: solid;
height: 100%;
width: calc(100% - 488px);
}

cde-violin {
height: 100%;
width: 488px;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,24 @@ import {
signal,
ViewContainerRef,
} from '@angular/core';

import { CellTypesComponent } from '../components/cell-types/cell-types.component';
import { HistogramComponent } from '../components/histogram/histogram.component';
import { MetadataComponent } from '../components/metadata/metadata.component';
import { NodeDistVisualizationComponent } from '../components/node-dist-visualization/node-dist-visualization.component';
import { ViolinComponent } from '../components/violin/violin.component';
import { VisualizationHeaderComponent } from '../components/visualization-header/visualization-header.component';
import { CellTypeEntry } from '../models/cell-type';
import { rgbToHex } from '@hra-ui/design-system/color-picker';
import { Rgb, rgbToHex } from '@hra-ui/design-system/color-picker';
import {
ColorMapColorKey,
ColorMapEntry,
colorMapToLookup,
ColorMapTypeKey,
DEFAULT_COLOR_MAP_KEY,
DEFAULT_COLOR_MAP_VALUE_KEY,
colorMapToLookup,
} from '../models/color-map';
import { DEFAULT_MAX_EDGE_DISTANCE, EdgeEntry } from '../models/edge';
import { DEFAULT_MAX_EDGE_DISTANCE, edgeDistance, EdgeEntry, EdgeIndex } from '../models/edge';
import { Metadata } from '../models/metadata';
import { DEFAULT_NODE_TARGET_KEY, NodeEntry, NodeTargetKey, selectNodeTargetValue } from '../models/node';
import { ColorMapFileLoaderService } from '../services/data/color-map-loader.service';
Expand All @@ -40,6 +42,14 @@ import { createColorGenerator } from '../shared/color-generator';
import { emptyArrayEquals } from '../shared/empty-array-equals';
import { mergeObjects } from '../shared/merge';

/** Interface for representing the distance entry */
export interface DistanceEntry {
/** Type of the entry */
type: string;
/** Distance value of the entry */
distance: number;
}

/**
* CDE Visualization Root Component
*/
Expand All @@ -53,6 +63,7 @@ import { mergeObjects } from '../shared/merge';
CellTypesComponent,
NodeDistVisualizationComponent,
HistogramComponent,
ViolinComponent,
],
templateUrl: './cde-visualization.component.html',
styleUrl: './cde-visualization.component.scss',
Expand Down Expand Up @@ -241,6 +252,26 @@ export class CdeVisualizationComponent {
/** View container. Do NOT change the name. It is used by ngx-color-picker! */
readonly vcRef = inject(ViewContainerRef);

/** List of filtered cell types based on selection */
protected readonly filteredCellTypes = computed(
() => {
const selection = new Set(this.cellTypesSelection());
selection.delete(this.selectedNodeTargetValue());
const filtered = this.cellTypes().filter(({ name }) => selection.has(name));
return filtered.sort((a, b) => b.count - a.count);
},
{ equal: emptyArrayEquals },
);

/** Computed distances between nodes */
protected readonly distances = computed(() => this.computeDistances(), { equal: emptyArrayEquals });

/** Data for the histogram visualization */
protected readonly filteredDistances = computed(() => this.computeFilteredDistances(), { equal: emptyArrayEquals });

/** Colors for the histogram visualization */
protected readonly filteredColors = computed(() => this.computeFilteredColors(), { equal: emptyArrayEquals });

/** Setup component */
constructor() {
// Workaround for getting ngx-color-picker to attach to the root view
Expand Down Expand Up @@ -281,4 +312,53 @@ export class CdeVisualizationComponent {
this.fileSaver.saveCsv(data, 'color-map.csv');
}
}

/** Compute distances between nodes based on edges */
private computeDistances(): DistanceEntry[] {
const nodes = this.loadedNodes();
const edges = this.loadedEdges();
if (nodes.length === 0 || edges.length === 0) {
return [];
}

const nodeTypeKey = this.nodeTypeKey();
const selectedCellType = this.selectedNodeTargetValue();
const distances: DistanceEntry[] = [];
for (const edge of edges) {
const sourceNode = nodes[edge[EdgeIndex.SourceNode]];
const type = sourceNode[nodeTypeKey];
if (type !== selectedCellType) {
distances.push({ type, distance: edgeDistance(edge) });
}
}

return distances;
}

/** Compute data for the violin visualization */
private computeFilteredDistances(): DistanceEntry[] {
const selection = new Set(this.cellTypesSelection());
if (selection.size === 0) {
return [];
}

return this.distances().filter(({ type }) => selection.has(type));
}

/** Compute colors for the violin visualization */
private computeFilteredColors(): string[] {
return this.filteredCellTypes()
.sort((a, b) => (a.name < b.name ? -1 : a.name === b.name ? 0 : 1))
.map(({ color }) => rgbToHex(color));
}

/** Update the color of a specific cell type entry */
updateColor(entry: CellTypeEntry, color: Rgb): void {
const entries = this.cellTypes();
const index = entries.indexOf(entry);
const copy = [...entries];

copy[index] = { ...copy[index], color };
this.cellTypes.set(copy);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@
(colorPickerOpen)="colorPicker.set($event)"
>
</cde-color-picker-label>
@for (type of filteredCellTypes(); track type.name) {
@for (entry of filteredCellTypes(); track entry.name) {
<cde-color-picker-label
[label]="type.name"
[color]="type.color"
[isAnchor]="type.name === selectedCellType()"
[label]="entry.name"
[color]="entry.color"
[isAnchor]="entry.name === selectedCellType()"
(colorPickerOpen)="colorPicker.set($event)"
(colorChange)="updateColor(type, $event)"
(colorChange)="updateColor.emit({ entry, color: $event })"
></cde-color-picker-label>
}
</ng-scrollbar>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { WritableSignal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { Rgb } from '@hra-ui/design-system/color-picker';
import { provideScrolling } from '@hra-ui/design-system/scrolling';
import { RenderComponentOptions, render, screen } from '@testing-library/angular';
import { render, RenderComponentOptions, screen } from '@testing-library/angular';
import userEvent from '@testing-library/user-event';
import { mockClear, mockDeep } from 'jest-mock-extended';
import embed, { Result } from 'vega-embed';
import { CellTypeEntry } from '../../models/cell-type';
import { Rgb } from '@hra-ui/design-system/color-picker';
import { EdgeEntry } from '../../models/edge';

import { DEFAULT_NODE_TARGET_KEY, NodeEntry } from '../../models/node';
import { FileSaverService } from '../../services/file-saver/file-saver.service';
import { HistogramComponent } from './histogram.component';
Expand All @@ -21,17 +20,16 @@ describe('HistogramComponent', () => {
}

const sampleNodes = [createNodeEntry('a', 0, 0), createNodeEntry('b', 0, 2), createNodeEntry('c', 0, 4)];
const sampleEdges: EdgeEntry[] = [
[0, 0, 0, 3, 4, 5, 6],
[1, 0, 2, 3, 4, 5, 6],
[2, 0, 4, 3, 4, 5, 6],
];
const sampleCellTypes: CellTypeEntry[] = [
{ name: 'a', count: 2, color: [0, 0, 0] },
{ name: 'b', count: 4, color: [0, 1, 2] },
{ name: 'c', count: 6, color: [0, 1, 3] },
const sampleData = [
{
type: 'A',
distance: 5,
},
{
type: 'B',
distance: 7,
},
];
const sampleCellTypesSelection: string[] = [sampleCellTypes[0].name, sampleCellTypes[1].name];

const embedResult = mockDeep<Result>();

Expand Down Expand Up @@ -61,12 +59,10 @@ describe('HistogramComponent', () => {
it('should render the histogram using vega', async () => {
const { fixture } = await setup({
componentInputs: {
nodes: sampleNodes,
nodeTargetKey,
edges: sampleEdges,
data: sampleData,
colors: [],
filteredCellTypes: [],
selectedCellType: sampleNodes[0][nodeTargetKey],
cellTypes: sampleCellTypes,
cellTypesSelection: sampleCellTypesSelection,
},
});
await fixture.whenStable();
Expand All @@ -75,15 +71,13 @@ describe('HistogramComponent', () => {
expect(embed).toHaveBeenCalledWith(container, expect.anything(), expect.anything());
});

it('should set empty data when nodes or edges are empty', async () => {
it('should set empty data when input data is empty', async () => {
const { fixture } = await setup({
componentInputs: {
nodes: [],
nodeTargetKey,
edges: [],
data: [],
colors: [],
filteredCellTypes: [],
selectedCellType: sampleNodes[0][nodeTargetKey],
cellTypes: sampleCellTypes,
cellTypesSelection: sampleCellTypesSelection,
},
});
await fixture.whenStable();
Expand All @@ -94,12 +88,10 @@ describe('HistogramComponent', () => {
it('should download in the specified format', async () => {
await setup({
componentInputs: {
nodes: [],
nodeTargetKey,
edges: [],
data: [],
colors: [],
filteredCellTypes: [],
selectedCellType: '',
cellTypes: [],
cellTypesSelection: [],
},
});

Expand All @@ -115,35 +107,15 @@ describe('HistogramComponent', () => {
expect(fileSaveSpy).toHaveBeenCalledWith(imageUrl, 'cde-histogram.svg');
});

it('should updateColor', async () => {
const {
fixture: { componentInstance: instance },
} = await setup({
componentInputs: {
nodes: [],
nodeTargetKey: 'key',
edges: [],
selectedCellType: 'type',
cellTypes: sampleCellTypes,
cellTypesSelection: sampleCellTypesSelection,
},
});

instance.updateColor(sampleCellTypes[0], [255, 255, 255]);
expect(instance.cellTypes()[0].color).toEqual([255, 255, 255]);
});

it('should reset all cell colors', async () => {
const {
fixture: { componentInstance: instance },
} = await setup({
componentInputs: {
nodes: [],
nodeTargetKey,
edges: [],
data: [],
colors: [],
filteredCellTypes: [],
selectedCellType: '',
cellTypes: [],
cellTypesSelection: [],
},
});

Expand Down
Loading

0 comments on commit b220efc

Please sign in to comment.