Skip to content

Commit cf1433c

Browse files
committed
Docs
1 parent c0d8cd5 commit cf1433c

File tree

12 files changed

+664
-41
lines changed

12 files changed

+664
-41
lines changed

component_tests/cli-node18/run.mjs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -205,15 +205,19 @@ ctgA\ttest\tgene\t5000\t6000\t.\t+\t.\tID=gene3;Name=TestGene3
205205
const cmd = `npx jbrowse text-index --file http://localhost:${port}/test.gff3.gz --out ${outDir} --quiet`
206206
console.log(`[TEST] Command: ${cmd}`)
207207
const { stdout, stderr } = await new Promise((resolve, reject) => {
208-
exec(cmd, { encoding: 'utf8', timeout: 60000 }, (error, stdout, stderr) => {
209-
if (error) {
210-
console.log(`[TEST] Command stderr: ${stderr}`)
211-
console.log(`[TEST] Command stdout: ${stdout}`)
212-
reject(error)
213-
} else {
214-
resolve({ stdout, stderr })
215-
}
216-
})
208+
exec(
209+
cmd,
210+
{ encoding: 'utf8', timeout: 60000 },
211+
(error, stdout, stderr) => {
212+
if (error) {
213+
console.log(`[TEST] Command stderr: ${stderr}`)
214+
console.log(`[TEST] Command stdout: ${stdout}`)
215+
reject(error)
216+
} else {
217+
resolve({ stdout, stderr })
218+
}
219+
},
220+
)
217221
})
218222
console.log(`[TEST] Command stdout: ${stdout}`)
219223
console.log(`[TEST] Command stderr: ${stderr}`)

plugins/variants/src/MultiLinearVariantMatrixDisplay/configSchema.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ConfigurationSchema } from '@jbrowse/core/configuration'
2-
import { linearBasicDisplayConfigSchemaFactory } from '@jbrowse/plugin-linear-genome-view'
32
import { types } from '@jbrowse/mobx-state-tree'
3+
import { linearBasicDisplayConfigSchemaFactory } from '@jbrowse/plugin-linear-genome-view'
44

55
import configSchema from '../MultiLinearVariantMatrixRenderer/configSchema.ts'
66

