Skip to content

Commit 1909ff0

Browse files
authored
Cici Punch List (#16)
* PopulationOnlyWidget. It begins * Population Only Widget build out * WidgetService. Remove non-existent css class * Bug fix: context-menu dismissal nolonger disables tooltips * Add Assemblies to context menu. It begins * Assembly context menu build out * Prep for AssemblyVisualizationLook tooltip edit * PopulationWidget. Remove thin dividing line between items * PopulationWidget and Tooltip edits * clean up * typo * Context Menu hacking * Population tooltip. Now highligts selected population * Added population count to tooltip * Population Tooltip futzing * Added release tag retrieval function * Added Info button
1 parent 73c1ba0 commit 1909ff0

23 files changed

+937
-238
lines changed

index.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@
1111
<link href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css" rel="stylesheet">
1212
<!-- Bootstrap JS Bundle with Popper -->
1313
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
14+
15+
<style>
16+
#info-button {
17+
color: #6c757d !important;
18+
}
19+
#info-button:hover {
20+
border: 1px solid #6c757d !important;
21+
border-radius: 0.375rem;
22+
}
23+
</style>
1424

1525
<script type="module" src="src/main.js"></script>
1626
</head>
@@ -23,6 +33,11 @@
2333
</button>
2434
<div class="collapse navbar-collapse" id="navbarNav">
2535
<div id="pgb-locus-input-container" class="mx-auto w-50"></div>
36+
<div class="navbar-nav ms-auto">
37+
<button type="button" class="btn btn-link" id="info-button" data-bs-toggle="popover" data-bs-placement="bottom" data-bs-content="Loading..." data-bs-title="Release Information">
38+
<i class="bi bi-info-circle"></i>
39+
</button>
40+
</div>
2641
</div>
2742
</div>
2843
</nav>

notes/artwork/pop.png

192 KB
Loading

notes/artwork/superpop-pop.png

112 KB
Loading
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
const assembly_metadata =
2+
{
3+
"count": {
4+
"sex": {
5+
"female": 56,
6+
"male": 38
7+
},
8+
"superpopulation": {
9+
"AMR": 32,
10+
"AFR": 48,
11+
"EAS": 8,
12+
"N/A": 4,
13+
"SAS": 2
14+
},
15+
"population": {
16+
"CLM": 8,
17+
"ACB": 14,
18+
"GWD": 16,
19+
"ESN": 2,
20+
"CHS": 6,
21+
"PUR": 16,
22+
"PEL": 8,
23+
"MSL": 8,
24+
"N/A": 4,
25+
"KHV": 2,
26+
"PJL": 2,
27+
"YRI": 4,
28+
"ASW": 2,
29+
"MKK": 2
30+
}
31+
},
32+
"frequency": {
33+
"sex": {
34+
"female": 1,
35+
"male": 1
36+
},
37+
"superpopulation": {
38+
"AMR": 1,
39+
"AFR": 1,
40+
"EAS": 1,
41+
"N/A": 1,
42+
"SAS": 1
43+
},
44+
"population": {
45+
"CLM": 1,
46+
"ACB": 1,
47+
"GWD": 1,
48+
"ESN": 1,
49+
"CHS": 1,
50+
"PUR": 1,
51+
"PEL": 1,
52+
"MSL": 1,
53+
"N/A": 1,
54+
"KHV": 1,
55+
"PJL": 1,
56+
"YRI": 1,
57+
"ASW": 1,
58+
"MKK": 1
59+
}
60+
}
61+
}

src/assemblyMetadataService.js

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getSuperpopulationName, getPopulationName, findSuperpopulationForPopulation } from './utils/populationUtils.js';
22
import {frequencyAnalysisService} from "./frequencyAnalysisService.js"
3+
import eventBus from "./utils/eventBus.js"
34

