jbrowse-plugin-graphgenomeviewer 7.1.2 → 7.1.4

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 (68) hide show
  1. package/dist/chunks/{GraphGenomeView-NJ5XCREB.js → GraphGenomeView-PSDUS64B.js} +2 -2
  2. package/dist/chunks/{HighlightColorDialog-UK5VDDV6.js → HighlightColorDialog-2MP3C5AK.js} +2 -2
  3. package/dist/chunks/{LinearGraphDisplay-TXTKAACH.js → LinearGraphDisplay-FTFJFNKT.js} +2 -2
  4. package/dist/chunks/{WalkTabixSyntenyAdapter-U53JLYVU.js → WalkTabixSyntenyAdapter-GH6NYQTF.js} +2 -2
  5. package/dist/chunks/{WalkTabixSyntenyAdapter-U53JLYVU.js.map → WalkTabixSyntenyAdapter-GH6NYQTF.js.map} +1 -1
  6. package/dist/chunks/chunk-HM57SCYO.js +4 -0
  7. package/dist/chunks/chunk-HM57SCYO.js.map +7 -0
  8. package/dist/chunks/{chunk-WEGWTC52.js → chunk-LELA7BG7.js} +2 -2
  9. package/dist/chunks/{chunk-WEGWTC52.js.map → chunk-LELA7BG7.js.map} +4 -4
  10. package/dist/chunks/chunk-NP42Y2MV.js +2 -0
  11. package/dist/chunks/chunk-NP42Y2MV.js.map +7 -0
  12. package/dist/chunks/chunk-PJ43KEV6.js +2 -0
  13. package/dist/chunks/chunk-PJ43KEV6.js.map +7 -0
  14. package/dist/chunks/chunk-RP3L3Z5W.js +3 -0
  15. package/dist/chunks/chunk-RP3L3Z5W.js.map +7 -0
  16. package/dist/chunks/{chunk-FITPB5X6.js → chunk-VQUMZEGN.js} +2 -2
  17. package/dist/chunks/renderSvg-V4OYQTQZ.js +2 -0
  18. package/dist/chunks/renderSvg-V4OYQTQZ.js.map +7 -0
  19. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +4 -4
  20. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
  21. package/package.json +2 -2
  22. package/src/GbzBaseSyntenyAdapter/configSchema.ts +2 -2
  23. package/src/GetSubgraph.ts +1 -1
  24. package/src/GraphAddTrackWorkflow/buildTrackConfig.test.ts +2 -2
  25. package/src/GraphGenomeView/components/GraphCanvas.test.tsx +108 -0
  26. package/src/GraphGenomeView/components/GraphCanvas.tsx +11 -8
  27. package/src/GraphGenomeView/components/Legend.tsx +3 -121
  28. package/src/GraphGenomeView/components/LegendSwatch.tsx +237 -0
  29. package/src/GraphGenomeView/components/PaneSvg.tsx +3 -190
  30. package/src/GraphGenomeView/components/WalkStrip.tsx +1 -1
  31. package/src/GraphGenomeView/model.test.ts +13 -5
  32. package/src/GraphGenomeView/pane/graphViews.ts +10 -0
  33. package/src/GraphGenomeView/pane/loadActions.ts +5 -1
  34. package/src/GraphGenomeView/pane/morph.test.ts +34 -1
  35. package/src/GraphGenomeView/pane/morph.ts +74 -5
  36. package/src/GraphGenomeView/pane/renderingActions.ts +1 -1
  37. package/src/GraphGenomeView/pane/settingActions.ts +10 -43
  38. package/src/GraphGenomeView/subgraphLoad.test.ts +56 -1
  39. package/src/GraphGenomeView/viewModel.ts +55 -33
  40. package/src/GraphGenomeView/walkRowSpans.ts +4 -10
  41. package/src/GraphTrack/index.ts +2 -2
  42. package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +12 -2
  43. package/src/LinearGraphDisplay/denseWindow.ts +0 -14
  44. package/src/LinearGraphDisplay/model.ts +2 -1
  45. package/src/RgfaTabixAdapter/walkFiles.test.ts +1 -0
  46. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +1 -1
  47. package/src/graphTrackDefaults/graphTrackDefaults.test.ts +15 -3
  48. package/src/graphTrackDefaults/index.ts +10 -5
  49. package/src/launchFromGraph/launchFromGraph.test.ts +7 -10
  50. package/src/launchFromGraph/launchFromGraph.ts +26 -2
  51. package/src/nodeLimitError.ts +12 -0
  52. package/src/panSNAliases/panSNAliases.test.ts +129 -3
  53. package/src/panSNAliases/trackAdapterConfig.ts +55 -11
  54. package/src/version.ts +1 -1
  55. package/dist/chunks/chunk-2MCOY3YG.js +0 -3
  56. package/dist/chunks/chunk-2MCOY3YG.js.map +0 -7
  57. package/dist/chunks/chunk-MURN3JPK.js +0 -2
  58. package/dist/chunks/chunk-MURN3JPK.js.map +0 -7
  59. package/dist/chunks/chunk-NWJYLP6Z.js +0 -4
  60. package/dist/chunks/chunk-NWJYLP6Z.js.map +0 -7
  61. package/dist/chunks/chunk-WCGVUZWP.js +0 -2
  62. package/dist/chunks/chunk-WCGVUZWP.js.map +0 -7
  63. package/dist/chunks/renderSvg-KSQI5DH6.js +0 -2
  64. package/dist/chunks/renderSvg-KSQI5DH6.js.map +0 -7
  65. /package/dist/chunks/{GraphGenomeView-NJ5XCREB.js.map → GraphGenomeView-PSDUS64B.js.map} +0 -0
  66. /package/dist/chunks/{HighlightColorDialog-UK5VDDV6.js.map → HighlightColorDialog-2MP3C5AK.js.map} +0 -0
  67. /package/dist/chunks/{LinearGraphDisplay-TXTKAACH.js.map → LinearGraphDisplay-FTFJFNKT.js.map} +0 -0
  68. /package/dist/chunks/{chunk-FITPB5X6.js.map → chunk-VQUMZEGN.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jbrowse-plugin-graphgenomeviewer",
3
- "version": "7.1.2",
3
+ "version": "7.1.4",
4
4
  "description": "JBrowse 2 graph genome (pangenome) view: rGFA reference-anchored layouts, the Bandage force-directed engine, and haplotype lanes read from a gbz-base database",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -98,7 +98,7 @@
98
98
  "test": "vitest run",
99
99
  "test:watch": "vitest",
100
100
  "test:e2e": "vitest run --config vitest.e2e.config.ts",
101
- "preversion": "node scripts/ci-green.mjs && pnpm lint && pnpm format:check && pnpm typecheck && pnpm typecheck:tests && pnpm test && pnpm build && pnpm host-names && pnpm host-compat",
101
+ "preversion": "node scripts/ci-green.mjs && pnpm build && pnpm host-names && pnpm host-compat",
102
102
  "version": "node scripts/version.mjs",
103
103
  "postversion": "git push --atomic --follow-tags"
104
104
  }