@@ -71,6 +71,17 @@ export default function configSchemaF(pluginManager: PluginManager) {
7171
type: 'number',
7272
defaultValue: 0,
7373
},
74+
/**
75+
* #slot
76+
* Automatically color samples by this metadata attribute when the track
77+
* loads. The attribute must be present in the sample metadata TSV file
78+
* (configured via samplesTsvLocation on the adapter). Leave empty to
79+
* disable auto-coloring.
80+
*/
81+
colorBy: {
82+
type: 'string',
83+
defaultValue: '',
84+
},
7485
},
7586
{
7687
/**

plugins/variants/src/shared/MultiVariantBaseModel.test.ts

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
12
import { readConfObject } from '@jbrowse/core/configuration'
2-
import { getSnapshot } from '@jbrowse/mobx-state-tree'
33

44
import sharedVariantConfigFactory from './SharedVariantConfigSchema.ts'
5+
import { applyColorPalette } from './applyColorPalette.ts'
56

67
describe('SharedVariantConfigSchema', () => {
78
const configSchema = sharedVariantConfigFactory()
@@ -103,7 +104,6 @@ describe('SharedVariantConfigSchema', () => {
103104
})
104105

105106
describe('Config-to-getter fallback logic', () => {
106-
107107
it('referenceDrawingMode getter returns draw when showReferenceAlleles config is true', () => {
108108
const showReferenceAlleles = true
109109
const referenceDrawingModeSetting: string | undefined = undefined
@@ -200,3 +200,121 @@ describe('Config-to-getter fallback logic', () => {
200200
expect(result).toBe(0.2)
201201
})
202202
})
203+
204+
describe('colorBy config slot', () => {
205+
const configSchema = sharedVariantConfigFactory()
206+
207+
it('has default value of empty string', () => {
208+
const config = configSchema.create({
209+
type: 'SharedVariantDisplay',
210+
displayId: 'test-colorby-1',
211+
})
212+
expect(readConfObject(config, 'colorBy')).toBe('')
213+
})
214+
215+
it('can be set to a metadata attribute name', () => {
216+
const config = configSchema.create({
217+
type: 'SharedVariantDisplay',
218+
displayId: 'test-colorby-2',
219+
colorBy: 'population',
220+
})
221+
expect(readConfObject(config, 'colorBy')).toBe('population')
222+
})
223+
})
224+
225+
describe('applyColorPalette', () => {
226+
it('returns original sources when attribute is empty', () => {
227+
const sources = [
228+
{ name: 'sample1', population: 'EUR' },
229+
{ name: 'sample2', population: 'AFR' },
230+
]
231+
const result = applyColorPalette(sources, '')
232+
expect(result).toBe(sources)
233+
})
234+
235+
it('returns original sources when sources array is empty', () => {
236+
const sources: { name: string }[] = []
237+
const result = applyColorPalette(sources, 'population')
238+
expect(result).toBe(sources)
239+
})
240+
241+
it('returns original sources when attribute does not exist', () => {
242+
const sources = [
243+
{ name: 'sample1', population: 'EUR' },
244+
{ name: 'sample2', population: 'AFR' },
245+
]
246+
const result = applyColorPalette(sources, 'nonexistent')
247+
expect(result).toBe(sources)
248+
})
249+
250+
it('applies colors based on attribute values', () => {
251+
const sources = [
252+
{ name: 'sample1', population: 'EUR' },
253+
{ name: 'sample2', population: 'AFR' },
254+
{ name: 'sample3', population: 'EUR' },
255+
]
256+
const result = applyColorPalette(sources, 'population')
257+
258+
expect(result).toHaveLength(3)
259+
expect(result[0]).toHaveProperty('color')
260+
expect(result[1]).toHaveProperty('color')
261+
expect(result[2]).toHaveProperty('color')
262+
263+
// Samples with same population value should have same color
264+
expect(result[0]!.color).toBe(result[2]!.color)
265+
// Samples with different population values should have different colors
266+
expect(result[0]!.color).not.toBe(result[1]!.color)
267+
})
268+
269+
it('assigns colors by frequency (less common values get colors first)', () => {
270+
const sources = [
271+
{ name: 'sample1', population: 'EUR' },
272+
{ name: 'sample2', population: 'EUR' },
273+
{ name: 'sample3', population: 'EUR' },
274+
{ name: 'sample4', population: 'AFR' },
275+
]
276+
const result = applyColorPalette(sources, 'population')
277+
278+
// AFR (1 occurrence) should get the first color from palette
279+
// EUR (3 occurrences) should get the second color
280+
const afrSample = result.find(s => s.population === 'AFR')
281+
const eurSample = result.find(s => s.population === 'EUR')
282+
283+
expect(afrSample).toHaveProperty('color')
284+
expect(eurSample).toHaveProperty('color')
285+
expect(afrSample!.color).not.toBe(eurSample!.color)
286+
})
287+
288+
it('preserves other properties on sources', () => {
289+
const sources = [
290+
{ name: 'sample1', population: 'EUR', region: 'Western', custom: 123 },
291+
{ name: 'sample2', population: 'AFR', region: 'Eastern', custom: 456 },
292+
]
293+
const result = applyColorPalette(sources, 'population')
294+
295+
expect(result[0]!.name).toBe('sample1')
296+
// @ts-expect-error
297+
expect(result[0]!.region).toBe('Western')
298+
// @ts-expect-error
299+
expect(result[0]!.custom).toBe(123)
300+
expect(result[1]!.name).toBe('sample2')
301+
// @ts-expect-error
302+
expect(result[1]!.region).toBe('Eastern')
303+
// @ts-expect-error
304+
expect(result[1]!.custom).toBe(456)
305+
})
306+
307+
it('handles undefined attribute values by converting to string', () => {
308+
const sources = [
309+
{ name: 'sample1', population: 'EUR' },
310+
{ name: 'sample2' }, // no population attribute
311+
]
312+
const result = applyColorPalette(sources, 'population')
313+
314+
expect(result).toHaveLength(2)
315+
expect(result[0]).toHaveProperty('color')
316+
expect(result[1]).toHaveProperty('color')
317+
// They should have different colors (EUR vs undefined)
318+
expect(result[0]!.color).not.toBe(result[1]!.color)
319+
})
320+
})

plugins/variants/src/shared/MultiVariantBaseModel.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,12 @@ export default function MultiVariantBaseModelF(
159159
* #volatile
160160
*/
161161
sourcesVolatile: undefined as Source[] | undefined,
162+
/**
163+
* #volatile
164+
* Tracks whether the colorBy config has been applied (to avoid
165+
* re-applying on every source update)
166+
*/
167+
colorByApplied: false,
162168
/**
163169
* #volatile
164170
*/
@@ -190,6 +196,15 @@ export default function MultiVariantBaseModelF(
190196
*/
191197
mouseoverCanvas: undefined as HTMLCanvasElement | undefined,
192198
}))
199+
.views(self => ({
200+
/**
201+
* #getter
202+
* Returns the effective rendering mode, falling back to config
203+
*/
204+
get renderingMode(): string {
205+
return self.renderingModeSetting ?? getConf(self, 'renderingMode')
206+
},
207+
}))
193208
.actions(self => ({
194209
/**
195210
* #action
@@ -240,6 +255,13 @@ export default function MultiVariantBaseModelF(
240255
self.featuresVolatile = f
241256
},
242257

258+
/**
259+
* #action
260+
*/
261+
setColorByApplied(value: boolean) {
262+
self.colorByApplied = value
263+
},
264+
243265
/**
244266
* #action
245267
*/
@@ -390,14 +412,6 @@ export default function MultiVariantBaseModelF(
390412
return self.showTreeSetting ?? getConf(self, 'showTree')
391413
},
392414

393-
/**
394-
* #getter
395-
* Returns the effective rendering mode, falling back to config
396-
*/
397-
get renderingMode(): string {
398-
return self.renderingModeSetting ?? getConf(self, 'renderingMode')
399-
},
400-
401415
/**
402416
* #getter
403417
* Returns the effective reference drawing mode, derived from config showReferenceAlleles

plugins/variants/src/shared/SharedVariantConfigSchema.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ConfigurationSchema } from '@jbrowse/core/configuration'
2-
import { baseLinearDisplayConfigSchema } from '@jbrowse/plugin-linear-genome-view'
32
import { types } from '@jbrowse/mobx-state-tree'
3+
import { baseLinearDisplayConfigSchema } from '@jbrowse/plugin-linear-genome-view'
44

55
/**
66
* #config SharedVariantDisplay
@@ -53,6 +53,17 @@ export default function sharedVariantConfigFactory() {
5353
type: 'number',
5454
defaultValue: 0,
5555
},
56+
/**
57+
* #slot
58+
* Automatically color samples by this metadata attribute when the track
59+
* loads. The attribute must be present in the sample metadata TSV file
60+
* (configured via samplesTsvLocation on the adapter). Leave empty to
61+
* disable auto-coloring.
62+
*/
63+
colorBy: {
64+
type: 'string',
65+
defaultValue: '',
66+
},
5667
},
5768
{
5869
/**
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { set1 } from '@jbrowse/core/ui/colors'
2+
import { randomColor } from '@jbrowse/core/util/color'
3+
4+
import type { Source } from './types.ts'
5+
6+
/**
7+
* Applies a color palette to sources based on a metadata attribute.
8+
* Colors are assigned based on the frequency of values, with less common
9+
* values getting colors first from the set1 palette.
10+
*
11+
* @param sources - The source array to apply colors to
12+
* @param attribute - The metadata attribute to color by
13+
* @returns A new array of sources with colors applied
14+
*/
15+
export function applyColorPalette(sources: Source[], attribute: string) {
16+
if (!attribute || sources.length === 0) {
17+
return sources
18+
}
19+
20+
// Check if any source has the attribute
21+
const hasAttribute = sources.some(source => attribute in source)
22+
if (!hasAttribute) {
23+
return sources
24+
}
25+
26+
// Count occurrences of each value
27+
const counts = new Map<string, number>()
28+
for (const source of sources) {
29+
const key = String(source[attribute] ?? '')
30+
counts.set(key, (counts.get(key) || 0) + 1)
31+
}
32+
33+
// Create color map, sorting by frequency (least common first gets colors first)
34+
const colorMap = Object.fromEntries(
35+
[...counts.entries()]
36+
.sort((a, b) => a[1] - b[1])
37+
.map(([key], idx) => [key, set1[idx] || randomColor(key)]),
38+
)
39+
40+
// Apply colors to sources
41+
return sources.map(source => ({
42+
...source,
43+
color: colorMap[String(source[attribute] ?? '')],
44+
}))
45+
}

0 commit comments

Comments
 (0)