jbrowse-plugin-graphgenomeviewer 4.0.21 → 4.0.23
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/README.md +5 -2
- package/dist/chunks/{GraphGenomeView-CLUMXH4K.js → GraphGenomeView-QXKHSZQJ.js} +2 -2
- package/dist/chunks/{LinearGraphDisplay-4CMVCEM5.js → LinearGraphDisplay-GMMLJWQ6.js} +2 -2
- package/dist/chunks/chunk-75CO6QJZ.js +2 -0
- package/dist/chunks/chunk-75CO6QJZ.js.map +7 -0
- package/dist/chunks/chunk-NSXARD3R.js +3 -0
- package/dist/chunks/chunk-NSXARD3R.js.map +7 -0
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +2 -2
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +3 -3
- package/package.json +2 -2
- package/src/GraphGenomeView/components/BubbleHalos.tsx +2 -10
- package/src/GraphGenomeView/components/FacetPanels.tsx +159 -0
- package/src/GraphGenomeView/components/GenePins.tsx +12 -5
- package/src/GraphGenomeView/components/GraphCanvas.tsx +45 -318
- package/src/GraphGenomeView/components/GraphToolbar.tsx +6 -2
- package/src/GraphGenomeView/components/ReferenceStripOverlay.tsx +21 -16
- package/src/GraphGenomeView/components/TubeMapOverlay.tsx +2 -9
- package/src/GraphGenomeView/components/UnpopButton.tsx +1 -2
- package/src/GraphGenomeView/components/WalkKey.tsx +52 -0
- package/src/GraphGenomeView/components/WalkRowsOverlay.tsx +1 -9
- package/src/GraphGenomeView/components/WalkSelect.tsx +2 -2
- package/src/GraphGenomeView/components/legendStyles.ts +11 -0
- package/src/GraphGenomeView/components/usePaneGestures.ts +274 -0
- package/src/GraphGenomeView/model.test.ts +102 -10
- package/src/GraphGenomeView/model.ts +318 -119
- package/src/LinearGraphDisplay/model.ts +2 -0
- package/src/version.ts +1 -1
- package/dist/chunks/chunk-3FGJLRG2.js +0 -2
- package/dist/chunks/chunk-3FGJLRG2.js.map +0 -7
- package/dist/chunks/chunk-3SZJ4AFI.js +0 -3
- package/dist/chunks/chunk-3SZJ4AFI.js.map +0 -7
- /package/dist/chunks/{GraphGenomeView-CLUMXH4K.js.map → GraphGenomeView-QXKHSZQJ.js.map} +0 -0
- /package/dist/chunks/{LinearGraphDisplay-4CMVCEM5.js.map → LinearGraphDisplay-GMMLJWQ6.js.map} +0 -0
|
@@ -18,9 +18,13 @@ import {
|
|
|
18
18
|
resolveColorScheme,
|
|
19
19
|
} from '@jbrowse/bandage-core/colorSchemes'
|
|
20
20
|
import { deletionEdges } from '@jbrowse/bandage-core/deletionEdges'
|
|
21
|
+
import { FACET_PAD_PX, facetGrid } from '@jbrowse/bandage-core/facetGrid'
|
|
21
22
|
import { genePins } from '@jbrowse/bandage-core/genes/genePins'
|
|
22
23
|
import { rowLabelBox } from '@jbrowse/bandage-core/graphLabels'
|
|
23
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
LEGEND_INSET_PX,
|
|
26
|
+
layoutLabels,
|
|
27
|
+
} from '@jbrowse/bandage-core/labelLayout'
|
|
24
28
|
import { ROW_HEIGHT_PX } from '@jbrowse/bandage-core/layout/rowSpacing'
|
|
25
29
|
import { trimToWindow } from '@jbrowse/bandage-core/layout/trimToWindow'
|
|
26
30
|
import { walkRowsExtent } from '@jbrowse/bandage-core/layout/walkRowLayout'
|
|
@@ -98,7 +102,7 @@ import {
|
|
|
98
102
|
zoomAbout,
|
|
99
103
|
} from '@jbrowse/bandage-core/viewport'
|
|
100
104
|
import { WALK_FIELDS, WALK_SCHEMES } from '@jbrowse/bandage-core/walkEncoding'
|
|
101
|
-
import { walkLift } from '@jbrowse/bandage-core/walkHighlight'
|
|
105
|
+
import { facetLifts, walkLift } from '@jbrowse/bandage-core/walkHighlight'
|
|
102
106
|
import { readConfObject } from '@jbrowse/core/configuration'
|
|
103
107
|
import { pushLaunchViewMenuItem } from '@jbrowse/core/ui'
|
|
104
108
|
import {
|
|
@@ -164,6 +168,7 @@ import type {
|
|
|
164
168
|
ColorScheme,
|
|
165
169
|
ResolvedColorScheme,
|
|
166
170
|
} from '@jbrowse/bandage-core/colorSchemes'
|
|
171
|
+
import type { FacetGrid } from '@jbrowse/bandage-core/facetGrid'
|
|
167
172
|
import type { GeneModel } from '@jbrowse/bandage-core/genes/genePins'
|
|
168
173
|
import type { LayoutModeValue } from '@jbrowse/bandage-core/layoutModes'
|
|
169
174
|
import type { NodeWidth } from '@jbrowse/bandage-core/nodeWidths'
|
|
@@ -184,6 +189,7 @@ import type {
|
|
|
184
189
|
WalkEncoding,
|
|
185
190
|
WalkLayer,
|
|
186
191
|
} from '@jbrowse/bandage-core/walkEncoding'
|
|
192
|
+
import type { WalkLift } from '@jbrowse/bandage-core/walkHighlight'
|
|
187
193
|
import type { AnyConfigurationModel } from '@jbrowse/core/configuration'
|
|
188
194
|
import type { MenuItem } from '@jbrowse/core/ui'
|
|
189
195
|
import type { Feature } from '@jbrowse/core/util'
|
|
@@ -317,15 +323,15 @@ function geometryPainted(model: {
|
|
|
317
323
|
)
|
|
318
324
|
}
|
|
319
325
|
|
|
320
|
-
// What the pane shows, in layout units
|
|
326
|
+
// What the pane shows, in layout units: what each panel shows while faceted
|
|
321
327
|
function paneViewportOf(model: {
|
|
322
328
|
translateX: number
|
|
323
329
|
translateY: number
|
|
324
|
-
paneWidth: number
|
|
325
330
|
axisScale: AxisScale
|
|
326
|
-
|
|
331
|
+
viewBox: { width: number; height: number }
|
|
327
332
|
}): Bounds {
|
|
328
|
-
|
|
333
|
+
const { width, height } = model.viewBox
|
|
334
|
+
return viewportOf(model, model.axisScale, width, height)
|
|
329
335
|
}
|
|
330
336
|
|
|
331
337
|
// Force layouts already computed for a graph, keyed by the view props the
|
|
@@ -449,6 +455,13 @@ export function GraphPaneMixin() {
|
|
|
449
455
|
// and the rest fading, coloured by the encoding it states or by the
|
|
450
456
|
// default one. See walkEncoding.ts. Empty lifts none.
|
|
451
457
|
walkLayers: types.optional(types.frozen<WalkLayer[]>(), []),
|
|
458
|
+
// Facets, as a grammar of graphics splits a plot: 'walk' draws the
|
|
459
|
+
// pane once per lifted walk, side by side on the same layout, each
|
|
460
|
+
// panel with that walk alone. See facetPanels.
|
|
461
|
+
facet: lenientOptionalEnum<'none' | 'walk'>(['none', 'walk'], 'none'),
|
|
462
|
+
// How many facet panels go across; unset takes whichever count draws
|
|
463
|
+
// each largest. See facetGrid.
|
|
464
|
+
facetColumns: types.maybe(types.number),
|
|
452
465
|
// Which of a general GFA's paths the anchored layouts put on x. A path
|
|
453
466
|
// GFA's names are arbitrary and none of them is marked as the
|
|
454
467
|
// reference, so this is a choice; empty means "infer", which is the
|
|
@@ -753,19 +766,54 @@ export function GraphPaneMixin() {
|
|
|
753
766
|
get rampDomain() {
|
|
754
767
|
return self.colorDomain ?? self.graphRegion
|
|
755
768
|
},
|
|
769
|
+
// The ramp a lane coloured by reference position reads, computed only
|
|
770
|
+
// when a layer asks for one: it is a neighbour walk per node
|
|
771
|
+
get walkRamp() {
|
|
772
|
+
const { graph } = self
|
|
773
|
+
return graph &&
|
|
774
|
+
self.walkLayers.some(layer => layer.color?.field === 'reference')
|
|
775
|
+
? computeReferenceRamp(graph, this.rampDomain)
|
|
776
|
+
: undefined
|
|
777
|
+
},
|
|
756
778
|
// The lifted walks the graph on screen carries, or undefined when none
|
|
757
|
-
// is named or it carries none of those that were
|
|
758
|
-
// reference position reads the ramp whatever the node colour scheme.
|
|
779
|
+
// is named or it carries none of those that were
|
|
759
780
|
get walkLift() {
|
|
760
781
|
const { graph } = self
|
|
761
|
-
return graph &&
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
782
|
+
return graph &&
|
|
783
|
+
self.walkLayers.length > 0 &&
|
|
784
|
+
!self.layoutResult?.tubeMap
|
|
785
|
+
? walkLift(graph, self.walkLayers, this.walkRamp)
|
|
786
|
+
: undefined
|
|
787
|
+
},
|
|
788
|
+
// One panel per lifted walk while the pane is faceted by walk, each a
|
|
789
|
+
// lift of that walk alone. See facetLifts.
|
|
790
|
+
get facetPanels() {
|
|
791
|
+
const { graph } = self
|
|
792
|
+
const lift = this.walkLift
|
|
793
|
+
return self.facet === 'walk' && graph && lift && lift.walks.length > 1
|
|
794
|
+
? facetLifts(graph, lift, self.walkLayers, this.walkRamp)
|
|
767
795
|
: undefined
|
|
768
796
|
},
|
|
797
|
+
// The lifted walks as drawn, each panel's own while faceted
|
|
798
|
+
get drawnWalks() {
|
|
799
|
+
return (
|
|
800
|
+
this.facetPanels?.map(panel => panel.walks[0]!) ??
|
|
801
|
+
this.walkLift?.walks ??
|
|
802
|
+
[]
|
|
803
|
+
)
|
|
804
|
+
},
|
|
805
|
+
// A tube map draws every walk as a tube of its own
|
|
806
|
+
get liftsWalks() {
|
|
807
|
+
return !TUBE_MAP_MODES.has(self.chosenLayoutMode)
|
|
808
|
+
},
|
|
809
|
+
// The window a lane coloured by reference position spans, by name
|
|
810
|
+
get walkReference() {
|
|
811
|
+
const domain = this.walkLift?.referenceDomain
|
|
812
|
+
return domain && { ...domain, name: self.graphRegion?.refName }
|
|
813
|
+
},
|
|
814
|
+
walkLabel(name: string) {
|
|
815
|
+
return this.walkChoices.find(c => c.name === name)?.label ?? name
|
|
816
|
+
},
|
|
769
817
|
// The scheme the renderer actually paints with, which is the raw prop
|
|
770
818
|
// unless it is 'auto'. A bare getter returns a resolved value (root
|
|
771
819
|
// CLAUDE.md): every consumer of a colour reads this one, and the two
|
|
@@ -784,8 +832,9 @@ export function GraphPaneMixin() {
|
|
|
784
832
|
get nodePositions() {
|
|
785
833
|
return self.layoutResult?.nodePositions
|
|
786
834
|
},
|
|
835
|
+
// a length label would sit on the lanes of lifted walks
|
|
787
836
|
get labelsNodeSizes() {
|
|
788
|
-
return !self.layoutResult?.tubeMap
|
|
837
|
+
return !self.layoutResult?.tubeMap && !this.walkLift
|
|
789
838
|
},
|
|
790
839
|
// Empty rather than undefined: every consumer maps over it, and a layout
|
|
791
840
|
// with no row structure (FMMM) is a normal state, not a missing one.
|
|
@@ -1420,6 +1469,7 @@ export function GraphPaneMixin() {
|
|
|
1420
1469
|
!!layoutResult &&
|
|
1421
1470
|
!layoutResult.tubeMap &&
|
|
1422
1471
|
!self.hostPlacesX &&
|
|
1472
|
+
!self.facetPanels &&
|
|
1423
1473
|
self.chosenLayoutMode !== 'walkrows'
|
|
1424
1474
|
)
|
|
1425
1475
|
},
|
|
@@ -1432,6 +1482,7 @@ export function GraphPaneMixin() {
|
|
|
1432
1482
|
colorScheme: self.effectiveColorScheme,
|
|
1433
1483
|
referenceRamp: self.referenceRamp,
|
|
1434
1484
|
walkNodes: self.walkLift?.nodeIds,
|
|
1485
|
+
walkColors: self.walkLift?.walks.find(w => w.reference)?.colors,
|
|
1435
1486
|
})
|
|
1436
1487
|
: []
|
|
1437
1488
|
},
|
|
@@ -1546,27 +1597,59 @@ export function GraphPaneMixin() {
|
|
|
1546
1597
|
// isotropic layout has no height of its own and takes its aspect ratio
|
|
1547
1598
|
// at the width's fit. Neither reads `scale`, so the fit reads this
|
|
1548
1599
|
// without feeding back into it.
|
|
1549
|
-
|
|
1600
|
+
// Lifted walks' legend rows carry their scales, which makes the legend
|
|
1601
|
+
// wide, so the fit leaves it that width rather than draw under it
|
|
1602
|
+
get fitPadRight() {
|
|
1603
|
+
return self.walkLift
|
|
1604
|
+
? Math.max(FIT_PADDING, self.legendSize.width + 2 * LEGEND_INSET_PX)
|
|
1605
|
+
: FIT_PADDING
|
|
1606
|
+
},
|
|
1607
|
+
// How the facet panels tile the pane. A track's height is its own, and
|
|
1608
|
+
// a track whose x the linear view places stacks full-width panels so
|
|
1609
|
+
// each keeps that x.
|
|
1610
|
+
get facetGrid(): FacetGrid | undefined {
|
|
1611
|
+
const panels = self.facetPanels
|
|
1550
1612
|
const bounds = self.layoutBounds
|
|
1551
|
-
|
|
1613
|
+
return panels && bounds && bounds.w > 0
|
|
1614
|
+
? facetGrid({
|
|
1615
|
+
count: panels.length,
|
|
1616
|
+
bounds,
|
|
1617
|
+
pixelRows: self.pixelRows,
|
|
1618
|
+
width: self.paneWidth,
|
|
1619
|
+
room: self.host ? this.canvasHeight : this.paneCeiling,
|
|
1620
|
+
columns: self.hostPlacesX ? 1 : self.facetColumns,
|
|
1621
|
+
})
|
|
1622
|
+
: undefined
|
|
1623
|
+
},
|
|
1624
|
+
get canvasHeight(): number {
|
|
1625
|
+
const bounds = self.layoutBounds
|
|
1626
|
+
const usableWidth = self.paneWidth - FIT_PADDING - this.fitPadRight
|
|
1552
1627
|
const ceiling = this.paneCeiling
|
|
1553
1628
|
if (!bounds) {
|
|
1554
1629
|
return ceiling
|
|
1555
1630
|
}
|
|
1631
|
+
const grid = this.facetGrid
|
|
1632
|
+
if (grid) {
|
|
1633
|
+
return Math.max(MIN_CANVAS_HEIGHT, grid.total)
|
|
1634
|
+
}
|
|
1635
|
+
// never shorter than the legend, which a flat drawing would clip
|
|
1636
|
+
const floor = Math.max(
|
|
1637
|
+
MIN_CANVAS_HEIGHT,
|
|
1638
|
+
self.legendSize.height +
|
|
1639
|
+
2 * LEGEND_INSET_PX +
|
|
1640
|
+
self.referenceStripZonePx,
|
|
1641
|
+
)
|
|
1556
1642
|
if (self.pixelRows) {
|
|
1557
1643
|
return Math.min(
|
|
1558
1644
|
ceiling,
|
|
1559
|
-
Math.max(
|
|
1560
|
-
MIN_CANVAS_HEIGHT,
|
|
1561
|
-
bounds.h + this.fitPadTop + FIT_PADDING,
|
|
1562
|
-
),
|
|
1645
|
+
Math.max(floor, bounds.h + this.fitPadTop + FIT_PADDING),
|
|
1563
1646
|
)
|
|
1564
1647
|
}
|
|
1565
1648
|
return bounds.w > 0 && usableWidth > 0
|
|
1566
1649
|
? Math.min(
|
|
1567
1650
|
ceiling,
|
|
1568
1651
|
Math.max(
|
|
1569
|
-
|
|
1652
|
+
floor,
|
|
1570
1653
|
bounds.h * Math.max(usableWidth / bounds.w, this.minFitScale) +
|
|
1571
1654
|
this.fitPadTop +
|
|
1572
1655
|
FIT_PADDING,
|
|
@@ -1574,26 +1657,125 @@ export function GraphPaneMixin() {
|
|
|
1574
1657
|
)
|
|
1575
1658
|
: ceiling
|
|
1576
1659
|
},
|
|
1660
|
+
// What the transform maps the drawing into: one facet panel while the
|
|
1661
|
+
// pane is faceted, since every panel shares it, else the pane
|
|
1662
|
+
get viewBox() {
|
|
1663
|
+
const grid = this.facetGrid
|
|
1664
|
+
return grid
|
|
1665
|
+
? { width: grid.width, height: grid.height }
|
|
1666
|
+
: { width: self.paneWidth, height: this.canvasHeight }
|
|
1667
|
+
},
|
|
1577
1668
|
// Where the fit puts the drawing, or undefined until there is a layout
|
|
1578
1669
|
// and a measured canvas to fit it into
|
|
1579
1670
|
get fittedTransform() {
|
|
1580
1671
|
const bounds = self.layoutBounds
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1672
|
+
const grid = this.facetGrid
|
|
1673
|
+
return !bounds
|
|
1674
|
+
? undefined
|
|
1675
|
+
: grid
|
|
1676
|
+
? fitTransform(bounds, grid.width, grid.height, self.pixelRows, {
|
|
1677
|
+
padLeft: FACET_PAD_PX,
|
|
1678
|
+
padTop: FACET_PAD_PX,
|
|
1679
|
+
padRight: FACET_PAD_PX,
|
|
1680
|
+
padBottom: FACET_PAD_PX,
|
|
1681
|
+
})
|
|
1682
|
+
: fitTransform(
|
|
1683
|
+
bounds,
|
|
1684
|
+
self.paneWidth,
|
|
1685
|
+
this.canvasHeight,
|
|
1686
|
+
self.pixelRows,
|
|
1687
|
+
{
|
|
1688
|
+
minScale: this.minFitScale,
|
|
1689
|
+
padLeft: self.fitPadLeft,
|
|
1690
|
+
padTop: this.fitPadTop,
|
|
1691
|
+
padRight: this.fitPadRight,
|
|
1692
|
+
},
|
|
1693
|
+
)
|
|
1594
1694
|
},
|
|
1595
1695
|
}))
|
|
1596
1696
|
.views(self => ({
|
|
1697
|
+
// The window the transform shows plus a pane of overscan all round,
|
|
1698
|
+
// which a pan inside needs no rebuild for
|
|
1699
|
+
viewportToBuild() {
|
|
1700
|
+
return padded(paneViewportOf(self), VIEWPORT_PANES_BUILT)
|
|
1701
|
+
},
|
|
1702
|
+
// Hover and selection as draw-time colour overrides, stated whole, so
|
|
1703
|
+
// there is nothing to restore and nothing to go stale when a rebuild
|
|
1704
|
+
// renumbers the batch
|
|
1705
|
+
applyHighlights(b: Renderer) {
|
|
1706
|
+
const { hoveredNode, hoveredEdge, selectedNode } = self
|
|
1707
|
+
const nodes = new Map<string, number>()
|
|
1708
|
+
if (selectedNode !== null) {
|
|
1709
|
+
nodes.set(selectedNode, SELECT_BRIGHTEN)
|
|
1710
|
+
}
|
|
1711
|
+
if (hoveredNode !== null && hoveredNode !== selectedNode) {
|
|
1712
|
+
nodes.set(hoveredNode, HOVER_BRIGHTEN)
|
|
1713
|
+
}
|
|
1714
|
+
b.setNodeHighlights(nodes)
|
|
1715
|
+
b.setEdgeHighlight(hoveredEdge, HOVER_BRIGHTEN)
|
|
1716
|
+
},
|
|
1717
|
+
// Draws the uploaded batch through the pane's transform
|
|
1718
|
+
paint(b: Renderer) {
|
|
1719
|
+
// getDpr(), never a bare `devicePixelRatio`: it is capped at
|
|
1720
|
+
// MAX_DPR so this canvas costs what every other canvas in the app
|
|
1721
|
+
// costs on a 3x display (the square of the ratio, i.e. 9x the
|
|
1722
|
+
// pixels of 1x against the 4x everything else pays), and it is the
|
|
1723
|
+
// same read `syncCanvasSize` sizes the backing store with — two
|
|
1724
|
+
// call sites reading the global separately can disagree, and then
|
|
1725
|
+
// the geometry lands at a different scale from the canvas under it.
|
|
1726
|
+
const dpr = getDpr()
|
|
1727
|
+
b.updateTransform({
|
|
1728
|
+
scaleX: self.scaleX * dpr,
|
|
1729
|
+
scaleY: self.scaleY * dpr,
|
|
1730
|
+
translateX: self.translateX * dpr,
|
|
1731
|
+
translateY: self.translateY * dpr,
|
|
1732
|
+
// Handed over rather than read again by the backend: the
|
|
1733
|
+
// thicknesses in the vertex buffer are css px and are expanded
|
|
1734
|
+
// after this transform, so they need the same ratio the fields
|
|
1735
|
+
// above were already multiplied by. See TransformUniform.dpr.
|
|
1736
|
+
dpr,
|
|
1737
|
+
})
|
|
1738
|
+
b.render(self.darkMode ? [0.12, 0.12, 0.12, 1] : [1, 1, 1, 1])
|
|
1739
|
+
},
|
|
1740
|
+
}))
|
|
1741
|
+
.views(self => ({
|
|
1742
|
+
// The drawing's batch with `highlight`'s walks lifted, for the window
|
|
1743
|
+
// the transform shows. A caller's autorun rebuilds it when the window
|
|
1744
|
+
// settles after a pan or zoom, when a drag moves the positions, and when
|
|
1745
|
+
// any display option read here changes.
|
|
1746
|
+
buildDrawing(highlight: WalkLift | undefined, drawPaths: boolean) {
|
|
1747
|
+
const { nodePositions, graph, nodeById } = self
|
|
1748
|
+
if (!nodePositions || !graph || !nodeById) {
|
|
1749
|
+
return undefined
|
|
1750
|
+
}
|
|
1751
|
+
dependOn(self.viewportDirty, self.positionsVersion)
|
|
1752
|
+
const viewportBounds = untracked(() => self.viewportToBuild())
|
|
1753
|
+
const batch = buildGeometry({
|
|
1754
|
+
nodePositions,
|
|
1755
|
+
graph,
|
|
1756
|
+
nodeById,
|
|
1757
|
+
colorScheme: self.effectiveColorScheme,
|
|
1758
|
+
contigThickness: self.contigThickness,
|
|
1759
|
+
connectorThickness: self.connectorThickness,
|
|
1760
|
+
drawPaths,
|
|
1761
|
+
nodeWidth: self.nodeWidth,
|
|
1762
|
+
highlight,
|
|
1763
|
+
// Untracked, so a zoom does not eagerly rebuild geometry — the
|
|
1764
|
+
// debounced viewportDirty bump drives the scale-dependent rebuild
|
|
1765
|
+
// (flatness, arrow visibility, viewport culling), same as pan.
|
|
1766
|
+
axis: untracked(() => self.axisScale),
|
|
1767
|
+
linearLayout: self.linearLayout,
|
|
1768
|
+
viewportBounds,
|
|
1769
|
+
// Held against the graph rather than derived here, the same way
|
|
1770
|
+
// `deletions` is and for the same reason — see `referenceRamp`.
|
|
1771
|
+
referenceRamp: self.referenceRamp,
|
|
1772
|
+
deletions: self.deletionEdgeIndexes,
|
|
1773
|
+
hiddenEdges: self.hiddenEdgeIndexes,
|
|
1774
|
+
// passed so the shared edge-curve cache can tell a drag from a pan
|
|
1775
|
+
version: self.positionsVersion,
|
|
1776
|
+
})
|
|
1777
|
+
return { batch, viewportBounds }
|
|
1778
|
+
},
|
|
1597
1779
|
get overlayLabels() {
|
|
1598
1780
|
return layoutLabels(self)
|
|
1599
1781
|
},
|
|
@@ -1786,6 +1968,12 @@ export function GraphPaneMixin() {
|
|
|
1786
1968
|
const had = new Map(self.walkLayers.map(l => [l.walk, l]))
|
|
1787
1969
|
self.walkLayers = names.map(walk => had.get(walk) ?? { walk })
|
|
1788
1970
|
},
|
|
1971
|
+
setFacet(facet: 'none' | 'walk') {
|
|
1972
|
+
self.facet = facet
|
|
1973
|
+
},
|
|
1974
|
+
setFacetColumns(columns: number | undefined) {
|
|
1975
|
+
self.facetColumns = columns
|
|
1976
|
+
},
|
|
1789
1977
|
toggleWalk(walk: string) {
|
|
1790
1978
|
const layers = self.walkLayers
|
|
1791
1979
|
self.walkLayers = layers.some(l => l.walk === walk)
|
|
@@ -1797,6 +1985,7 @@ export function GraphPaneMixin() {
|
|
|
1797
1985
|
l.walk === walk ? { ...l, color: { ...l.color, ...color } } : l,
|
|
1798
1986
|
)
|
|
1799
1987
|
},
|
|
1988
|
+
|
|
1800
1989
|
// Undefined restores the built-in ceiling. Nothing recomputes: the pane
|
|
1801
1990
|
// reads canvasHeight and the drawing is placed by zoomToFit, which the
|
|
1802
1991
|
// caller runs if it wants the drawing refitted into the new pane.
|
|
@@ -1952,12 +2141,14 @@ export function GraphPaneMixin() {
|
|
|
1952
2141
|
// A host owns x, so a fit while hosted places the rows only.
|
|
1953
2142
|
if (self.viewportOwner === 'host') {
|
|
1954
2143
|
const bounds = self.layoutBounds
|
|
1955
|
-
const
|
|
2144
|
+
const pad = self.facetGrid ? FACET_PAD_PX : FIT_PADDING
|
|
2145
|
+
const usableHeight = self.viewBox.height - pad * 2
|
|
1956
2146
|
if (bounds && usableHeight > 0) {
|
|
1957
2147
|
self.translateY = fittedTranslateY(
|
|
1958
2148
|
bounds,
|
|
1959
2149
|
usableHeight,
|
|
1960
2150
|
self.scaleY,
|
|
2151
|
+
pad,
|
|
1961
2152
|
)
|
|
1962
2153
|
}
|
|
1963
2154
|
return
|
|
@@ -1995,6 +2186,14 @@ export function GraphPaneMixin() {
|
|
|
1995
2186
|
self.zoomToFit()
|
|
1996
2187
|
}
|
|
1997
2188
|
},
|
|
2189
|
+
// Faceting changes the box the drawing is fitted into, so a drawing the
|
|
2190
|
+
// user placed is fitted again; a host keeps its x
|
|
2191
|
+
refitView() {
|
|
2192
|
+
if (self.viewportOwner === 'user') {
|
|
2193
|
+
self.viewportOwner = 'fit'
|
|
2194
|
+
}
|
|
2195
|
+
self.zoomToFit()
|
|
2196
|
+
},
|
|
1998
2197
|
// A drawing still on the reference stays where the host left it; one
|
|
1999
2198
|
// whose x no longer means bp is refit.
|
|
2000
2199
|
releaseHost() {
|
|
@@ -2693,6 +2892,16 @@ export function GraphPaneMixin() {
|
|
|
2693
2892
|
}),
|
|
2694
2893
|
)
|
|
2695
2894
|
|
|
2895
|
+
addDisposer(
|
|
2896
|
+
self,
|
|
2897
|
+
reaction(
|
|
2898
|
+
() => self.facetGrid !== undefined,
|
|
2899
|
+
() => {
|
|
2900
|
+
self.refitView()
|
|
2901
|
+
},
|
|
2902
|
+
),
|
|
2903
|
+
)
|
|
2904
|
+
|
|
2696
2905
|
// Autorun: mirror a connected linear view's hover onto the graph. An
|
|
2697
2906
|
// LGV writes `{hoverPosition, hoverFeature}` to session.hovered on
|
|
2698
2907
|
// every mousemove; neither field names the source view, so the guard
|
|
@@ -2780,28 +2989,15 @@ export function GraphPaneMixin() {
|
|
|
2780
2989
|
}),
|
|
2781
2990
|
)
|
|
2782
2991
|
|
|
2783
|
-
// Autorun: hover
|
|
2784
|
-
//
|
|
2785
|
-
// stale when a rebuild renumbers the batch. Tracks `geometryVersion`
|
|
2786
|
-
// because an upload drops the renderer's edge highlight.
|
|
2992
|
+
// Autorun: hover and selection. Tracks `geometryVersion` because an
|
|
2993
|
+
// upload drops the renderer's edge highlight.
|
|
2787
2994
|
addDisposer(
|
|
2788
2995
|
self,
|
|
2789
2996
|
autorun(() => {
|
|
2790
2997
|
const b = self.currentRenderingBackend as Renderer | undefined
|
|
2791
|
-
const hoveredNode = self.hoveredNode
|
|
2792
|
-
const hoveredEdge = self.hoveredEdge
|
|
2793
|
-
const selectedNode = self.selectedNode
|
|
2794
2998
|
dependOn(self.geometryVersion)
|
|
2795
2999
|
if (b) {
|
|
2796
|
-
|
|
2797
|
-
if (selectedNode !== null) {
|
|
2798
|
-
nodes.set(selectedNode, SELECT_BRIGHTEN)
|
|
2799
|
-
}
|
|
2800
|
-
if (hoveredNode !== null && hoveredNode !== selectedNode) {
|
|
2801
|
-
nodes.set(hoveredNode, HOVER_BRIGHTEN)
|
|
2802
|
-
}
|
|
2803
|
-
b.setNodeHighlights(nodes)
|
|
2804
|
-
b.setEdgeHighlight(hoveredEdge, HOVER_BRIGHTEN)
|
|
3000
|
+
self.applyHighlights(b)
|
|
2805
3001
|
self.renderNow()
|
|
2806
3002
|
}
|
|
2807
3003
|
}),
|
|
@@ -2817,7 +3013,6 @@ export function GraphPaneMixin() {
|
|
|
2817
3013
|
// rebuild — only the debounced viewportDirty flag does.
|
|
2818
3014
|
upload: (b: Renderer) => {
|
|
2819
3015
|
b.resize(self.paneWidth, self.canvasHeight)
|
|
2820
|
-
const nodeById = self.nodeById
|
|
2821
3016
|
if (self.layoutResult?.tubeMap) {
|
|
2822
3017
|
dependOn(self.viewportDirty)
|
|
2823
3018
|
b.uploadGeometry(EMPTY_BATCH)
|
|
@@ -2827,48 +3022,29 @@ export function GraphPaneMixin() {
|
|
|
2827
3022
|
})
|
|
2828
3023
|
return true
|
|
2829
3024
|
}
|
|
2830
|
-
if (self.
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
const viewportBounds = untracked(() =>
|
|
2837
|
-
padded(paneViewportOf(self), VIEWPORT_PANES_BUILT),
|
|
2838
|
-
)
|
|
2839
|
-
const batch = buildGeometry({
|
|
2840
|
-
nodePositions: self.nodePositions,
|
|
2841
|
-
graph: self.graph,
|
|
2842
|
-
nodeById,
|
|
2843
|
-
colorScheme: self.effectiveColorScheme,
|
|
2844
|
-
contigThickness: self.contigThickness,
|
|
2845
|
-
connectorThickness: self.connectorThickness,
|
|
2846
|
-
drawPaths: self.effectiveDrawPaths,
|
|
2847
|
-
nodeWidth: self.nodeWidth,
|
|
2848
|
-
highlight: self.walkLift,
|
|
2849
|
-
// Untracked, so a zoom does not eagerly rebuild geometry — the
|
|
2850
|
-
// debounced viewportDirty bump drives the scale-dependent
|
|
2851
|
-
// rebuild (flatness, arrow visibility, viewport culling), same
|
|
2852
|
-
// as pan.
|
|
2853
|
-
axis: untracked(() => self.axisScale),
|
|
2854
|
-
linearLayout: self.linearLayout,
|
|
2855
|
-
viewportBounds,
|
|
2856
|
-
// Where each node sits on the reference and what interval the
|
|
2857
|
-
// hue spans. Held against the graph rather than derived here,
|
|
2858
|
-
// the same way `deletions` is and for the same reason — see
|
|
2859
|
-
// the `referenceRamp` view.
|
|
2860
|
-
referenceRamp: self.referenceRamp,
|
|
2861
|
-
deletions: self.deletionEdgeIndexes,
|
|
2862
|
-
hiddenEdges: self.hiddenEdgeIndexes,
|
|
2863
|
-
// Read tracked above; passed here so the shared edge-curve
|
|
2864
|
-
// cache can tell a drag from a pan.
|
|
2865
|
-
version: self.positionsVersion,
|
|
3025
|
+
if (self.facetPanels) {
|
|
3026
|
+
dependOn(self.viewportDirty)
|
|
3027
|
+
b.uploadGeometry(EMPTY_BATCH)
|
|
3028
|
+
self.setGeometryMetrics(0, 0, {
|
|
3029
|
+
scale: untracked(() => self.scale),
|
|
3030
|
+
bounds: untracked(() => self.viewportToBuild()),
|
|
2866
3031
|
})
|
|
2867
|
-
|
|
3032
|
+
return true
|
|
3033
|
+
}
|
|
3034
|
+
const geometryStart = performance.now()
|
|
3035
|
+
const built = self.buildDrawing(
|
|
3036
|
+
self.walkLift,
|
|
3037
|
+
self.effectiveDrawPaths,
|
|
3038
|
+
)
|
|
3039
|
+
if (built) {
|
|
3040
|
+
b.uploadGeometry(built.batch)
|
|
2868
3041
|
self.setGeometryMetrics(
|
|
2869
3042
|
performance.now() - geometryStart,
|
|
2870
|
-
batch.nodeStrokes.length,
|
|
2871
|
-
{
|
|
3043
|
+
built.batch.nodeStrokes.length,
|
|
3044
|
+
{
|
|
3045
|
+
scale: untracked(() => self.scale),
|
|
3046
|
+
bounds: built.viewportBounds,
|
|
3047
|
+
},
|
|
2872
3048
|
)
|
|
2873
3049
|
return true
|
|
2874
3050
|
}
|
|
@@ -2883,26 +3059,7 @@ export function GraphPaneMixin() {
|
|
|
2883
3059
|
if (!self.nodePositions) {
|
|
2884
3060
|
return false
|
|
2885
3061
|
}
|
|
2886
|
-
|
|
2887
|
-
// MAX_DPR so this canvas costs what every other canvas in the app
|
|
2888
|
-
// costs on a 3x display (the square of the ratio, i.e. 9x the
|
|
2889
|
-
// pixels of 1x against the 4x everything else pays), and it is the
|
|
2890
|
-
// same read `syncCanvasSize` sizes the backing store with — two
|
|
2891
|
-
// call sites reading the global separately can disagree, and then
|
|
2892
|
-
// the geometry lands at a different scale from the canvas under it.
|
|
2893
|
-
const dpr = getDpr()
|
|
2894
|
-
b.updateTransform({
|
|
2895
|
-
scaleX: self.scaleX * dpr,
|
|
2896
|
-
scaleY: self.scaleY * dpr,
|
|
2897
|
-
translateX: self.translateX * dpr,
|
|
2898
|
-
translateY: self.translateY * dpr,
|
|
2899
|
-
// Handed over rather than read again by the backend: the
|
|
2900
|
-
// thicknesses in the vertex buffer are css px and are expanded
|
|
2901
|
-
// after this transform, so they need the same ratio the fields
|
|
2902
|
-
// above were already multiplied by. See TransformUniform.dpr.
|
|
2903
|
-
dpr,
|
|
2904
|
-
})
|
|
2905
|
-
b.render(self.darkMode ? [0.12, 0.12, 0.12, 1] : [1, 1, 1, 1])
|
|
3062
|
+
self.paint(b)
|
|
2906
3063
|
self.markPainted()
|
|
2907
3064
|
return true
|
|
2908
3065
|
},
|
|
@@ -3025,7 +3182,7 @@ export function GraphPaneMixin() {
|
|
|
3025
3182
|
},
|
|
3026
3183
|
})),
|
|
3027
3184
|
},
|
|
3028
|
-
...(walks.length > 0
|
|
3185
|
+
...(walks.length > 0 && self.liftsWalks
|
|
3029
3186
|
? [
|
|
3030
3187
|
{
|
|
3031
3188
|
label: 'Walk',
|
|
@@ -3036,6 +3193,39 @@ export function GraphPaneMixin() {
|
|
|
3036
3193
|
self.setWalkLayers([])
|
|
3037
3194
|
},
|
|
3038
3195
|
},
|
|
3196
|
+
...(self.walkLayers.length > 1
|
|
3197
|
+
? [
|
|
3198
|
+
{
|
|
3199
|
+
type: 'checkbox' as const,
|
|
3200
|
+
label: 'Side by side',
|
|
3201
|
+
checked: self.facet === 'walk',
|
|
3202
|
+
onClick: () => {
|
|
3203
|
+
self.setFacet(
|
|
3204
|
+
self.facet === 'walk' ? 'none' : 'walk',
|
|
3205
|
+
)
|
|
3206
|
+
},
|
|
3207
|
+
},
|
|
3208
|
+
]
|
|
3209
|
+
: []),
|
|
3210
|
+
...(self.facetPanels && !self.hostPlacesX
|
|
3211
|
+
? [
|
|
3212
|
+
{
|
|
3213
|
+
label: 'Columns',
|
|
3214
|
+
subMenu: [
|
|
3215
|
+
undefined,
|
|
3216
|
+
...self.facetPanels.map((_, i) => i + 1),
|
|
3217
|
+
].map(columns => ({
|
|
3218
|
+
type: 'radio' as const,
|
|
3219
|
+
label:
|
|
3220
|
+
columns === undefined ? 'Auto' : `${columns}`,
|
|
3221
|
+
checked: self.facetColumns === columns,
|
|
3222
|
+
onClick: () => {
|
|
3223
|
+
self.setFacetColumns(columns)
|
|
3224
|
+
},
|
|
3225
|
+
})),
|
|
3226
|
+
},
|
|
3227
|
+
]
|
|
3228
|
+
: []),
|
|
3039
3229
|
...walks.map(walk => ({
|
|
3040
3230
|
type: 'checkbox' as const,
|
|
3041
3231
|
label: walk.label,
|
|
@@ -3044,8 +3234,8 @@ export function GraphPaneMixin() {
|
|
|
3044
3234
|
self.toggleWalk(walk.name)
|
|
3045
3235
|
},
|
|
3046
3236
|
})),
|
|
3047
|
-
...
|
|
3048
|
-
label: `Colour ${
|
|
3237
|
+
...self.drawnWalks.map(lifted => ({
|
|
3238
|
+
label: `Colour ${self.walkLabel(lifted.name)}`,
|
|
3049
3239
|
subMenu: [
|
|
3050
3240
|
{ type: 'subHeader' as const, label: 'Colour by' },
|
|
3051
3241
|
...WALK_FIELDS.map(field => ({
|
|
@@ -3059,7 +3249,12 @@ export function GraphPaneMixin() {
|
|
|
3059
3249
|
},
|
|
3060
3250
|
})),
|
|
3061
3251
|
{ type: 'subHeader' as const, label: 'Palette' },
|
|
3062
|
-
|
|
3252
|
+
// the rainbow is the reference-position ramp
|
|
3253
|
+
...WALK_SCHEMES.filter(
|
|
3254
|
+
scheme =>
|
|
3255
|
+
scheme.value !== 'rainbow' ||
|
|
3256
|
+
lifted.encoding.field === 'reference',
|
|
3257
|
+
).map(scheme => ({
|
|
3063
3258
|
type: 'radio' as const,
|
|
3064
3259
|
label: scheme.label,
|
|
3065
3260
|
checked: lifted.encoding.scheme === scheme.value,
|
|
@@ -3124,7 +3319,11 @@ export function GraphPaneMixin() {
|
|
|
3124
3319
|
{
|
|
3125
3320
|
label: 'Zoom in',
|
|
3126
3321
|
onClick: () => {
|
|
3127
|
-
self.zoom(
|
|
3322
|
+
self.zoom(
|
|
3323
|
+
1.5,
|
|
3324
|
+
self.viewBox.width / 2,
|
|
3325
|
+
self.viewBox.height / 2,
|
|
3326
|
+
)
|
|
3128
3327
|
},
|
|
3129
3328
|
},
|
|
3130
3329
|
{
|
|
@@ -3132,8 +3331,8 @@ export function GraphPaneMixin() {
|
|
|
3132
3331
|
onClick: () => {
|
|
3133
3332
|
self.zoom(
|
|
3134
3333
|
1 / 1.5,
|
|
3135
|
-
self.
|
|
3136
|
-
self.
|
|
3334
|
+
self.viewBox.width / 2,
|
|
3335
|
+
self.viewBox.height / 2,
|
|
3137
3336
|
)
|
|
3138
3337
|
},
|
|
3139
3338
|
},
|
|
@@ -380,6 +380,8 @@ export function stateModelFactory(configSchema: LinearGraphDisplayConfigModel) {
|
|
|
380
380
|
showGenes: self.showGenes,
|
|
381
381
|
showBubbles: self.showBubbles,
|
|
382
382
|
showDeletionEdges: self.showDeletionEdges,
|
|
383
|
+
walkLayers: self.walkLayers,
|
|
384
|
+
facet: self.facet,
|
|
383
385
|
} satisfies GraphViewSpec
|
|
384
386
|
},
|
|
385
387
|
}))
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '4.0.
|
|
1
|
+
export const version = '4.0.23'
|