@@ -22,8 +22,8 @@ import type { Instance } from '@jbrowse/mobx-state-tree'
22
22
  * haplotype a walk belongs to.
23
23
  *
24
24
  * Only the fine tier exists: a whole-chromosome view stays on a PIF's coarse
25
- * tier. A query on a haplotype lane answers nothing, so the multi-way display
26
- * composes its lane-to-lane links through the reference.
25
+ * tier. A window of the anchor answers any two lanes' alignment, so the
26
+ * multi-way display fetches each adjacent lane pair directly.
27
27
  *
28
28
  * #example
29
29
  * ```js
@@ -65,7 +65,7 @@ declare module '@jbrowse/core/rpc/RpcRegistry' {
65
65
  }
66
66
 
67
67
  // Adapters that can cut a local subgraph out of a graph file implement this:
68
- // RgfaTabixAdapter and GbzBaseSyntenyAdapter today.
68
+ // RgfaTabixAdapter, WalkTabixSyntenyAdapter and GbzBaseSyntenyAdapter.
69
69
  interface SubgraphAdapter {
70
70
  getSubgraph(
71
71
  region: Region,
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
 
3
3
  import { buildTrackConfig } from './buildTrackConfig'
4
- import { withGbzAnchor } from '../graphTrackDefaults/index'
4
+ import { withLaneAnchor } from '../graphTrackDefaults/index'
5
5
 
6
6
  const segs = {
7
7
  uri: 'https://example.com/hprc.segs.bed.gz',
@@ -225,7 +225,7 @@ describe('buildTrackConfig', () => {
225
225
  name: 'HPRC graph',
226
226
  })
227
227
  expect(conf.type).toBe('GraphTrack')
228
- expect(withGbzAnchor(conf).adapter).toEqual({
228
+ expect(withLaneAnchor(conf).adapter).toEqual({
229
229
  type: 'GbzBaseSyntenyAdapter',
230
230
  gbzDbLocation: uri('hprc.gbz.db'),
231
231
  assemblyNames: ['hg38'],
@@ -0,0 +1,108 @@
1
+ import { types } from '@jbrowse/mobx-state-tree'
2
+ import { act, render } from '@testing-library/react'
3
+
4
+ import { Legends } from './GraphCanvas'
5
+
6
+ import type { GraphPaneModel } from '../model'
7
+
8
+ vi.mock('./Legend', () => ({ LegendPanel: () => null }))
9
+
10
+ const Pane = types
11
+ .model({
12
+ hoveredNode: types.maybeNull(types.string),
13
+ hoveredRowWalks: types.array(types.string),
14
+ legendSize: types.frozen({ width: 0, height: 0 }),
15
+ })
16
+ .views(self => ({
17
+ get legendAtRest() {
18
+ return self.hoveredNode === null && self.hoveredRowWalks.length === 0
19
+ },
20
+ get referenceStripZonePx() {
21
+ return 0
22
+ },
23
+ }))
24
+ .actions(self => ({
25
+ setLegendSize(size: { width: number; height: number }) {
26
+ self.legendSize = size
27
+ },
28
+ setHoveredNode(id: string | null) {
29
+ self.hoveredNode = id
30
+ },
31
+ setHoveredRowWalks(walks: string[]) {
32
+ self.hoveredRowWalks.replace(walks)
33
+ },
34
+ }))
35
+
36
+ let size = { width: 0, height: 0 }
37
+ let resized = () => {}
38
+
39
+ beforeEach(() => {
40
+ vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockImplementation(
41
+ () => size.width,
42
+ )
43
+ vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockImplementation(
44
+ () => size.height,
45
+ )
46
+ vi.stubGlobal(
47
+ 'ResizeObserver',
48
+ class {
49
+ constructor(callback: () => void) {
50
+ resized = callback
51
+ }
52
+ observe() {}
53
+ disconnect() {
54
+ resized = () => {}
55
+ }
56
+ },
57
+ )
58
+ })
59
+
60
+ afterEach(() => {
61
+ vi.restoreAllMocks()
62
+ vi.unstubAllGlobals()
63
+ })
64
+
65
+ function shown() {
66
+ size = { width: 120, height: 40 }
67
+ const model = Pane.create({})
68
+ render(<Legends model={model as unknown as GraphPaneModel} />)
69
+ act(() => {
70
+ resized()
71
+ })
72
+ return model
73
+ }
74
+
75
+ function resize(width: number, height: number) {
76
+ size = { width, height }
77
+ act(() => {
78
+ resized()
79
+ })
80
+ }
81
+
82
+ test('a lane-row hover leaves the measured legend as it was', () => {
83
+ const model = shown()
84
+ expect(model.legendSize).toEqual({ width: 120, height: 40 })
85
+ act(() => {
86
+ model.setHoveredRowWalks(['HG00097#1'])
87
+ })
88
+ resize(120, 72)
89
+ expect(model.legendSize).toEqual({ width: 120, height: 40 })
90
+ act(() => {
91
+ model.setHoveredRowWalks([])
92
+ })
93
+ resize(120, 40)
94
+ expect(model.legendSize).toEqual({ width: 120, height: 40 })
95
+ })
96
+
97
+ test('a legend that grows during a node hover is measured when the hover ends', () => {
98
+ const model = shown()
99
+ act(() => {
100
+ model.setHoveredNode('1+')
101
+ })
102
+ resize(120, 90)
103
+ expect(model.legendSize).toEqual({ width: 120, height: 40 })
104
+ act(() => {
105
+ model.setHoveredNode(null)
106
+ })
107
+ expect(model.legendSize).toEqual({ width: 120, height: 90 })
108
+ })
@@ -152,30 +152,33 @@ const deletionLabelStyle = {
152
152
  // Measures itself for the model, so no label is placed under a legend and
153
153
  // none gives way to one that is not drawn.
154
154
  //
155
- // At rest only: the walk keys' hover readout would otherwise refit the view,
156
- // and the refit clears the hover that grew it
157
- const Legends = observer(function Legends({
155
+ // At rest only, and again on returning to rest: a hover's walk keys would
156
+ // otherwise refit the view, and the refit clears the hover that grew them
157
+ export const Legends = observer(function Legends({
158
158
  model,
159
159
  }: {
160
160
  model: GraphPaneModel
161
161
  }) {
162
162
  const ref = useRef<HTMLDivElement>(null)
163
+ const atRest = model.legendAtRest
163
164
  useEffect(() => {
164
165
  const el = ref.current
165
- if (!el) {
166
+ if (!el || !atRest) {
166
167
  return
167
168
  }
168
169
  // the last callback can land after the track is closed
169
- const observer = new ResizeObserver(() => {
170
- if (isAlive(model) && model.hoveredNode === null) {
170
+ const measure = () => {
171
+ if (isAlive(model) && model.legendAtRest) {
171
172
  model.setLegendSize({ width: el.offsetWidth, height: el.offsetHeight })
172
173
  }
173
- })
174
+ }
175
+ measure()
176
+ const observer = new ResizeObserver(measure)
174
177
  observer.observe(el)
175
178
  return () => {
176
179
  observer.disconnect()
177
180
  }
178
- }, [model])
181
+ }, [model, atRest])
179
182
  return (
180
183
  <div
181
184
  ref={ref}
@@ -1,132 +1,14 @@
1
1
  import type { ReactNode } from 'react'
2
2
 
3
- import { rulerInk } from '@jbrowse/bandage-core/tubeMap/axis'
4
3
  import { observer } from 'mobx-react'
5
4
 
6
- import { SWATCH_PX, legendSections, swatchWidth } from '../legendSections'
5
+ import { LegendSwatchView } from './LegendSwatch'
7
6
  import { legendBoxStyle, legendNoteStyle, legendRowStyle } from './legendStyles'
7
+ import { legendSections } from '../legendSections'
8
8
 
9
- import type { LegendRow, LegendSwatch, Stop } from '../legendSections'
9
+ import type { LegendRow } from '../legendSections'
10
10
  import type { GraphPaneModel } from '../model'
11
11
 
12
- function stopsCss(stops: readonly Stop[]) {
13
- return stops.length === 1
14
- ? stops[0]!.color
15
- : `linear-gradient(to right, ${stops.map(s => `${s.color} ${s.offset * 100}%`).join(', ')})`
16
- }
17
-
18
- const glyphStyle = {
19
- width: SWATCH_PX,
20
- flex: 'none',
21
- textAlign: 'center' as const,
22
- fontFamily: 'monospace',
23
- fontSize: 12,
24
- }
25
-
26
- export function LegendSwatchView({ swatch }: { swatch: LegendSwatch }) {
27
- const width = swatchWidth(swatch)
28
- const box = { width, flex: 'none' }
29
- switch (swatch.kind) {
30
- case 'fill':
31
- return (
32
- <div
33
- style={{
34
- ...box,
35
- height: swatch.height ?? 8,
36
- borderRadius: swatch.square ? 0 : 2,
37
- background: stopsCss(swatch.stops),
38
- }}
39
- />
40
- )
41
- case 'bands':
42
- return (
43
- <div style={box}>
44
- {swatch.bands.map((stops, i) => (
45
- <div
46
- key={i}
47
- style={{
48
- height: swatch.bands.length > 1 ? 3 : 4,
49
- background: stopsCss(stops),
50
- }}
51
- />
52
- ))}
53
- </div>
54
- )
55
- case 'line':
56
- return (
57
- <div
58
- style={{
59
- ...box,
60
- height: 3,
61
- borderRadius: 2,
62
- background: swatch.color,
63
- }}
64
- />
65
- )
66
- case 'halo':
67
- return (
68
- <svg width={width} height={10} style={{ flex: 'none' }}>
69
- <line
70
- x1={5}
71
- y1={5}
72
- x2={13}
73
- y2={5}
74
- stroke={swatch.color}
75
- strokeOpacity={0.35}
76
- strokeWidth={8}
77
- strokeLinecap="round"
78
- />
79
- </svg>
80
- )
81
- case 'outline':
82
- return (
83
- <div
84
- style={{
85
- ...box,
86
- height: swatch.height,
87
- boxSizing: 'border-box',
88
- border: `${swatch.stroke}px solid ${swatch.color}`,
89
- borderRadius: swatch.radius,
90
- }}
91
- />
92
- )
93
- case 'overhang':
94
- return (
95
- <svg width={width} height={10} style={{ flex: 'none' }}>
96
- <path d="M0,5 L8,0 L8,10 Z M18,5 L10,0 L10,10 Z" fill="#18181c" />
97
- </svg>
98
- )
99
- case 'glyph':
100
- return <span style={glyphStyle}>{swatch.char}</span>
101
- case 'tick':
102
- return (
103
- <div
104
- style={{
105
- ...box,
106
- height: swatch.height,
107
- display: 'flex',
108
- justifyContent: 'center',
109
- background: swatch.backing,
110
- }}
111
- >
112
- <div style={{ width: 2, backgroundColor: swatch.color }} />
113
- </div>
114
- )
115
- case 'bracket':
116
- return (
117
- <svg width={width} height={7} style={{ flex: 'none' }}>
118
- <path
119
- d={`M0.5,6 V1.5 H5.5 V6 M8.5,6 V1.5 H${SWATCH_PX - 0.5} V6`}
120
- fill="none"
121
- stroke={rulerInk()}
122
- />
123
- </svg>
124
- )
125
- case 'blank':
126
- return <span style={box} />
127
- }
128
- }
129
-
130
12
  // One row: its swatch then its words, or a note with neither. `at`, where
131
13
  // the hovered node sits on a walk, stands in for the line under it.
132
14
  export function LegendRowView({
@@ -0,0 +1,237 @@
1
+ import { rulerInk } from '@jbrowse/bandage-core/tubeMap/axis'
2
+
3
+ import { useSvgId } from './ElTree'
4
+ import { SWATCH_PX, swatchWidth } from '../legendSections'
5
+
6
+ import type { LegendSwatch, Stop } from '../legendSections'
7
+
8
+ function Gradient({
9
+ id,
10
+ x,
11
+ y,
12
+ stops,
13
+ width,
14
+ height,
15
+ square,
16
+ }: {
17
+ id: string
18
+ x: number
19
+ y: number
20
+ stops: readonly Stop[]
21
+ width: number
22
+ height: number
23
+ square?: boolean
24
+ }) {
25
+ const rect = (fill: string) => (
26
+ <rect
27
+ x={x}
28
+ y={y}
29
+ width={width}
30
+ height={height}
31
+ rx={square ? 0 : 2}
32
+ fill={fill}
33
+ />
34
+ )
35
+ return stops.length > 1 ? (
36
+ <>
37
+ <defs>
38
+ <linearGradient id={id}>
39
+ {stops.map((s, i) => (
40
+ <stop key={i} offset={s.offset} stopColor={s.color} />
41
+ ))}
42
+ </linearGradient>
43
+ </defs>
44
+ {rect(`url(#${id})`)}
45
+ </>
46
+ ) : (
47
+ rect(stops[0]!.color)
48
+ )
49
+ }
50
+
51
+ const bandPx = (bands: readonly unknown[]) => (bands.length > 1 ? 3 : 4)
52
+
53
+ function swatchHeight(swatch: LegendSwatch) {
54
+ switch (swatch.kind) {
55
+ case 'fill':
56
+ return swatch.height ?? 8
57
+ case 'bands':
58
+ return bandPx(swatch.bands) * swatch.bands.length
59
+ case 'line':
60
+ return 3
61
+ case 'halo':
62
+ case 'overhang':
63
+ return 10
64
+ case 'outline':
65
+ case 'tick':
66
+ return swatch.height
67
+ case 'glyph':
68
+ return 15
69
+ case 'bracket':
70
+ return 7
71
+ case 'blank':
72
+ return 0
73
+ }
74
+ }
75
+
76
+ // A row's swatch with its left edge at x, centred on the row's text 5 px
77
+ // above the baseline
78
+ export function SvgSwatch({
79
+ swatch,
80
+ x,
81
+ id,
82
+ }: {
83
+ swatch: LegendSwatch
84
+ x: number
85
+ id: string
86
+ }) {
87
+ const mid = -5
88
+ switch (swatch.kind) {
89
+ case 'fill': {
90
+ const height = swatchHeight(swatch)
91
+ return (
92
+ <Gradient
93
+ id={id}
94
+ x={x}
95
+ y={mid - height / 2}
96
+ stops={swatch.stops}
97
+ width={swatchWidth(swatch)}
98
+ height={height}
99
+ square={swatch.square}
100
+ />
101
+ )
102
+ }
103
+ case 'bands': {
104
+ const band = bandPx(swatch.bands)
105
+ const top = mid - swatchHeight(swatch) / 2
106
+ return (
107
+ <>
108
+ {swatch.bands.map((stops, i) => (
109
+ <Gradient
110
+ key={i}
111
+ id={`${id}-${i}`}
112
+ x={x}
113
+ y={top + i * band}
114
+ stops={stops}
115
+ width={SWATCH_PX}
116
+ height={band}
117
+ square
118
+ />
119
+ ))}
120
+ </>
121
+ )
122
+ }
123
+ case 'line':
124
+ return (
125
+ <rect
126
+ x={x}
127
+ y={mid - 1.5}
128
+ width={SWATCH_PX}
129
+ height={3}
130
+ rx={2}
131
+ fill={swatch.color}
132
+ />
133
+ )
134
+ case 'halo':
135
+ return (
136
+ <line
137
+ x1={x + 5}
138
+ x2={x + 13}
139
+ y1={mid}
140
+ y2={mid}
141
+ stroke={swatch.color}
142
+ strokeOpacity={0.35}
143
+ strokeWidth={8}
144
+ strokeLinecap="round"
145
+ />
146
+ )
147
+ case 'outline': {
148
+ const inset = swatch.stroke / 2
149
+ return (
150
+ <rect
151
+ x={x + inset}
152
+ y={mid - swatch.height / 2 + inset}
153
+ width={SWATCH_PX - swatch.stroke}
154
+ height={swatch.height - swatch.stroke}
155
+ rx={Math.max(0, swatch.radius - inset)}
156
+ fill="none"
157
+ stroke={swatch.color}
158
+ strokeWidth={swatch.stroke}
159
+ />
160
+ )
161
+ }
162
+ case 'overhang':
163
+ return (
164
+ <path
165
+ transform={`translate(${x} ${mid - 5})`}
166
+ d="M0,5 L8,0 L8,10 Z M18,5 L10,0 L10,10 Z"
167
+ fill="#18181c"
168
+ />
169
+ )
170
+ case 'glyph':
171
+ return (
172
+ <text
173
+ x={x + SWATCH_PX / 2}
174
+ y={mid + 4}
175
+ textAnchor="middle"
176
+ fontFamily="monospace"
177
+ fontSize={12}
178
+ fill="currentColor"
179
+ >
180
+ {swatch.char}
181
+ </text>
182
+ )
183
+ case 'tick': {
184
+ const y = mid - swatch.height / 2
185
+ return (
186
+ <>
187
+ {swatch.backing ? (
188
+ <rect
189
+ x={x}
190
+ y={y}
191
+ width={SWATCH_PX}
192
+ height={swatch.height}
193
+ fill={swatch.backing}
194
+ />
195
+ ) : null}
196
+ <rect
197
+ x={x + SWATCH_PX / 2 - 1}
198
+ y={y}
199
+ width={2}
200
+ height={swatch.height}
201
+ fill={swatch.color}
202
+ />
203
+ </>
204
+ )
205
+ }
206
+ case 'bracket':
207
+ return (
208
+ <path
209
+ transform={`translate(${x} ${mid - 3.5})`}
210
+ d={`M0.5,6 V1.5 H5.5 V6 M8.5,6 V1.5 H${SWATCH_PX - 0.5} V6`}
211
+ fill="none"
212
+ stroke={rulerInk()}
213
+ />
214
+ )
215
+ case 'blank':
216
+ return null
217
+ }
218
+ }
219
+
220
+ // The same swatch on screen, in a box as tall as the swatch so the row
221
+ // centres it on its words
222
+ export function LegendSwatchView({ swatch }: { swatch: LegendSwatch }) {
223
+ const id = useSvgId()
224
+ const height = swatchHeight(swatch)
225
+ return (
226
+ <svg
227
+ width={swatchWidth(swatch)}
228
+ height={height}
229
+ overflow="visible"
230
+ style={{ flex: 'none' }}
231
+ >
232
+ <g transform={`translate(0 ${height / 2 + 5})`}>
233
+ <SvgSwatch swatch={swatch} x={0} id={id} />
234
+ </g>
235
+ </svg>
236
+ )
237
+ }