Skip to content

Commit 3c2c4c4

Browse files
committed
Updates
1 parent 615377b commit 3c2c4c4

File tree

6 files changed

+267
-44
lines changed

6 files changed

+267
-44
lines changed

plugins/alignments/src/LinearReadCloudDisplay/components/LinearReadCloudReactComponent.tsx

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -461,23 +461,6 @@ const Cloud = observer(function Cloud({
461461
selectedFeatureBounds={selectedFeatureBounds}
462462
hoveredFeature={hoveredFeature}
463463
/>
464-
{model.drawCloud && model.cloudTicks ? (
465-
<svg
466-
style={{
467-
position: 'absolute',
468-
top: 0,
469-
left: 50,
470-
pointerEvents: 'none',
471-
height: model.cloudTicks.height,
472-
width: 60,
473-
zIndex: 100,
474-
}}
475-
>
476-
<g transform="translate(55, 0)">
477-
<CloudYScaleBar model={model} orientation="left" />
478-
</g>
479-
</svg>
480-
) : null}
481464
{hoveredMismatchData && mousePosition ? (
482465
<MismatchTooltip
483466
mismatchData={hoveredMismatchData}
@@ -501,9 +484,27 @@ const LinearReadCloudReactComponent = observer(
501484
model: LinearReadCloudDisplayModel
502485
}) {
503486
return (
504-
<BaseDisplayComponent model={model}>
505-
<Cloud model={model} />
506-
</BaseDisplayComponent>
487+
<div>
488+
<BaseDisplayComponent model={model}>
489+
<Cloud model={model} />
490+
</BaseDisplayComponent>
491+
{model.drawCloud && model.cloudTicks ? (
492+
<svg
493+
style={{
494+
position: 'absolute',
495+
top: 0,
496+
left: 50,
497+
pointerEvents: 'none',
498+
height: model.cloudTicks.height,
499+
width: 50,
500+
}}
501+
>
502+
<g transform="translate(45, 0)">
503+
<CloudYScaleBar model={model} orientation="left" />
504+
</g>
505+
</svg>
506+
) : null}
507+
</div>
507508
)
508509
},
509510
)

plugins/alignments/src/LinearReadCloudDisplay/model.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,6 @@ function stateModelFactory(configSchema: AnyConfigurationSchemaType) {
378378
hideMismatches: self.hideMismatches,
379379
hideLargeIndels: self.hideLargeIndels,
380380
showOutline: self.showOutline,
381-
cloudDomain: self.cloudDomain,
382381
visibleModifications: Object.fromEntries(
383382
self.visibleModifications.toJSON(),
384383
),

plugins/alignments/src/RenderLinearReadCloudDisplayRPC/RenderLinearReadCloudDisplay.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ export interface RenderLinearReadCloudDisplayArgs {
2525
flipStrandLongReadChains: boolean
2626
trackMaxHeight?: number
2727
cloudModeHeight?: number
28-
cloudDomain?: [number, number]
2928
highResolutionScaling?: number
3029
exportSVG?: { rasterizeLayers?: boolean; scale?: number }
3130
statusCallback?: (status: string) => void
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import {
2+
calculateCloudTicks,
3+
calculateCloudYOffsetsUtil,
4+
CLOUD_HEIGHT_PADDING,
5+
createCloudScale,
6+
} from './drawFeatsCloud.ts'
7+
8+
import type { ComputedChain } from './drawFeatsCommon.ts'
9+
10+
// Helper to create mock computed chains
11+
function createMockChain(
12+
id: string,
13+
distance: number,
14+
overrides?: Partial<ComputedChain>,
15+
): ComputedChain {
16+
return {
17+
id,
18+
distance,
19+
minX: 0,
20+
maxX: 100,
21+
chain: [],
22+
isPairedEnd: true,
23+
nonSupplementary: [],
24+
...overrides,
25+
}
26+
}
27+
28+
describe('createCloudScale', () => {
29+
test('creates a log scale with correct domain and range', () => {
30+
const scale = createCloudScale(1, 1000, 500)
31+
32+
// Scale should map minDistance to 0
33+
expect(scale(1)).toBe(0)
34+
35+
// Scale should map maxDistance to height - padding
36+
expect(scale(1000)).toBe(500 - CLOUD_HEIGHT_PADDING)
37+
38+
// Values in between should be mapped logarithmically
39+
const midValue = scale(100)
40+
expect(midValue).toBeGreaterThan(0)
41+
expect(midValue).toBeLessThan(500 - CLOUD_HEIGHT_PADDING)
42+
})
43+
44+
test('clamps values outside domain', () => {
45+
const scale = createCloudScale(1, 1000, 500)
46+
47+
// Values below min should clamp to 0
48+
expect(scale(0.1)).toBe(0)
49+
50+
// Values above max should clamp to max range
51+
expect(scale(10000)).toBe(500 - CLOUD_HEIGHT_PADDING)
52+
})
53+
54+
test('handles small maxDistance correctly', () => {
55+
// createCloudScale uses Math.max(2, maxDistance) for domain upper bound
56+
const scale = createCloudScale(1, 1, 500)
57+
58+
// Should still work without errors
59+
expect(scale(1)).toBeDefined()
60+
expect(scale(2)).toBeDefined()
61+
})
62+
})
63+
64+
describe('calculateCloudTicks', () => {
65+
test('generates ticks for a typical domain', () => {
66+
const result = calculateCloudTicks([1, 10000], 500)
67+
68+
expect(result.height).toBe(500)
69+
expect(result.minDistance).toBe(1)
70+
expect(result.maxDistance).toBe(10000)
71+
expect(result.ticks.length).toBeGreaterThan(0)
72+
73+
// Each tick should have a value and y position
74+
for (const tick of result.ticks) {
75+
expect(tick.value).toBeGreaterThan(0)
76+
expect(tick.y).toBeGreaterThanOrEqual(0)
77+
expect(tick.y).toBeLessThanOrEqual(500 - CLOUD_HEIGHT_PADDING)
78+
}
79+
})
80+
81+
test('tick y values increase with tick values', () => {
82+
const result = calculateCloudTicks([1, 10000], 500)
83+
84+
// Ticks should be in increasing order of both value and y
85+
for (let i = 1; i < result.ticks.length; i++) {
86+
const prev = result.ticks[i - 1]!
87+
const curr = result.ticks[i]!
88+
expect(curr.value).toBeGreaterThan(prev.value)
89+
expect(curr.y).toBeGreaterThan(prev.y)
90+
}
91+
})
92+
93+
test('handles small domain', () => {
94+
const result = calculateCloudTicks([1, 100], 300)
95+
96+
expect(result.ticks.length).toBeGreaterThan(0)
97+
expect(result.maxDistance).toBe(100)
98+
})
99+
100+
test('handles large domain', () => {
101+
const result = calculateCloudTicks([1, 1000000], 800)
102+
103+
expect(result.ticks.length).toBeGreaterThan(0)
104+
expect(result.maxDistance).toBe(1000000)
105+
})
106+
})
107+
108+
describe('calculateCloudYOffsetsUtil', () => {
109+
test('computes Y offsets from chain distances', () => {
110+
const chains = [
111+
createMockChain('chain1', 100),
112+
createMockChain('chain2', 500),
113+
createMockChain('chain3', 1000),
114+
]
115+
116+
const result = calculateCloudYOffsetsUtil(chains, 500)
117+
118+
expect(result.chainYOffsets.size).toBe(3)
119+
expect(result.cloudMaxDistance).toBe(1000)
120+
121+
// Y offsets should increase with distance
122+
const y1 = result.chainYOffsets.get('chain1')!
123+
const y2 = result.chainYOffsets.get('chain2')!
124+
const y3 = result.chainYOffsets.get('chain3')!
125+
126+
expect(y1).toBeLessThan(y2)
127+
expect(y2).toBeLessThan(y3)
128+
})
129+
130+
test('chains with distance 0 are placed at y=0', () => {
131+
const chains = [
132+
createMockChain('chain1', 0),
133+
createMockChain('chain2', 500),
134+
]
135+
136+
const result = calculateCloudYOffsetsUtil(chains, 500)
137+
138+
expect(result.chainYOffsets.get('chain1')).toBe(0)
139+
expect(result.chainYOffsets.get('chain2')).toBeGreaterThan(0)
140+
})
141+
142+
test('uses default max distance when all chains are singletons (distance 0)', () => {
143+
const chains = [
144+
createMockChain('chain1', 0),
145+
createMockChain('chain2', 0),
146+
createMockChain('chain3', 0),
147+
]
148+
149+
const result = calculateCloudYOffsetsUtil(chains, 500)
150+
151+
// Default max distance is 1000
152+
expect(result.cloudMaxDistance).toBe(1000)
153+
154+
// All chains should be at y=0
155+
expect(result.chainYOffsets.get('chain1')).toBe(0)
156+
expect(result.chainYOffsets.get('chain2')).toBe(0)
157+
expect(result.chainYOffsets.get('chain3')).toBe(0)
158+
})
159+
160+
test('computes maxDistance from the largest distance in visible data', () => {
161+
const chains = [
162+
createMockChain('chain1', 100),
163+
createMockChain('chain2', 5000),
164+
createMockChain('chain3', 200),
165+
]
166+
167+
const result = calculateCloudYOffsetsUtil(chains, 500)
168+
169+
// Should find the max distance from the data
170+
expect(result.cloudMaxDistance).toBe(5000)
171+
})
172+
173+
test('handles empty chains array', () => {
174+
const result = calculateCloudYOffsetsUtil([], 500)
175+
176+
expect(result.chainYOffsets.size).toBe(0)
177+
// Uses default max distance when no positive distances found
178+
expect(result.cloudMaxDistance).toBe(1000)
179+
})
180+
181+
test('scale adapts when scrolling to region with larger insert sizes', () => {
182+
// First region with smaller insert sizes
183+
const smallInsertChains = [
184+
createMockChain('chain1', 200),
185+
createMockChain('chain2', 300),
186+
createMockChain('chain3', 400),
187+
]
188+
189+
const result1 = calculateCloudYOffsetsUtil(smallInsertChains, 500)
190+
expect(result1.cloudMaxDistance).toBe(400)
191+
192+
// Second region with larger insert sizes (simulating scroll)
193+
const largeInsertChains = [
194+
createMockChain('chain4', 1000),
195+
createMockChain('chain5', 5000),
196+
createMockChain('chain6', 10000),
197+
]
198+
199+
const result2 = calculateCloudYOffsetsUtil(largeInsertChains, 500)
200+
expect(result2.cloudMaxDistance).toBe(10000)
201+
202+
// The max distance should be different, showing the scale adapts
203+
expect(result2.cloudMaxDistance).toBeGreaterThan(result1.cloudMaxDistance)
204+
})
205+
206+
test('Y offsets are bounded by height minus padding', () => {
207+
const chains = [
208+
createMockChain('chain1', 100),
209+
createMockChain('chain2', 1000),
210+
createMockChain('chain3', 10000),
211+
]
212+
213+
const height = 500
214+
const result = calculateCloudYOffsetsUtil(chains, height)
215+
216+
for (const [, yOffset] of result.chainYOffsets) {
217+
expect(yOffset).toBeGreaterThanOrEqual(0)
218+
expect(yOffset).toBeLessThanOrEqual(height - CLOUD_HEIGHT_PADDING)
219+
}
220+
})
221+
222+
test('consistent scale for same data', () => {
223+
const chains = [
224+
createMockChain('chain1', 100),
225+
createMockChain('chain2', 500),
226+
createMockChain('chain3', 1000),
227+
]
228+
229+
const result1 = calculateCloudYOffsetsUtil(chains, 500)
230+
const result2 = calculateCloudYOffsetsUtil(chains, 500)
231+
232+
// Same input should produce same output
233+
expect(result1.cloudMaxDistance).toBe(result2.cloudMaxDistance)
234+
expect(result1.chainYOffsets.get('chain1')).toBe(
235+
result2.chainYOffsets.get('chain1'),
236+
)
237+
expect(result1.chainYOffsets.get('chain2')).toBe(
238+
result2.chainYOffsets.get('chain2'),
239+
)
240+
expect(result1.chainYOffsets.get('chain3')).toBe(
241+
result2.chainYOffsets.get('chain3'),
242+
)
243+
})
244+
})

plugins/alignments/src/RenderLinearReadCloudDisplayRPC/drawFeatsCloud.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,33 +55,14 @@ export function calculateCloudTicks(
5555
* Calculate Y-offsets using logarithmic scaling for cloud mode
5656
* @param computedChains - Pre-computed chain data with distances
5757
* @param height - Canvas height
58-
* @param cloudDomain - Optional [min, max] domain. If provided, use it; otherwise compute from data
5958
*/
6059
export function calculateCloudYOffsetsUtil(
6160
computedChains: ComputedChain[],
6261
height: number,
63-
cloudDomain?: [number, number],
6462
) {
65-
// Calculate Y-offsets for each chain using the d3 scale
6663
const chainYOffsets = new Map<string, number>()
6764

68-
// If cloudDomain is provided, use it directly
69-
if (cloudDomain) {
70-
const [, maxDistance] = cloudDomain
71-
const scale = createCloudScale(1, maxDistance, height)
72-
73-
for (const { id, distance } of computedChains) {
74-
const top = distance > 0 ? scale(distance) : 0
75-
chainYOffsets.set(id, top)
76-
}
77-
78-
return {
79-
chainYOffsets,
80-
cloudMaxDistance: maxDistance,
81-
}
82-
}
83-
84-
// Otherwise, compute maxDistance from data
65+
// Compute maxDistance from visible data
8566
let maxDistance = Number.MIN_VALUE
8667

8768
for (const { distance } of computedChains) {

plugins/alignments/src/RenderLinearReadCloudDisplayRPC/executeRenderLinearReadCloudDisplay.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ export async function executeRenderLinearReadCloudDisplay({
5959
flipStrandLongReadChains,
6060
trackMaxHeight,
6161
cloudModeHeight,
62-
cloudDomain,
6362
highResolutionScaling,
6463
exportSVG,
6564
statusCallback = () => {},
@@ -254,7 +253,7 @@ export async function executeRenderLinearReadCloudDisplay({
254253
view: viewSnap,
255254
calculateYOffsets: (chains: ComputedChain[]) => {
256255
return drawCloud
257-
? calculateCloudYOffsetsUtil(chains, actualHeight, cloudDomain)
256+
? calculateCloudYOffsetsUtil(chains, actualHeight)
258257
: calculateStackYOffsetsUtil(
259258
chains,
260259
featureHeight,

0 commit comments

Comments
 (0)