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.
- package/dist/chunks/{GraphGenomeView-NJ5XCREB.js → GraphGenomeView-PSDUS64B.js} +2 -2
- package/dist/chunks/{HighlightColorDialog-UK5VDDV6.js → HighlightColorDialog-2MP3C5AK.js} +2 -2
- package/dist/chunks/{LinearGraphDisplay-TXTKAACH.js → LinearGraphDisplay-FTFJFNKT.js} +2 -2
- package/dist/chunks/{WalkTabixSyntenyAdapter-U53JLYVU.js → WalkTabixSyntenyAdapter-GH6NYQTF.js} +2 -2
- package/dist/chunks/{WalkTabixSyntenyAdapter-U53JLYVU.js.map → WalkTabixSyntenyAdapter-GH6NYQTF.js.map} +1 -1
- package/dist/chunks/chunk-HM57SCYO.js +4 -0
- package/dist/chunks/chunk-HM57SCYO.js.map +7 -0
- package/dist/chunks/{chunk-WEGWTC52.js → chunk-LELA7BG7.js} +2 -2
- package/dist/chunks/{chunk-WEGWTC52.js.map → chunk-LELA7BG7.js.map} +4 -4
- package/dist/chunks/chunk-NP42Y2MV.js +2 -0
- package/dist/chunks/chunk-NP42Y2MV.js.map +7 -0
- package/dist/chunks/chunk-PJ43KEV6.js +2 -0
- package/dist/chunks/chunk-PJ43KEV6.js.map +7 -0
- package/dist/chunks/chunk-RP3L3Z5W.js +3 -0
- package/dist/chunks/chunk-RP3L3Z5W.js.map +7 -0
- package/dist/chunks/{chunk-FITPB5X6.js → chunk-VQUMZEGN.js} +2 -2
- package/dist/chunks/renderSvg-V4OYQTQZ.js +2 -0
- package/dist/chunks/renderSvg-V4OYQTQZ.js.map +7 -0
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +4 -4
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
- package/package.json +2 -2
- package/src/GbzBaseSyntenyAdapter/configSchema.ts +2 -2
- package/src/GetSubgraph.ts +1 -1
- package/src/GraphAddTrackWorkflow/buildTrackConfig.test.ts +2 -2
- package/src/GraphGenomeView/components/GraphCanvas.test.tsx +108 -0
- package/src/GraphGenomeView/components/GraphCanvas.tsx +11 -8
- package/src/GraphGenomeView/components/Legend.tsx +3 -121
- package/src/GraphGenomeView/components/LegendSwatch.tsx +237 -0
- package/src/GraphGenomeView/components/PaneSvg.tsx +3 -190
- package/src/GraphGenomeView/components/WalkStrip.tsx +1 -1
- package/src/GraphGenomeView/model.test.ts +13 -5
- package/src/GraphGenomeView/pane/graphViews.ts +10 -0
- package/src/GraphGenomeView/pane/loadActions.ts +5 -1
- package/src/GraphGenomeView/pane/morph.test.ts +34 -1
- package/src/GraphGenomeView/pane/morph.ts +74 -5
- package/src/GraphGenomeView/pane/renderingActions.ts +1 -1
- package/src/GraphGenomeView/pane/settingActions.ts +10 -43
- package/src/GraphGenomeView/subgraphLoad.test.ts +56 -1
- package/src/GraphGenomeView/viewModel.ts +55 -33
- package/src/GraphGenomeView/walkRowSpans.ts +4 -10
- package/src/GraphTrack/index.ts +2 -2
- package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +12 -2
- package/src/LinearGraphDisplay/denseWindow.ts +0 -14
- package/src/LinearGraphDisplay/model.ts +2 -1
- package/src/RgfaTabixAdapter/walkFiles.test.ts +1 -0
- package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +1 -1
- package/src/graphTrackDefaults/graphTrackDefaults.test.ts +15 -3
- package/src/graphTrackDefaults/index.ts +10 -5
- package/src/launchFromGraph/launchFromGraph.test.ts +7 -10
- package/src/launchFromGraph/launchFromGraph.ts +26 -2
- package/src/nodeLimitError.ts +12 -0
- package/src/panSNAliases/panSNAliases.test.ts +129 -3
- package/src/panSNAliases/trackAdapterConfig.ts +55 -11
- package/src/version.ts +1 -1
- package/dist/chunks/chunk-2MCOY3YG.js +0 -3
- package/dist/chunks/chunk-2MCOY3YG.js.map +0 -7
- package/dist/chunks/chunk-MURN3JPK.js +0 -2
- package/dist/chunks/chunk-MURN3JPK.js.map +0 -7
- package/dist/chunks/chunk-NWJYLP6Z.js +0 -4
- package/dist/chunks/chunk-NWJYLP6Z.js.map +0 -7
- package/dist/chunks/chunk-WCGVUZWP.js +0 -2
- package/dist/chunks/chunk-WCGVUZWP.js.map +0 -7
- package/dist/chunks/renderSvg-KSQI5DH6.js +0 -2
- package/dist/chunks/renderSvg-KSQI5DH6.js.map +0 -7
- /package/dist/chunks/{GraphGenomeView-NJ5XCREB.js.map → GraphGenomeView-PSDUS64B.js.map} +0 -0
- /package/dist/chunks/{HighlightColorDialog-UK5VDDV6.js.map → HighlightColorDialog-2MP3C5AK.js.map} +0 -0
- /package/dist/chunks/{LinearGraphDisplay-TXTKAACH.js.map → LinearGraphDisplay-FTFJFNKT.js.map} +0 -0
- /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.
|
|
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
|
|
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
|
|
26
|
-
*
|
|
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
|
package/src/GetSubgraph.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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(
|
|
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
|
|
156
|
-
// and the refit clears the hover that grew
|
|
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
|
|
170
|
-
if (isAlive(model) && model.
|
|
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 {
|
|
5
|
+
import { LegendSwatchView } from './LegendSwatch'
|
|
7
6
|
import { legendBoxStyle, legendNoteStyle, legendRowStyle } from './legendStyles'
|
|
7
|
+
import { legendSections } from '../legendSections'
|
|
8
8
|
|
|
9
|
-
import type { LegendRow
|
|
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
|
+
}
|