Skip to content

Commit 4ff3be5

Browse files
committed
Making the plot mass native
1 parent 7cfa89b commit 4ff3be5

File tree

6 files changed

+153
-68
lines changed

6 files changed

+153
-68
lines changed

client/plots/dmr/DmrPlot.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { PlotBase } from '../PlotBase.ts'
2+
import { getCompInit, copyMerge, type RxComponent } from '#rx'
3+
import { sayerror } from '#dom'
4+
import { dofetch3 } from '#common/dofetch'
5+
import { first_genetrack_tolist } from '#common/1stGenetk'
6+
import type { DmrConfig, DmrDom, DmrResult, BedItem } from './DmrTypes.ts'
7+
8+
class DmrPlot extends PlotBase implements RxComponent {
9+
static type = 'dmr'
10+
type = DmrPlot.type
11+
declare dom: DmrDom
12+
blockInstance: InstanceType<any> | null = null
13+
14+
constructor(opts: any, api: any) {
15+
super(opts, api)
16+
this.dom = {
17+
header: opts.header,
18+
holder: opts.holder.append('div'),
19+
error: opts.holder.append('div'),
20+
loading: opts.holder.append('div').text('Running DMR analysis…')
21+
}
22+
}
23+
24+
getState(appState: { plots: DmrConfig[] }): { config: DmrConfig } {
25+
const config = appState.plots.find(p => p.id === this.id)
26+
if (!config) throw new Error(`No plot with id='${this.id}' found`)
27+
return { config }
28+
}
29+
30+
async init() {}
31+
32+
async main() {
33+
const config = this.state.config as DmrConfig
34+
if (this.dom.header) this.dom.header.text(config.headerText || 'DMR Analysis')
35+
36+
this.dom.holder.selectAll('*').remove()
37+
this.dom.error.selectAll('*').remove()
38+
this.dom.loading.style('display', 'block')
39+
40+
try {
41+
const { genome, dslabel, chr, start, stop, group1, group2 } = config
42+
const dmrResult: DmrResult = await dofetch3('termdb/dmr', {
43+
body: { genome, dslabel, chr, start, stop, group1, group2 }
44+
})
45+
if (dmrResult.error) throw new Error(dmrResult.error)
46+
47+
const genomeObj = this.app.opts.genome
48+
const tklst: { type: string; name: string; bedItems?: BedItem[]; __isgene?: boolean }[] = []
49+
first_genetrack_tolist(genomeObj, tklst)
50+
51+
tklst.push({
52+
type: 'bedj',
53+
name: 'DMRs',
54+
bedItems: (dmrResult.dmrs ?? []).map(dmr => {
55+
const alpha = Math.round(Math.min(255, (0.5 + dmr.probability * 0.5) * 255))
56+
const hex = alpha.toString(16).padStart(2, '0')
57+
const base = dmr.direction === 'hyper' ? '#e66101' : '#5e81f4'
58+
return { chr: dmr.chr, start: dmr.start, stop: dmr.stop, color: base + hex }
59+
})
60+
})
61+
62+
const { Block } = await import('#src/block')
63+
this.blockInstance = new Block({
64+
holder: this.dom.holder,
65+
genome: genomeObj,
66+
chr,
67+
start,
68+
stop,
69+
tklst,
70+
nobox: true,
71+
width: 800,
72+
hidegenelegend: true
73+
})
74+
} catch (e: unknown) {
75+
const msg = e instanceof Error ? e.message : String(e)
76+
sayerror(this.dom.error, msg)
77+
}
78+
this.dom.loading.style('display', 'none')
79+
}
80+
}
81+
82+
export const componentInit = getCompInit(DmrPlot)
83+
84+
export function getPlotConfig(opts: Partial<DmrConfig>): DmrConfig {
85+
return copyMerge({ chartType: 'dmr', headerText: 'DMR Analysis' }, opts)
86+
}