45
/**
56
* AssemblyMetadataService - Manages and provides access to assembly metadata
@@ -14,19 +15,22 @@ class AssemblyMetadataService {
1415

1516
this.metadata = new Map(); // nodeId -> metadata object
1617
this.totalAssemblies = 0; // Total count across all nodes
18+
this.selectedPopulation = null;
19+
20+
this.popSelectUnsub = eventBus.subscribe('population:selected', data => {
21+
this.handleSelectionEvent(data, 'population');
22+
})
23+
24+
this.popDeselectUnsub = eventBus.subscribe('population:deselected', data => {
25+
this.selectedPopulation = null;
26+
});
1727

1828
AssemblyMetadataService.instance = this;
1929
}
2030

21-
/**
22-
* Get the singleton instance
23-
* @returns {AssemblyMetadataService} The singleton instance
24-
*/
25-
static getInstance() {
26-
if (!AssemblyMetadataService.instance) {
27-
AssemblyMetadataService.instance = new AssemblyMetadataService();
28-
}
29-
return AssemblyMetadataService.instance;
31+
handleSelectionEvent(data, eventType) {
32+
const { acronym } = data
33+
this.selectedPopulation = acronym;
3034
}
3135

3236
/**
@@ -161,6 +165,51 @@ class AssemblyMetadataService {
161165
return html;
162166
}
163167

168+
/**
169+
* Generate HTML snippet showing population breakdown for a node
170+
* Simple presentation of population frequency values as percentages
171+
* @param {string} nodeId - The node identifier
172+
* @returns {string} HTML snippet with population breakdown
173+
*/
174+
getPopulationTooltip(nodeId) {
175+
176+
let html = '<div class="population-tooltip">'
177+
178+
const { frequency, count } = this.metadata.get(nodeId)
179+
180+
const populationCounts = Object.entries(count.population)
181+
const populationFrequencies = Object.entries(frequency.population)
182+
183+
for (let i = 0; i < populationFrequencies.length; i++ ) {
184+
185+
const [ acronym, frequency ] = populationFrequencies[ i ];
186+
const [_, count ] = populationCounts[ i ];
187+
188+
if ('N/A' === acronym) {
189+
continue;
190+
}
191+
192+
const emphasisStyle = acronym === this.selectedPopulation ? 'style="font-size: 0.9rem; font-weight: bold;"' : '';
193+
194+
html += `<div class="population-item"><span class="population-name" ${emphasisStyle}>${getPopulationName(acronym)}</span><span class="population-count" ${emphasisStyle}>${count}</span><span class="population-percentage" ${emphasisStyle}>${ AssemblyMetadataService.formatNumber(frequency) }</span></div>`;
195+
}
196+
197+
html += '</div>';
198+
199+
return html;
200+
}
201+
202+
/**
203+
* Get the singleton instance
204+
* @returns {AssemblyMetadataService} The singleton instance
205+
*/
206+
static getInstance() {
207+
if (!AssemblyMetadataService.instance) {
208+
AssemblyMetadataService.instance = new AssemblyMetadataService();
209+
}
210+
return AssemblyMetadataService.instance;
211+
}
212+
164213
static formatNumber(frequency) {
165214

166215
if (0 === frequency) {

src/assemblyVisualizationLook.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import GeometryFactory from "./geometryFactory.js"
77
import eventBus from "./utils/eventBus.js"
88
import {getAppleCrayonColorByName} from "./utils/color/color.js"
99
import lineMaterialResolutionService from './lineMaterialResolutionService.js'
10+
import GenomicService from "./genomicService.js"
1011

1112
class AssemblyVisualizationLook extends Look {
1213

@@ -315,6 +316,77 @@ class AssemblyVisualizationLook extends Look {
315316
});
316317
}
317318

319+
// createNodeTooltipContent(nodeObject) {
320+
// const { nodeName } = nodeObject.userData;
321+
// const assemblies = this.genomicService.getAssemblyListForNodeName(nodeName);
322+
//
323+
// // Group assemblies by assembly name, then sort within each group by haplotype
324+
// const assemblyGroups = {};
325+
// assemblies.forEach(assembly => {
326+
// const parts = assembly.split('#');
327+
// const assemblyName = parts[0];
328+
// if (!assemblyGroups[assemblyName]) {
329+
// assemblyGroups[assemblyName] = [];
330+
// }
331+
// assemblyGroups[assemblyName].push(assembly);
332+
// });
333+
//
334+
// // Sort each group by haplotype and flatten into a single array
335+
// const sortedAssemblies = Object.keys(assemblyGroups)
336+
// .sort() // Sort assembly names alphabetically
337+
// .flatMap(assemblyName =>
338+
// assemblyGroups[assemblyName].sort((a, b) => {
339+
// const haplotypeA = a.split('#')[1];
340+
// const haplotypeB = b.split('#')[1];
341+
// return haplotypeA.localeCompare(haplotypeB);
342+
// })
343+
// );
344+
//
345+
// const selectedAssembly = this.assemblyWidget?.selectedAssembly;
346+
//
347+
// // Create table rows with 4 columns
348+
// const tableRows = [];
349+
// for (let i = 0; i < sortedAssemblies.length; i += 4) {
350+
// const row = sortedAssemblies.slice(i, i + 4);
351+
// let cells = row.map(assembly => {
352+
//
353+
// let isSelected
354+
// if (selectedAssembly) {
355+
// isSelected = selectedAssembly && assembly === selectedAssembly.name
356+
// } else {
357+
// isSelected = false
358+
// }
359+
//
360+
// const colorStyle = true === isSelected ? `style="color: #dc3545; font-weight: bold;"` : ''
361+
//
362+
// const [ assemblyName, haplotype ] = GenomicService.presentationAssemblyLabel(assembly);
363+
//
364+
// // HG00438&thinsp;&middot;&thinsp;h2
365+
// const str = `${assemblyName}&middot;hap${haplotype}`
366+
// return `<td class="assembly-cell" ${colorStyle}>${str}</td>`;
367+
// }).join('');
368+
//
369+
// // Pad with empty cells if needed
370+
// while (row.length < 4) {
371+
// cells += '<td class="assembly-cell empty"></td>';
372+
// row.push('');
373+
// }
374+
// tableRows.push(`<tr>${cells}</tr>`);
375+
// }
376+
//
377+
// return `<div class="look-tooltip">
378+
// <div class="node-section">
379+
// <!-- <div class="node-title">Node: ${nodeName}</div> -->
380+
// <div class="assembly-table-container">
381+
// <div class="assembly-table-title">Assemblies</div>
382+
// <table class="assembly-table">
383+
// ${tableRows.join('')}
384+
// </table>
385+
// </div>
386+
// </div>
387+
// </div>`
388+
// }
389+
318390
/**
319391
* Activate this look - subscribe to events
320392
*/
Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { app } from "./main.js"
22

3-
class SequenceService {
3+
class ContextMenuService {
44
constructor(container, raycastService, genomicService) {
55

66
this.container = container;
@@ -14,22 +14,39 @@ class SequenceService {
1414

1515
createContextMenu(container) {
1616
this.contextMenu = document.createElement('div');
17+
container.appendChild(this.contextMenu);
18+
1719
this.contextMenu.id = 'pgb-context-menu';
20+
1821
this.contextMenu.style.display = 'none';
22+
1923
this.contextMenu.style.position = 'absolute';
24+
2025
this.contextMenu.style.zIndex = '9999';
26+
2127
this.contextMenu.style.backgroundColor = 'white';
22-
this.contextMenu.style.border = '1px solid #ccc';
28+
29+
this.contextMenu.style.borderWidth = 'thin';
30+
this.contextMenu.style.borderStyle = 'solid';
31+
this.contextMenu.style.borderColor = '#ccc';
2332
this.contextMenu.style.borderRadius = '4px';
24-
this.contextMenu.style.padding = '4px 0';
33+
34+
this.contextMenu.style.paddingTop = '4px';
35+
this.contextMenu.style.paddingBottom = '4px';
36+
this.contextMenu.style.paddingLeft = '0';
37+
this.contextMenu.style.paddingRight = '0';
38+
2539
this.contextMenu.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
40+
2641
this.contextMenu.style.pointerEvents = 'auto';
42+
2743
this.contextMenu.innerHTML = `
2844
<ul style="list-style: none; padding: 0; margin: 0;">
29-
<li data-action="copy-info" style="padding: 8px 16px; cursor: pointer; pointer-events: auto;">Copy Assembly & Sequence</li>
45+
<li data-action="copy-info" style="padding: 8px 16px; cursor: pointer; pointer-events: auto;">Copy Sequence</li>
46+
<li data-action="assemblies" style="padding: 8px 16px; cursor: pointer; pointer-events: auto;">Copy Assemblies, Haplotypes, and Sequence IDs</li>
3047
</ul>
3148
`;
32-
container.appendChild(this.contextMenu);
49+
3350

3451
const listItems = this.contextMenu.querySelectorAll('li');
3552
for (const listItem of listItems) {
@@ -73,11 +90,15 @@ class SequenceService {
7390
return;
7491
}
7592

76-
const { sequence } = payload
93+
7794
let textToCopy;
7895

7996
if (action === 'copy-info') {
97+
const { sequence } = payload
8098
textToCopy = `Sequence:\n${sequence}`;
99+
} else if (action === 'assemblies') {
100+
const assemblyHaplotypeSequenceIds = this.genomicService.getAssemblyListForNodeName(this.currentNodeName).join('\n');
101+
textToCopy = `${assemblyHaplotypeSequenceIds}`;
81102
}
82103

83104
if (textToCopy) {
@@ -88,14 +109,14 @@ class SequenceService {
88109
});
89110
}
90111
this.dismissContextMenu();
91-
92-
app.enableTooltip()
93112
}
94113

95114
dismissContextMenu() {
96115
if (this.contextMenu) {
97116
this.contextMenu.style.display = 'none';
98117
}
118+
// Re-enable tooltip when context menu is dismissed
119+
app.enableTooltip();
99120
}
100121

101122
raycastClickHandler(intersection, event) {
@@ -137,4 +158,4 @@ class SequenceService {
137158
}
138159
}
139160

140-
export default SequenceService;
161+
export default ContextMenuService;

src/genomicService.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class GenomicService {
2727
console.log(`locus length ${ prettyPrint(this.locus.endBP - this.locus.startBP) }`)
2828

2929
this.startNode = undefined
30-
for (const [nodeName, { assembly, assembly_metadata }] of Object.entries(nodes)) {
30+
for (const [nodeName, { length, assembly, assembly_metadata }] of Object.entries(nodes)) {
3131

3232
if (undefined === this.startNode) {
3333
this.startNode = nodeName
@@ -38,7 +38,8 @@ class GenomicService {
3838
assemblySet.add(GenomicService.tripleKey(item))
3939
}
4040

41-
this.nodeMetadata.set(nodeName, { assemblySet, sequence: sequences[nodeName], frequency: assembly_metadata.frequency });
41+
const { frequency, count } = assembly_metadata
42+
this.nodeMetadata.set(nodeName, { assemblySet, frequency, count, length, sequence: sequences[nodeName] });
4243

4344
}
4445

src/heatmapLook.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class HeatmapLook extends Look {
1616

1717
createNodeTooltipContent(nodeObject) {
1818
const { nodeName } = nodeObject.userData;
19-
return assemblyMetadataService.getDemographicBreakdownHTML(nodeName)
19+
return assemblyMetadataService.getPopulationTooltip(nodeName)
2020
}
2121

2222
handleSelectionEvent(data, eventType) {

0 commit comments

Comments
 (0)