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.
Files changed (33) hide show
  1. package/README.md +5 -2
  2. package/dist/chunks/{GraphGenomeView-CLUMXH4K.js → GraphGenomeView-QXKHSZQJ.js} +2 -2
  3. package/dist/chunks/{LinearGraphDisplay-4CMVCEM5.js → LinearGraphDisplay-GMMLJWQ6.js} +2 -2
  4. package/dist/chunks/chunk-75CO6QJZ.js +2 -0
  5. package/dist/chunks/chunk-75CO6QJZ.js.map +7 -0
  6. package/dist/chunks/chunk-NSXARD3R.js +3 -0
  7. package/dist/chunks/chunk-NSXARD3R.js.map +7 -0
  8. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +2 -2
  9. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +3 -3
  10. package/package.json +2 -2
  11. package/src/GraphGenomeView/components/BubbleHalos.tsx +2 -10
  12. package/src/GraphGenomeView/components/FacetPanels.tsx +159 -0
  13. package/src/GraphGenomeView/components/GenePins.tsx +12 -5
  14. package/src/GraphGenomeView/components/GraphCanvas.tsx +45 -318
  15. package/src/GraphGenomeView/components/GraphToolbar.tsx +6 -2
  16. package/src/GraphGenomeView/components/ReferenceStripOverlay.tsx +21 -16
  17. package/src/GraphGenomeView/components/TubeMapOverlay.tsx +2 -9
  18. package/src/GraphGenomeView/components/UnpopButton.tsx +1 -2
  19. package/src/GraphGenomeView/components/WalkKey.tsx +52 -0
  20. package/src/GraphGenomeView/components/WalkRowsOverlay.tsx +1 -9
  21. package/src/GraphGenomeView/components/WalkSelect.tsx +2 -2
  22. package/src/GraphGenomeView/components/legendStyles.ts +11 -0
  23. package/src/GraphGenomeView/components/usePaneGestures.ts +274 -0
  24. package/src/GraphGenomeView/model.test.ts +102 -10
  25. package/src/GraphGenomeView/model.ts +318 -119
  26. package/src/LinearGraphDisplay/model.ts +2 -0
  27. package/src/version.ts +1 -1
  28. package/dist/chunks/chunk-3FGJLRG2.js +0 -2
  29. package/dist/chunks/chunk-3FGJLRG2.js.map +0 -7
  30. package/dist/chunks/chunk-3SZJ4AFI.js +0 -3
  31. package/dist/chunks/chunk-3SZJ4AFI.js.map +0 -7
  32. /package/dist/chunks/{GraphGenomeView-CLUMXH4K.js.map → GraphGenomeView-QXKHSZQJ.js.map} +0 -0
  33. /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 { layoutLabels } from '@jbrowse/bandage-core/labelLayout'
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
- canvasHeight: number
331
+ viewBox: { width: number; height: number }
327
332
  }): Bounds {
328
- return viewportOf(model, model.axisScale, model.paneWidth, model.canvasHeight)
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. A walk coloured by
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 && self.walkLayers.length > 0
762
- ? walkLift(
763
- graph,
764
- self.walkLayers,
765
- computeReferenceRamp(graph, this.rampDomain),
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
- get canvasHeight() {
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
- const usableWidth = self.paneWidth - FIT_PADDING * 2
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
- MIN_CANVAS_HEIGHT,
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
- return bounds
1582
- ? fitTransform(
1583
- bounds,
1584
- self.paneWidth,
1585
- this.canvasHeight,
1586
- self.pixelRows,
1587
- {
1588
- minScale: this.minFitScale,
1589
- padLeft: self.fitPadLeft,
1590
- padTop: this.fitPadTop,
1591
- },
1592
- )
1593
- : undefined
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 usableHeight = self.canvasHeight - FIT_PADDING * 2
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/select are draw-time colour overrides, stated whole
2784
- // on every change, so there is nothing to restore and nothing to go
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
- const nodes = new Map<string, number>()
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.nodePositions && self.graph && nodeById) {
2831
- // The window moved (debounced pan/zoom), or the positions did (a
2832
- // node drag, coalesced to a frame). Both change the drawing;
2833
- // everything below reads them untracked or not at all.
2834
- dependOn(self.viewportDirty, self.positionsVersion)
2835
- const geometryStart = performance.now()
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
- b.uploadGeometry(batch)
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
- { scale: untracked(() => self.scale), bounds: viewportBounds },
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
- // getDpr(), never a bare `devicePixelRatio`: it is capped at
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
- ...(self.walkLift?.walks ?? []).map(lifted => ({
3048
- label: `Colour ${walks.find(w => w.name === lifted.name)?.label ?? lifted.name}`,
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
- ...WALK_SCHEMES.map(scheme => ({
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(1.5, self.paneWidth / 2, self.canvasHeight / 2)
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.paneWidth / 2,
3136
- self.canvasHeight / 2,
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.21'
1
+ export const version = '4.0.23'