client/plots/dmr/DmrTypes.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { Elem } from '../../types/d3'
2+
3+
/** Config shape passed via plot_create from VolcanoInteractions */
4+
export type DmrConfig = {
5+
chartType: 'dmr'
6+
id: string
7+
headerText: string
8+
genome: string
9+
dslabel: string
10+
chr: string
11+
start: number
12+
stop: number
13+
group1: { sample: string }[]
14+
group2: { sample: string }[]
15+
}
16+
17+
export type DmrDom = {
18+
header: Elem
19+
holder: Elem
20+
error: Elem
21+
loading: Elem
22+
}
23+
24+
export type Dmr = {
25+
chr: string
26+
start: number
27+
stop: number
28+
direction: 'hyper' | 'hypo'
29+
probability: number
30+
}
31+
32+
export type DmrResult = {
33+
error?: string
34+
dmrs?: Dmr[]
35+
}
36+
37+
export type BedItem = {
38+
chr: string
39+
start: number
40+
stop: number
41+
color: string
42+
}

client/plots/importPlot.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export async function importPlot(chartType, notFoundMessage = '') {
1717
case 'correlationVolcano':
1818
return await import(`./corrVolcano/CorrelationVolcano.ts`)
1919

20+
case 'dmr':
21+
return await import('./dmr/DmrPlot.ts')
22+
2023
case 'genomeBrowser':
2124
return await import('./gb/GB.ts')
2225

client/plots/volcano/interactions/VolcanoInteractions.ts

Lines changed: 17 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { MassAppApi } from '#mass/types/mass'
2-
import { downloadTable, GeneSetEditUI, MultiTermWrapperEditUI, newSandboxDiv } from '#dom'
2+
import { downloadTable, GeneSetEditUI, MultiTermWrapperEditUI } from '#dom'
33
import { to_svg } from '#src/client'
44
import type { VolcanoDom, VolcanoPlotConfig } from '../VolcanoTypes'
55
import { DNA_METHYLATION, GENE_EXPRESSION } from '#shared/terms.js'
@@ -187,84 +187,40 @@ export class VolcanoInteractions {
187187
})
188188
}
189189

