jbrowse-plugin-graphgenomeviewer 7.4.4 → 7.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{GbzBaseSyntenyAdapter-A5MNPCKX.js → GbzBaseSyntenyAdapter-3PVPTKNW.js} +2 -2
- package/dist/chunks/{GbzBaseSyntenyAdapter-A5MNPCKX.js.map → GbzBaseSyntenyAdapter-3PVPTKNW.js.map} +1 -1
- package/dist/chunks/{GraphGenomeView-SEAPDRXS.js → GraphGenomeView-GQW4YC23.js} +2 -2
- package/dist/chunks/GraphTrackSettingsDialog-ARWCWJW4.js +4 -0
- package/dist/chunks/GraphTrackSettingsDialog-ARWCWJW4.js.map +7 -0
- package/dist/chunks/{HighlightColorDialog-QAYWLO5I.js → HighlightColorDialog-PD3KMF2U.js} +2 -2
- package/dist/chunks/LinearGraphDisplay-QNS4EAL3.js +2 -0
- package/dist/chunks/{LinearGraphDisplay-ZC4BQ27J.js.map → LinearGraphDisplay-QNS4EAL3.js.map} +2 -2
- package/dist/chunks/{MinigraphBubbleAdapter-3QM4XV47.js → MinigraphBubbleAdapter-L6JSQIOL.js} +2 -2
- package/dist/chunks/RgfaTabixAdapter-5HCAJ7QI.js +2 -0
- package/dist/chunks/RgfaTabixAdapter-5HCAJ7QI.js.map +7 -0
- package/dist/chunks/{WalkFormsAdapter-FT2SW4H6.js → WalkFormsAdapter-NX36GP35.js} +2 -2
- package/dist/chunks/WalkTabixSyntenyAdapter-JFNFX3JP.js +2 -0
- package/dist/chunks/{WalkTabixSyntenyAdapter-3WRVNDLB.js.map → WalkTabixSyntenyAdapter-JFNFX3JP.js.map} +2 -2
- package/dist/chunks/{chunk-KIYRMZWP.js → chunk-2CPD5NX2.js} +2 -2
- package/dist/chunks/{chunk-KIYRMZWP.js.map → chunk-2CPD5NX2.js.map} +1 -1
- package/dist/chunks/chunk-3AC33AXB.js +2 -0
- package/dist/chunks/{chunk-HF4H6LQI.js → chunk-4PHM6JNU.js} +2 -2
- package/dist/chunks/chunk-5LFPNQWH.js +2 -0
- package/dist/chunks/{chunk-6VNU7SD4.js.map → chunk-5LFPNQWH.js.map} +4 -4
- package/dist/chunks/{chunk-6WUWQX2P.js → chunk-CNEUOM7X.js} +2 -2
- package/dist/chunks/{chunk-6WUWQX2P.js.map → chunk-CNEUOM7X.js.map} +1 -1
- package/dist/chunks/{chunk-P3FBI5ML.js → chunk-DOC7QIE2.js} +1 -1
- package/dist/chunks/{chunk-P3FBI5ML.js.map → chunk-DOC7QIE2.js.map} +1 -1
- package/dist/chunks/{chunk-G53BI7VK.js → chunk-G4OPM6G7.js} +2 -2
- package/dist/chunks/{chunk-G53BI7VK.js.map → chunk-G4OPM6G7.js.map} +1 -1
- package/dist/chunks/chunk-GRLNDNX4.js +2 -0
- package/dist/chunks/{chunk-S333HOG5.js.map → chunk-GRLNDNX4.js.map} +4 -4
- package/dist/chunks/{chunk-7RJMG6I2.js → chunk-JYYF3U2H.js} +1 -1
- package/dist/chunks/{chunk-7RJMG6I2.js.map → chunk-JYYF3U2H.js.map} +1 -1
- package/dist/chunks/chunk-KDXVLEHS.js +3 -0
- package/dist/chunks/chunk-KDXVLEHS.js.map +7 -0
- package/dist/chunks/chunk-LGV6BTOY.js +2 -0
- package/dist/chunks/{chunk-HCDKP3IP.js.map → chunk-LGV6BTOY.js.map} +4 -4
- package/dist/chunks/{chunk-2KVE7YAS.js → chunk-LJ7O3736.js} +1 -1
- package/dist/chunks/{chunk-2KVE7YAS.js.map → chunk-LJ7O3736.js.map} +1 -1
- package/dist/chunks/{chunk-OJWBLPPH.js → chunk-OQHN2GWX.js} +2 -2
- package/dist/chunks/chunk-PTDOYJFH.js +4 -0
- package/dist/chunks/chunk-PTDOYJFH.js.map +7 -0
- package/dist/chunks/chunk-SGP63UMC.js +2 -0
- package/dist/chunks/chunk-SGP63UMC.js.map +7 -0
- package/dist/chunks/{chunk-XXBE4IFZ.js → chunk-SI5VLKVN.js} +2 -2
- package/dist/chunks/{chunk-XXBE4IFZ.js.map → chunk-SI5VLKVN.js.map} +1 -1
- package/dist/chunks/{chunk-6SWXMRAV.js → chunk-TWPUVVYH.js} +1 -1
- package/dist/chunks/{chunk-6SWXMRAV.js.map → chunk-TWPUVVYH.js.map} +1 -1
- package/dist/chunks/{chunk-FTXDTU27.js → chunk-XF6GU4GK.js} +3 -3
- package/dist/chunks/{chunk-FTXDTU27.js.map → chunk-XF6GU4GK.js.map} +2 -2
- package/dist/chunks/{chunk-MUONTMYZ.js → chunk-XSDGE4RF.js} +2 -2
- package/dist/chunks/{chunk-LE7FZM6Y.js → chunk-ZFJGNGIB.js} +2 -2
- package/dist/chunks/{chunk-LE7FZM6Y.js.map → chunk-ZFJGNGIB.js.map} +1 -1
- package/dist/chunks/chunk-ZI7RFIA2.js +4 -0
- package/dist/chunks/chunk-ZI7RFIA2.js.map +7 -0
- package/dist/chunks/{renderSvg-ZIUOPWJQ.js → renderSvg-NIWFCY3U.js} +2 -2
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +4 -3
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
- package/package.json +2 -2
- package/src/GraphComputeLayout.test.ts +24 -0
- package/src/GraphComputeLayout.ts +4 -2
- package/src/GraphGenomeView/components/GraphSettingsDialog.tsx +16 -0
- package/src/GraphGenomeView/components/WalkRowsOverlay.tsx +83 -0
- package/src/GraphGenomeView/components/usePaneGestures.test.tsx +82 -5
- package/src/GraphGenomeView/components/usePaneGestures.ts +10 -4
- package/src/GraphGenomeView/graphLayers.ts +14 -1
- package/src/GraphGenomeView/legendSections.ts +32 -0
- package/src/GraphGenomeView/model.test.ts +307 -7
- package/src/GraphGenomeView/model.ts +3 -10
- package/src/GraphGenomeView/nodeColor.test.ts +27 -1
- package/src/GraphGenomeView/nodeColor.ts +38 -29
- package/src/GraphGenomeView/nodeSize.test.ts +21 -5
- package/src/GraphGenomeView/nodeSize.ts +24 -8
- package/src/GraphGenomeView/pane/fitViews.ts +5 -1
- package/src/GraphGenomeView/pane/graphViews.ts +102 -20
- package/src/GraphGenomeView/pane/launchActions.ts +55 -11
- package/src/GraphGenomeView/pane/launchViews.ts +46 -0
- package/src/GraphGenomeView/pane/loadActions.ts +85 -25
- package/src/GraphGenomeView/pane/morph.test.ts +25 -1
- package/src/GraphGenomeView/pane/morph.ts +3 -1
- package/src/GraphGenomeView/pane/paneBase.ts +5 -0
- package/src/GraphGenomeView/pane/renderingActions.ts +61 -2
- package/src/GraphGenomeView/pane/settingActions.ts +65 -5
- package/src/GraphGenomeView/pane/walkRowViews.ts +17 -6
- package/src/GraphGenomeView/retiredNodeColor.test.ts +46 -0
- package/src/GraphGenomeView/retiredNodeColor.ts +76 -0
- package/src/GraphGenomeView/tubeMapPanels.ts +2 -1
- package/src/GraphGenomeView/viewModel.ts +16 -9
- package/src/GraphGenomeView/walkRowBlocks.ts +50 -0
- package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +97 -10
- package/src/LinearGraphDisplay/components/GraphStatusChrome.test.tsx +18 -0
- package/src/LinearGraphDisplay/components/GraphStatusChrome.tsx +4 -2
- package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.test.tsx +41 -1
- package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.tsx +33 -19
- package/src/LinearGraphDisplay/configSchema.ts +18 -10
- package/src/LinearGraphDisplay/model.ts +4 -2
- package/src/RgfaTabixAdapter/RgfaTabixAdapter.ts +2 -9
- package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +3 -6
- package/src/hoverSync/graphViewHighlights.test.ts +53 -0
- package/src/hoverSync/graphViewHighlights.ts +25 -0
- package/src/hoverSync/lgvHover.test.ts +100 -1
- package/src/hoverSync/lgvHover.ts +77 -6
- package/src/launchFromGraph/contributors.test.ts +52 -0
- package/src/launchFromGraph/contributors.ts +105 -1
- package/src/launchFromGraph/launchFromGraph.ts +2 -2
- package/src/panSNTabix.test.ts +28 -0
- package/src/panSNTabix.ts +26 -8
- package/src/version.ts +1 -1
- package/dist/chunks/GraphTrackSettingsDialog-EVIIGC5G.js +0 -6
- package/dist/chunks/GraphTrackSettingsDialog-EVIIGC5G.js.map +0 -7
- package/dist/chunks/LinearGraphDisplay-ZC4BQ27J.js +0 -2
- package/dist/chunks/RgfaTabixAdapter-XSOIEWBK.js +0 -2
- package/dist/chunks/RgfaTabixAdapter-XSOIEWBK.js.map +0 -7
- package/dist/chunks/WalkTabixSyntenyAdapter-3WRVNDLB.js +0 -2
- package/dist/chunks/chunk-6VNU7SD4.js +0 -2
- package/dist/chunks/chunk-GRTN25B6.js +0 -3
- package/dist/chunks/chunk-GRTN25B6.js.map +0 -7
- package/dist/chunks/chunk-HCDKP3IP.js +0 -2
- package/dist/chunks/chunk-LE43I4PT.js +0 -4
- package/dist/chunks/chunk-LE43I4PT.js.map +0 -7
- package/dist/chunks/chunk-QV7SSOMJ.js +0 -2
- package/dist/chunks/chunk-S333HOG5.js +0 -2
- package/dist/chunks/chunk-VZQ6VOUP.js +0 -2
- package/dist/chunks/chunk-VZQ6VOUP.js.map +0 -7
- package/dist/chunks/chunk-WLRRI35A.js +0 -3
- package/dist/chunks/chunk-WLRRI35A.js.map +0 -7
- package/src/launchFromGraph/launchTracks.test.ts +0 -95
- package/src/launchFromGraph/launchTracks.ts +0 -49
- /package/dist/chunks/{GraphGenomeView-SEAPDRXS.js.map → GraphGenomeView-GQW4YC23.js.map} +0 -0
- /package/dist/chunks/{HighlightColorDialog-QAYWLO5I.js.map → HighlightColorDialog-PD3KMF2U.js.map} +0 -0
- /package/dist/chunks/{MinigraphBubbleAdapter-3QM4XV47.js.map → MinigraphBubbleAdapter-L6JSQIOL.js.map} +0 -0
- /package/dist/chunks/{WalkFormsAdapter-FT2SW4H6.js.map → WalkFormsAdapter-NX36GP35.js.map} +0 -0
- /package/dist/chunks/{chunk-QV7SSOMJ.js.map → chunk-3AC33AXB.js.map} +0 -0
- /package/dist/chunks/{chunk-HF4H6LQI.js.map → chunk-4PHM6JNU.js.map} +0 -0
- /package/dist/chunks/{chunk-OJWBLPPH.js.map → chunk-OQHN2GWX.js.map} +0 -0
- /package/dist/chunks/{chunk-MUONTMYZ.js.map → chunk-XSDGE4RF.js.map} +0 -0
- /package/dist/chunks/{renderSvg-ZIUOPWJQ.js.map → renderSvg-NIWFCY3U.js.map} +0 -0
|
@@ -16,7 +16,12 @@ import {
|
|
|
16
16
|
} from '@jbrowse/core/pluggableElementTypes/models'
|
|
17
17
|
import { LAUNCH_LABEL } from '@jbrowse/core/ui'
|
|
18
18
|
import SimpleFeature from '@jbrowse/core/util/simpleFeature'
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
applyPatch,
|
|
21
|
+
getSnapshot,
|
|
22
|
+
getType,
|
|
23
|
+
types,
|
|
24
|
+
} from '@jbrowse/mobx-state-tree'
|
|
20
25
|
import { linearGenomeViewStateModelFactory } from '@jbrowse/plugin-linear-genome-view'
|
|
21
26
|
import RocketLaunchIcon from '@mui/icons-material/RocketLaunch'
|
|
22
27
|
import { reaction } from 'mobx'
|
|
@@ -868,8 +873,14 @@ test('walk rows fit their own bars in the track, since a walk can be longer than
|
|
|
868
873
|
expect(pane.viewportOwner).toBe('fit')
|
|
869
874
|
})
|
|
870
875
|
|
|
871
|
-
test('walk rows cut every snarl a walk takes past the window, so a walk comes whole', async () => {
|
|
872
|
-
const {
|
|
876
|
+
test('walk rows cut every snarl a GBZ walk takes past the window, so a walk comes whole', async () => {
|
|
877
|
+
const { view, cuts } = createEnvironment({ paths: true })
|
|
878
|
+
view.zoomTo(60_000 / WIDTH_PX)
|
|
879
|
+
view.scrollTo(1_000_000 / view.bpPerPx)
|
|
880
|
+
view.showTrack('walks')
|
|
881
|
+
const pane = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
|
|
882
|
+
pane.startRenderingBackend(fakeRenderer())
|
|
883
|
+
await wait(SETTLE_MS)
|
|
873
884
|
await pane.switchLayout('force')
|
|
874
885
|
await wait(SETTLE_MS)
|
|
875
886
|
const before = cuts.length
|
|
@@ -884,6 +895,18 @@ test('walk rows cut every snarl a walk takes past the window, so a walk comes wh
|
|
|
884
895
|
expect(cuts.at(-1)!.snarls).toBeUndefined()
|
|
885
896
|
})
|
|
886
897
|
|
|
898
|
+
// A walk file cuts the same whatever the snarl option says
|
|
899
|
+
test('walk rows on an rGFA track cut for the step budget, not for snarls', async () => {
|
|
900
|
+
const { pane, cuts } = await shownGraph({ paths: true })
|
|
901
|
+
await pane.switchLayout('force')
|
|
902
|
+
await wait(SETTLE_MS)
|
|
903
|
+
const before = cuts.length
|
|
904
|
+
await pane.switchLayout('walkrows')
|
|
905
|
+
await wait(SETTLE_MS)
|
|
906
|
+
expect(cuts).toHaveLength(before + 1)
|
|
907
|
+
expect(cuts.at(-1)!.snarls).toBeUndefined()
|
|
908
|
+
})
|
|
909
|
+
|
|
887
910
|
test('a launch in the force layout cuts the window alone', async () => {
|
|
888
911
|
const { view, cuts } = createEnvironment()
|
|
889
912
|
view.zoomTo(60_000 / WIDTH_PX)
|
|
@@ -967,7 +990,7 @@ test("a launch names the pane's props without its type, and opens in that layout
|
|
|
967
990
|
{
|
|
968
991
|
type: 'LinearGraphDisplay',
|
|
969
992
|
layoutMode: 'force',
|
|
970
|
-
color: 'uniform',
|
|
993
|
+
encoding: { color: { value: 'uniform' } },
|
|
971
994
|
},
|
|
972
995
|
)
|
|
973
996
|
const display = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
|
|
@@ -979,6 +1002,51 @@ test("a launch names the pane's props without its type, and opens in that layout
|
|
|
979
1002
|
expect(display.hostPlacesX).toBe(false)
|
|
980
1003
|
})
|
|
981
1004
|
|
|
1005
|
+
test.each([
|
|
1006
|
+
['uniform', 'uniform'],
|
|
1007
|
+
[{ field: 'id', scale: 'categorical' }, 'random'],
|
|
1008
|
+
[{ field: 'id', scheme: 'rainbow' }, 'rainbow'],
|
|
1009
|
+
[{ field: 'depth' }, 'depth'],
|
|
1010
|
+
])('a flat color %j in a saved config is read as %s', async (color, scheme) => {
|
|
1011
|
+
const { view } = createEnvironment()
|
|
1012
|
+
view.showTrack('graph', {}, { type: 'LinearGraphDisplay', color })
|
|
1013
|
+
const display = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
|
|
1014
|
+
expect(display.chosenColorScheme).toBe(scheme)
|
|
1015
|
+
expect(getSnapshot(display.configuration)).not.toHaveProperty('color')
|
|
1016
|
+
})
|
|
1017
|
+
|
|
1018
|
+
test('a flat color lifts to encoding.color', async () => {
|
|
1019
|
+
const { view } = createEnvironment()
|
|
1020
|
+
view.showTrack(
|
|
1021
|
+
'graph',
|
|
1022
|
+
{},
|
|
1023
|
+
{
|
|
1024
|
+
type: 'LinearGraphDisplay',
|
|
1025
|
+
color: { field: 'position', domainMin: 5, domainMax: 50 },
|
|
1026
|
+
},
|
|
1027
|
+
)
|
|
1028
|
+
const lifted = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
|
|
1029
|
+
expect(readConfObject(lifted.configuration, 'encoding')).toEqual({
|
|
1030
|
+
color: { field: 'position', scale: { domainMin: 5, domainMax: 50 } },
|
|
1031
|
+
})
|
|
1032
|
+
expect(lifted.statedColorDomain).toEqual({ start: 5, end: 50 })
|
|
1033
|
+
})
|
|
1034
|
+
|
|
1035
|
+
test('an encoding beside a flat color wins', () => {
|
|
1036
|
+
const { view } = createEnvironment()
|
|
1037
|
+
view.showTrack(
|
|
1038
|
+
'graph',
|
|
1039
|
+
{},
|
|
1040
|
+
{
|
|
1041
|
+
type: 'LinearGraphDisplay',
|
|
1042
|
+
color: { field: 'depth', domainMin: 1 },
|
|
1043
|
+
encoding: { color: { field: 'length' } },
|
|
1044
|
+
},
|
|
1045
|
+
)
|
|
1046
|
+
const display = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
|
|
1047
|
+
expect(display.chosenColorScheme).toBe('node-length')
|
|
1048
|
+
})
|
|
1049
|
+
|
|
982
1050
|
test.each([
|
|
983
1051
|
['force', false],
|
|
984
1052
|
['auto', true],
|
|
@@ -1000,7 +1068,14 @@ test('a launch that states one choice takes the rest from the config', async ()
|
|
|
1000
1068
|
const { view, cuts } = createEnvironment()
|
|
1001
1069
|
view.zoomTo(60_000 / WIDTH_PX)
|
|
1002
1070
|
view.scrollTo(1_000_000 / view.bpPerPx)
|
|
1003
|
-
view.showTrack(
|
|
1071
|
+
view.showTrack(
|
|
1072
|
+
'graph',
|
|
1073
|
+
{},
|
|
1074
|
+
{
|
|
1075
|
+
type: 'LinearGraphDisplay',
|
|
1076
|
+
encoding: { color: { value: 'uniform' } },
|
|
1077
|
+
},
|
|
1078
|
+
)
|
|
1004
1079
|
const display = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
|
|
1005
1080
|
display.startRenderingBackend(fakeRenderer())
|
|
1006
1081
|
await wait(SETTLE_MS)
|
|
@@ -1018,13 +1093,15 @@ test('each grammar setter writes the track config, and the pane reads it back',
|
|
|
1018
1093
|
display.setLayoutMode('ordered')
|
|
1019
1094
|
display.setColorScheme('depth')
|
|
1020
1095
|
display.setNodeWidth('uniform')
|
|
1096
|
+
display.setLinkWidth('walks')
|
|
1021
1097
|
display.setLayer('bubbles', true)
|
|
1022
1098
|
display.setFacet('walk')
|
|
1023
1099
|
display.setWalkRowSamples(['HG1'])
|
|
1100
|
+
display.setWalkRowOrder('shared')
|
|
1024
1101
|
display.setHover('off')
|
|
1025
1102
|
const slots = [
|
|
1026
1103
|
'layoutMode',
|
|
1027
|
-
'
|
|
1104
|
+
'encoding',
|
|
1028
1105
|
'size',
|
|
1029
1106
|
'layers',
|
|
1030
1107
|
'facet',
|
|
@@ -1039,22 +1116,32 @@ test('each grammar setter writes the track config, and the pane reads it back',
|
|
|
1039
1116
|
),
|
|
1040
1117
|
).toEqual({
|
|
1041
1118
|
layoutMode: 'ordered',
|
|
1042
|
-
color: { field: 'depth' },
|
|
1043
|
-
size: 6,
|
|
1119
|
+
encoding: { color: { field: 'depth' } },
|
|
1120
|
+
size: { value: 6, links: 'walks' },
|
|
1044
1121
|
layers: { bubbles: true },
|
|
1045
1122
|
facet: { field: 'walk', domain: [] },
|
|
1046
|
-
rows: { kept: ['HG1'] },
|
|
1123
|
+
rows: { kept: ['HG1'], order: 'shared' },
|
|
1047
1124
|
hover: 'off',
|
|
1048
1125
|
})
|
|
1049
1126
|
expect([
|
|
1050
1127
|
display.chosenLayoutMode,
|
|
1051
1128
|
display.chosenColorScheme,
|
|
1052
1129
|
display.nodeWidth,
|
|
1130
|
+
display.linkWidth,
|
|
1053
1131
|
display.showBubbles,
|
|
1054
1132
|
display.facetSetting.field,
|
|
1055
1133
|
display.walkRowSamples,
|
|
1056
1134
|
display.chosenHover,
|
|
1057
|
-
]).toEqual([
|
|
1135
|
+
]).toEqual([
|
|
1136
|
+
'ordered',
|
|
1137
|
+
'depth',
|
|
1138
|
+
'uniform',
|
|
1139
|
+
'walks',
|
|
1140
|
+
true,
|
|
1141
|
+
'walk',
|
|
1142
|
+
['HG1'],
|
|
1143
|
+
'off',
|
|
1144
|
+
])
|
|
1058
1145
|
})
|
|
1059
1146
|
|
|
1060
1147
|
// Reset track settings, undo and the config editor write the slot without
|
|
@@ -39,6 +39,24 @@ test('a drawn graph sits in the chrome with its readiness attributes', () => {
|
|
|
39
39
|
expect(screen.getByTestId('graph-canvas')).toBeTruthy()
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
+
// GraphCanvas shows it, with the retry that rebuilds the backend
|
|
43
|
+
test('a render error alone is left to the canvas', () => {
|
|
44
|
+
render(
|
|
45
|
+
<GraphStatusChrome
|
|
46
|
+
model={model({ renderError: new Error('context lost') })}
|
|
47
|
+
phase="error"
|
|
48
|
+
drawn={false}
|
|
49
|
+
testid="linear-graph-display"
|
|
50
|
+
>
|
|
51
|
+
<canvas />
|
|
52
|
+
</GraphStatusChrome>,
|
|
53
|
+
)
|
|
54
|
+
expect(screen.queryByText(/context lost/)).toBeNull()
|
|
55
|
+
expect(screen.getByTestId('linear-graph-display').dataset.displayPhase).toBe(
|
|
56
|
+
'error',
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
42
60
|
test('a refused cut shows its notice over the graph', () => {
|
|
43
61
|
const error = Object.assign(
|
|
44
62
|
new Error('Zoom in to about 54Kbp to see the graph'),
|
|
@@ -31,7 +31,7 @@ const FORCE_LOAD_ADVICE =
|
|
|
31
31
|
// read `model` and `visible`; later hosts read values and callbacks. Each
|
|
32
32
|
// overlay gets both, so one build runs on either.
|
|
33
33
|
function overlayProps(model: LinearGraphDisplayModel) {
|
|
34
|
-
const error = model
|
|
34
|
+
const { error } = model
|
|
35
35
|
const regionTooLarge =
|
|
36
36
|
error instanceof Error &&
|
|
37
37
|
(error as { regionTooLarge?: unknown }).regionTooLarge === true
|
|
@@ -128,7 +128,9 @@ export default observer(function GraphStatusChrome({
|
|
|
128
128
|
{children}
|
|
129
129
|
</BottomRightCornerContext>
|
|
130
130
|
<TrackOverlayPortal>
|
|
131
|
-
{
|
|
131
|
+
{/* a backend's render error is the canvas's banner, whose retry
|
|
132
|
+
rebuilds the backend */}
|
|
133
|
+
{phase === 'error' && model.error !== undefined ? (
|
|
132
134
|
<ErrorBar model={model} visible {...props.error} />
|
|
133
135
|
) : null}
|
|
134
136
|
<Loading
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from '@testing-library/react'
|
|
1
|
+
import { act, fireEvent, render, screen } from '@testing-library/react'
|
|
2
|
+
import { observable, runInAction } from 'mobx'
|
|
2
3
|
|
|
3
4
|
import SubgraphHaplotypesField from './SubgraphHaplotypesField'
|
|
4
5
|
|
|
@@ -58,3 +59,42 @@ test('without names the field takes typed ones', () => {
|
|
|
58
59
|
fireEvent.keyDown(field, { key: 'Enter' })
|
|
59
60
|
expect(m.setSubgraphHaplotypes).toHaveBeenCalledWith(['HG002', 'NA19240#1'])
|
|
60
61
|
})
|
|
62
|
+
|
|
63
|
+
// A model whose set re-renders the field, as the display's does
|
|
64
|
+
function liveModel(haplotypeNames?: string[]) {
|
|
65
|
+
const m = observable({
|
|
66
|
+
sourceAdapter: { type: 'RgfaTabixAdapter', walksUri: 'chr22' },
|
|
67
|
+
cutHaplotypes: ['HG002'],
|
|
68
|
+
haplotypeNames,
|
|
69
|
+
setSubgraphHaplotypes(haplotypes: string[]) {
|
|
70
|
+
m.cutHaplotypes = haplotypes
|
|
71
|
+
},
|
|
72
|
+
recut: vi.fn(),
|
|
73
|
+
})
|
|
74
|
+
return m as unknown as LinearGraphCutModel & { cutHaplotypes: string[] }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
test('applying from the field keeps its focus', () => {
|
|
78
|
+
const m = liveModel()
|
|
79
|
+
render(<SubgraphHaplotypesField model={m} />)
|
|
80
|
+
const field = screen.getByTestId<HTMLInputElement>('graph-haplotypes-field')
|
|
81
|
+
field.focus()
|
|
82
|
+
fireEvent.change(field, { target: { value: 'HG002, NA19240#1' } })
|
|
83
|
+
fireEvent.keyDown(field, { key: 'Enter' })
|
|
84
|
+
expect(m.cutHaplotypes).toEqual(['HG002', 'NA19240#1'])
|
|
85
|
+
expect(screen.getByTestId('graph-haplotypes-field')).toBe(field)
|
|
86
|
+
expect(document.activeElement).toBe(field)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('a set changed elsewhere resets the draft', () => {
|
|
90
|
+
const m = liveModel()
|
|
91
|
+
render(<SubgraphHaplotypesField model={m} />)
|
|
92
|
+
act(() => {
|
|
93
|
+
runInAction(() => {
|
|
94
|
+
m.cutHaplotypes = ['HG00097#1']
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
expect(
|
|
98
|
+
screen.getByTestId<HTMLInputElement>('graph-haplotypes-field').value,
|
|
99
|
+
).toBe('HG00097#1')
|
|
100
|
+
})
|
|
@@ -18,20 +18,39 @@ function parseHaplotypeList(text: string) {
|
|
|
18
18
|
return text.split(/[\s,]+/).filter(name => name !== '')
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const sameList = (a: readonly string[], b: readonly string[]) =>
|
|
22
|
+
a.join('\n') === b.join('\n')
|
|
23
|
+
|
|
24
|
+
// The cut's set, and the last one this field saw: an outside change (a
|
|
25
|
+
// restored session, a launch) resets the draft, while the field's own apply
|
|
26
|
+
// records what it set first, so it keeps its focus and draft
|
|
27
|
+
function useCutHaplotypes(model: LinearGraphCutModel, reset: () => void) {
|
|
28
|
+
const current = model.cutHaplotypes ?? []
|
|
29
|
+
const [seen, setSeen] = useState(current)
|
|
30
|
+
if (!sameList(current, seen)) {
|
|
31
|
+
setSeen(current)
|
|
32
|
+
reset()
|
|
33
|
+
}
|
|
34
|
+
return (haplotypes: string[]) => {
|
|
35
|
+
if (!sameList(haplotypes, current)) {
|
|
36
|
+
setSeen(haplotypes)
|
|
37
|
+
model.setSubgraphHaplotypes(haplotypes)
|
|
38
|
+
model.recut()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
21
43
|
const HaplotypeListField = observer(function HaplotypeListField({
|
|
22
44
|
model,
|
|
23
45
|
}: {
|
|
24
46
|
model: LinearGraphCutModel
|
|
25
47
|
}) {
|
|
26
48
|
const [draft, setDraft] = useState(model.cutHaplotypes?.join(', ') ?? '')
|
|
49
|
+
const setCut = useCutHaplotypes(model, () => {
|
|
50
|
+
setDraft(model.cutHaplotypes?.join(', ') ?? '')
|
|
51
|
+
})
|
|
27
52
|
const apply = () => {
|
|
28
|
-
|
|
29
|
-
const unchanged =
|
|
30
|
-
parsed.join('\n') === (model.cutHaplotypes ?? []).join('\n')
|
|
31
|
-
if (!unchanged) {
|
|
32
|
-
model.setSubgraphHaplotypes(parsed)
|
|
33
|
-
model.recut()
|
|
34
|
-
}
|
|
53
|
+
setCut(parseHaplotypeList(draft))
|
|
35
54
|
}
|
|
36
55
|
return (
|
|
37
56
|
<TextField
|
|
@@ -72,12 +91,10 @@ const HaplotypePickField = observer(function HaplotypePickField({
|
|
|
72
91
|
const draftRef = useRef(draft)
|
|
73
92
|
const typed = useRef('')
|
|
74
93
|
const [open, setOpen] = useState(false)
|
|
75
|
-
const apply = (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
}
|
|
94
|
+
const apply = useCutHaplotypes(model, () => {
|
|
95
|
+
draftRef.current = model.cutHaplotypes ?? []
|
|
96
|
+
setDraft(draftRef.current)
|
|
97
|
+
})
|
|
81
98
|
return (
|
|
82
99
|
<Autocomplete
|
|
83
100
|
multiple
|
|
@@ -126,9 +143,7 @@ const HaplotypePickField = observer(function HaplotypePickField({
|
|
|
126
143
|
)
|
|
127
144
|
})
|
|
128
145
|
|
|
129
|
-
// Only a GBZ cut and a walk-indexed rGFA cut read the set
|
|
130
|
-
// keyed on it so an outside change (a restored session, a launch) resets the
|
|
131
|
-
// draft rather than fighting it.
|
|
146
|
+
// Only a GBZ cut and a walk-indexed rGFA cut read the set
|
|
132
147
|
const SubgraphHaplotypesField = observer(function SubgraphHaplotypesField({
|
|
133
148
|
model,
|
|
134
149
|
}: {
|
|
@@ -136,14 +151,13 @@ const SubgraphHaplotypesField = observer(function SubgraphHaplotypesField({
|
|
|
136
151
|
}) {
|
|
137
152
|
const { classes } = useStyles()
|
|
138
153
|
const names = model.haplotypeNames
|
|
139
|
-
const key = model.cutHaplotypes?.join(',') ?? ''
|
|
140
154
|
const adapter = model.sourceAdapter
|
|
141
155
|
return adapter && cutsByHaplotype(adapter) ? (
|
|
142
156
|
<div className={classes.section}>
|
|
143
157
|
{names ? (
|
|
144
|
-
<HaplotypePickField
|
|
158
|
+
<HaplotypePickField model={model} names={names} />
|
|
145
159
|
) : (
|
|
146
|
-
<HaplotypeListField
|
|
160
|
+
<HaplotypeListField model={model} />
|
|
147
161
|
)}
|
|
148
162
|
<Typography variant="caption" component="div" color="text.secondary">
|
|
149
163
|
{names
|
|
@@ -4,6 +4,7 @@ import { trackHeightConfigSchemaFields } from '@jbrowse/display-kit/trackHeightC
|
|
|
4
4
|
import { types } from '@jbrowse/mobx-state-tree'
|
|
5
5
|
|
|
6
6
|
import { HOVER_HIGHLIGHT_VALUES } from '../GraphGenomeView/hoverHighlight'
|
|
7
|
+
import { retiredGraphColor } from '../GraphGenomeView/retiredNodeColor'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* #config LinearGraphDisplay
|
|
@@ -27,15 +28,17 @@ export function configSchemaFactory() {
|
|
|
27
28
|
},
|
|
28
29
|
/**
|
|
29
30
|
* #slot
|
|
30
|
-
* the
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* the channels the nodes are painted through, as core's `encoding`.
|
|
32
|
+
* `color` is `{ value: 'grey' }` or `{ value: 'uniform' }`, or the
|
|
33
|
+
* `field` nodes are colored by, `depth`, `length`, `rank`, `position` or
|
|
34
|
+
* `id` (`scale: { type: 'ordinal' }` or `scale: { scheme: 'rainbow' }`).
|
|
35
|
+
* `scale.domainMin` and `scale.domainMax` span the position ramp. `{}`
|
|
36
|
+
* colors an anchored graph by position.
|
|
34
37
|
* ```js
|
|
35
|
-
* { color: { field: 'depth' } }
|
|
38
|
+
* { encoding: { color: { field: 'depth' } } }
|
|
36
39
|
* ```
|
|
37
40
|
*/
|
|
38
|
-
|
|
41
|
+
encoding: {
|
|
39
42
|
type: 'frozen',
|
|
40
43
|
defaultValue: {},
|
|
41
44
|
plot: true,
|
|
@@ -44,7 +47,8 @@ export function configSchemaFactory() {
|
|
|
44
47
|
* #slot
|
|
45
48
|
* the node thickness: a px number draws every node that thick;
|
|
46
49
|
* `{ field: 'depth', value }` thickens a node the more paths carry it,
|
|
47
|
-
* around `value` px
|
|
50
|
+
* around `value` px; `links: 'walks'` thickens a link the more walks
|
|
51
|
+
* take it
|
|
48
52
|
*/
|
|
49
53
|
size: {
|
|
50
54
|
type: 'frozen',
|
|
@@ -54,8 +58,8 @@ export function configSchemaFactory() {
|
|
|
54
58
|
/**
|
|
55
59
|
* #slot
|
|
56
60
|
* what the drawing adds over its nodes and edges, stated as changes to
|
|
57
|
-
* the defaults: `paths`, `bubbles` and `
|
|
58
|
-
* `genes` and `referenceStrip` on
|
|
61
|
+
* the defaults: `paths`, `bubbles`, `walkStrip` and `walkRibbons` are
|
|
62
|
+
* off, `deletions`, `genes` and `referenceStrip` on
|
|
59
63
|
* ```js
|
|
60
64
|
* { layers: { bubbles: true, genes: false } }
|
|
61
65
|
* ```
|
|
@@ -101,7 +105,11 @@ export function configSchemaFactory() {
|
|
|
101
105
|
height: 'the height of the track the graph is drawn in',
|
|
102
106
|
}),
|
|
103
107
|
},
|
|
104
|
-
{
|
|
108
|
+
{
|
|
109
|
+
explicitlyTyped: true,
|
|
110
|
+
explicitIdentifier: 'displayId',
|
|
111
|
+
retired: retiredGraphColor,
|
|
112
|
+
},
|
|
105
113
|
)
|
|
106
114
|
}
|
|
107
115
|
|
|
@@ -129,7 +129,7 @@ export function stateModelFactory(configSchema: LinearGraphDisplayConfigModel) {
|
|
|
129
129
|
get grammar(): GraphGrammar {
|
|
130
130
|
return {
|
|
131
131
|
layoutMode: getConf(self, 'layoutMode'),
|
|
132
|
-
|
|
132
|
+
encoding: getConf(self, 'encoding'),
|
|
133
133
|
size: getConf(self, 'size'),
|
|
134
134
|
layers: getConf(self, 'layers') as GraphLayers,
|
|
135
135
|
facet: getConf(self, 'facet'),
|
|
@@ -590,6 +590,7 @@ export function stateModelFactory(configSchema: LinearGraphDisplayConfigModel) {
|
|
|
590
590
|
}
|
|
591
591
|
const start = Math.floor(seen.start)
|
|
592
592
|
const end = Math.ceil(seen.end)
|
|
593
|
+
const { encoding, ...grammar } = self.grammar
|
|
593
594
|
return {
|
|
594
595
|
...paneSettings(self),
|
|
595
596
|
loadedTrackId: trackId,
|
|
@@ -601,7 +602,8 @@ export function stateModelFactory(configSchema: LinearGraphDisplayConfigModel) {
|
|
|
601
602
|
},
|
|
602
603
|
subgraphHaplotypes: self.cutHaplotypes,
|
|
603
604
|
maxRegionBp: Math.max(self.maxRegionBp, end - start),
|
|
604
|
-
...
|
|
605
|
+
...grammar,
|
|
606
|
+
color: encoding?.color,
|
|
605
607
|
layoutMode: self.chosenLayoutMode,
|
|
606
608
|
connectedViewId: self.host?.id,
|
|
607
609
|
} satisfies GraphViewSpec
|
|
@@ -4,11 +4,7 @@ import { ObservableCreate } from '@jbrowse/core/util/rxjs'
|
|
|
4
4
|
import { parseSegmentRow, rgfaCut, segmentSamples } from '@jbrowse/graph-walks'
|
|
5
5
|
|
|
6
6
|
import { locationIsSet } from '../locationName.ts'
|
|
7
|
-
import {
|
|
8
|
-
PanSNRefNames,
|
|
9
|
-
openTabixSlot,
|
|
10
|
-
unmappedRegionMessage,
|
|
11
|
-
} from '../panSNTabix.ts'
|
|
7
|
+
import { PanSNRefNames, openTabixSlot } from '../panSNTabix.ts'
|
|
12
8
|
import { getLines } from '../tabixRange.ts'
|
|
13
9
|
import { WalkIndex } from './walkReader.ts'
|
|
14
10
|
|
|
@@ -194,10 +190,7 @@ export default class RgfaTabixAdapter extends BaseFeatureDataAdapter<RgfaTabixAd
|
|
|
194
190
|
): Promise<string | WalkCut> {
|
|
195
191
|
const { hops, signal, tier } = opts
|
|
196
192
|
const index = this.index(tier)
|
|
197
|
-
const tabixRefName = await index.refNames.
|
|
198
|
-
if (tabixRefName === undefined) {
|
|
199
|
-
throw new Error(unmappedRegionMessage(region))
|
|
200
|
-
}
|
|
193
|
+
const tabixRefName = await index.refNames.resolveForCut(region, { signal })
|
|
201
194
|
if (index.walks) {
|
|
202
195
|
return index.walks.subgraph(tabixRefName, region, opts)
|
|
203
196
|
}
|
|
@@ -101,12 +101,9 @@ export default class WalkTabixSyntenyAdapter extends ComparativeAdapterBase<Walk
|
|
|
101
101
|
* asks for, and the nodes and links they visit
|
|
102
102
|
*/
|
|
103
103
|
async getSubgraph(region: Region, opts: SubgraphAdapterOptions = {}) {
|
|
104
|
-
const refName = await this.refNames.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
`${region.assemblyName} ${region.refName} is not in this graph's index; the graph is cut on ${this.anchor}, mapped to its PanSN prefix in assemblyNameToPanSN`,
|
|
108
|
-
)
|
|
109
|
-
}
|
|
104
|
+
const refName = await this.refNames.resolveForCut(region, {
|
|
105
|
+
signal: opts.signal,
|
|
106
|
+
})
|
|
110
107
|
return this.walks.subgraph(refName, region, opts)
|
|
111
108
|
}
|
|
112
109
|
|
|
@@ -200,3 +200,56 @@ describe("the drawing view has to be on the highlight's assembly", () => {
|
|
|
200
200
|
expect(graphViewHighlights(noAssembly, 'lgv1', ['Sakai'])).toHaveLength(1)
|
|
201
201
|
})
|
|
202
202
|
})
|
|
203
|
+
|
|
204
|
+
describe("a hovered node's span on a haplotype's own assembly", () => {
|
|
205
|
+
const onHap = {
|
|
206
|
+
refName: 'CM094351.1',
|
|
207
|
+
start: 1200,
|
|
208
|
+
end: 1500,
|
|
209
|
+
assemblyName: 'HG002.1',
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// the stack's rows and a view opened beside are not the graph's pair
|
|
213
|
+
test('draws in a view of that assembly, paired or not', () => {
|
|
214
|
+
const views = [
|
|
215
|
+
graphView({
|
|
216
|
+
connectedViewId: 'lgv-ref',
|
|
217
|
+
assemblyHoverHighlights: [onHap],
|
|
218
|
+
}),
|
|
219
|
+
linearView('lgv-ref'),
|
|
220
|
+
linearView('lgv-hap'),
|
|
221
|
+
]
|
|
222
|
+
expect(graphViewHighlights(views, 'lgv-hap', ['HG002.1'])).toEqual([
|
|
223
|
+
{ key: 'graph1-0', region: onHap },
|
|
224
|
+
])
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('draws nowhere else, nor where the view states no assemblies', () => {
|
|
228
|
+
const views = [graphView({ assemblyHoverHighlights: [onHap] })]
|
|
229
|
+
expect(graphViewHighlights(views, 'lgv1', ['hg38'])).toEqual([])
|
|
230
|
+
expect(graphViewHighlights(views, 'lgv1')).toEqual([])
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test("comes from a graph track's display too", () => {
|
|
234
|
+
const views = [
|
|
235
|
+
{
|
|
236
|
+
...linearView('lgv-ref'),
|
|
237
|
+
tracks: [
|
|
238
|
+
{
|
|
239
|
+
displays: [
|
|
240
|
+
{
|
|
241
|
+
id: 'display1',
|
|
242
|
+
type: 'LinearGraphDisplay',
|
|
243
|
+
assemblyHoverHighlights: [onHap],
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
linearView('lgv-hap'),
|
|
250
|
+
]
|
|
251
|
+
expect(graphViewHighlights(views, 'lgv-hap', ['HG002.1'])).toEqual([
|
|
252
|
+
{ key: 'display1-0', region: onHap },
|
|
253
|
+
])
|
|
254
|
+
})
|
|
255
|
+
})
|
|
@@ -124,6 +124,31 @@ export function graphViewHighlights(
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
+
// A hovered node's span on each haplotype's own assembly goes to any
|
|
128
|
+
// view of that assembly, paired or not: the assembly name says where it
|
|
129
|
+
// belongs, and those views are a synteny stack's rows or opened beside
|
|
130
|
+
for (const graph of isRecord(view)
|
|
131
|
+
? [
|
|
132
|
+
...trackGraphs(view),
|
|
133
|
+
...(view.type === 'GraphGenomeView' ? [view] : []),
|
|
134
|
+
]
|
|
135
|
+
: []) {
|
|
136
|
+
const spans = Array.isArray(graph.assemblyHoverHighlights)
|
|
137
|
+
? graph.assemblyHoverHighlights
|
|
138
|
+
: []
|
|
139
|
+
spans.forEach((value: unknown, i) => {
|
|
140
|
+
const region = readRegion(value)
|
|
141
|
+
if (
|
|
142
|
+
region?.assemblyName !== undefined &&
|
|
143
|
+
linearAssemblyNames?.includes(region.assemblyName)
|
|
144
|
+
) {
|
|
145
|
+
highlights.push({
|
|
146
|
+
key: `${typeof graph.id === 'string' ? graph.id : 'graph'}-${i}`,
|
|
147
|
+
region,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
}
|
|
127
152
|
if (
|
|
128
153
|
isRecord(view) &&
|
|
129
154
|
view.type === 'GraphGenomeView' &&
|