190-
/** When clicking on a DM data point, runs GPDM analysis then opens a sandbox
191-
* (sibling to the volcano in the mass plotDiv) with a genome browser Block
192-
* at the gene locus and annotation-aware DMRs overlaid as highlight regions
193-
* (orange=hyper, blue=hypo). */
190+
/** When clicking on a DM data point, dispatches a DMR plot that runs GPDM
191+
* analysis and renders a genome browser Block with DMR regions overlaid. */
194192
async launchGpdm(geneName: string, promoterId?: string) {
195193
const config = this.app.getState().plots.find((p: VolcanoPlotConfig) => p.id === this.id)
196194
if (config.termType !== DNA_METHYLATION) return
197195

198196
const genome = this.app.vocabApi.vocab.genome
199197
const dslabel = this.app.vocabApi.vocab.dslabel
200-
const genomeObj = this.app.opts.genome
201198

202-
// Look up gene coordinates
203199
const geneResult = await dofetch3('genelookup', {
204200
body: { deep: 1, input: geneName, genome }
205201
})
206-
if (geneResult.error || !geneResult.gmlst || geneResult.gmlst.length === 0) {
202+
if (geneResult.error || !geneResult.gmlst?.length) {
207203
window.alert(`Could not find coordinates for gene "${geneName}"`)
208204
return
209205
}
210206

211207
const gm = geneResult.gmlst[0]
212208
const pad = 2000
213-
const chr = gm.chr
214-
const start = Math.max(0, gm.start - pad)
215-
const stop = gm.stop + pad
216209

217-
const group1 = config.samplelst.groups[0].values || []
218-
const group2 = config.samplelst.groups[1].values || []
219-
220-
const sandbox = newSandboxDiv(this.dom.holder)
221-
sandbox.header.text(promoterId ? `DMR: ${geneName} (${promoterId})` : `DMR: ${geneName}`)
222-
const waitDiv = sandbox.body.append('div').style('padding', '10px').text('Running GPDM analysis…')
223-
224-
const dmrResult = await dofetch3('termdb/dmr', {
225-
body: { genome, dslabel, chr, start, stop, group1, group2 }
226-
})
227-
waitDiv.remove()
228-
229-
if (dmrResult.error) {
230-
sandbox.body.append('div').style('padding', '10px').style('color', 'red').text(dmrResult.error)
231-
return
232-
}
233-
234-
const { first_genetrack_tolist } = await import('#common/1stGenetk')
235-
const tklst: any[] = []
236-
first_genetrack_tolist(genomeObj, tklst)
237-
// Add DMR regions as a custom bedj track — orange=hyper, blue=hypo, alpha scaled by probability
238-
tklst.push({
239-
type: 'bedj',
240-
name: 'DMRs',
241-
bedItems: (dmrResult.dmrs ?? []).map((dmr: any) => {
242-
const alpha = Math.round(Math.min(255, (0.5 + dmr.probability * 0.5) * 255))
243-
const hex = alpha.toString(16).padStart(2, '0')
244-
const base = dmr.direction === 'hyper' ? '#e66101' : '#5e81f4'
245-
return { chr: dmr.chr, start: dmr.start, stop: dmr.stop, color: base + hex }
246-
})
247-
})
248-
const { Block } = await import('#src/block')
249-
new Block({
250-
holder: sandbox.body,
251-
genome: genomeObj,
252-
chr,
253-
start,
254-
stop,
255-
tklst,
256-
nobox: true,
257-
width: 800,
258-
hidegenelegend: true
210+
this.app.dispatch({
211+
type: 'plot_create',
212+
config: {
213+
chartType: 'dmr',
214+
headerText: promoterId ? `DMR: ${geneName} (${promoterId})` : `DMR: ${geneName}`,
215+
genome,
216+
dslabel,
217+
chr: gm.chr,
218+
start: Math.max(0, gm.start - pad),
219+
stop: gm.stop + pad,
220+
group1: config.samplelst.groups[0].values || [],
221+
group2: config.samplelst.groups[1].values || []
222+
}
259223
})
260-
261-
if (!dmrResult.dmrs?.length) {
262-
sandbox.body
263-
.append('div')
264-
.style('padding', '6px 0')
265-
.style('color', '#888')
266-
.text('No significant DMRs detected in this region')
267-
}
268224
}
269225

270226
async launchDEGClustering() {

client/plots/volcano/view/DataPointMouseEvents.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class DataPointMouseEvents {
5656
this.addTooltipRow(table, 'Original p-value', roundValueAuto(d.original_p_value))
5757
this.addTooltipRow(table, 'Adjusted p-value', roundValueAuto(d.adjusted_p_value))
5858
if (this.termType === DNA_METHYLATION && d.gene_name) {
59-
this.addTooltipRow(table, '', 'Click for probe-level GP analysis')
59+
this.addTooltipRow(table, '', 'Click for region-level GP analysis')
6060
}
6161
}
6262
}

server/routes/termdb.dmr.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { RouteApi, TermdbDmrRequest, TermdbDmrSuccessResponse } from '#types'
22
import { TermdbDmrPayload } from '#types/checkers'
3-
// import { run_R } from '@sjcrh/proteinpaint-r' // replaced by GPDM Python analysis
43
import { run_python } from '@sjcrh/proteinpaint-python'
54
import { invalidcoord } from '#shared/common.js'
65
import serverconfig from '#src/serverconfig.js'
@@ -10,8 +9,6 @@ import path from 'path'
109
import fs from 'fs'
1110
import crypto from 'crypto'
1211

13-
// Ensure the gpdm cache subdirectory exists (mirrors the grin2 pattern)
14-
1512
const cachedir_gpdm = path.join(serverconfig.cachedir, 'gpdm')
1613
if (!fs.existsSync(cachedir_gpdm)) fs.mkdirSync(cachedir_gpdm, { recursive: true })
1714

@@ -37,7 +34,7 @@ function init({ genomes }) {
3734
if (!genome) throw new Error('invalid genome')
3835
const ds = genome.datasets?.[q.dslabel]
3936
if (!ds) throw new Error('invalid ds')
40-
if (!ds.queries?.dnaMethylation) throw new Error('not supported')
37+
if (!ds.queries?.dnaMethylation) throw new Error('analysis not supported')
4138

4239
if (!Array.isArray(q.group1) || q.group1.length == 0) throw new Error('group1 not non empty array')
4340
if (!Array.isArray(q.group2) || q.group2.length == 0) throw new Error('group2 not non empty array')
@@ -69,8 +66,9 @@ function init({ genomes }) {
6966

7067
// PNG is written to cachedir_gpdm by Python and kept there for reference
7168
res.send({ status: 'ok', dmrs: result.dmrs } as TermdbDmrSuccessResponse)
72-
} catch (e: any) {
73-
res.send({ error: e.message || e })
69+
} catch (e: unknown) {
70+
const msg = e instanceof Error ? e.message : String(e)
71+
res.send({ error: msg })
7472
if (e instanceof Error && e.stack) console.log(e)
7573
}
7674
}

0 commit comments

Comments
 (0)