@jbrowse/plugin-linear-genome-view 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { memo } from 'react';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
3
2
  import { getContainingView } from '@jbrowse/core/util';
4
3
  import { makeStyles } from '@jbrowse/core/util/tss-react';
5
4
  import { observer } from 'mobx-react';
@@ -25,12 +24,6 @@ const useStyles = makeStyles()({
25
24
  lineHeight: 1,
26
25
  },
27
26
  });
28
- const FixedLabel = memo(function FixedLabel({ text, color, isOverlay, featureLeftPx, featureId, subfeatureId, y, tooltip, labelClass, overlayClass, }) {
29
- return (_jsx("div", { "data-feature-id": featureId, "data-subfeature-id": subfeatureId, "data-tooltip": tooltip, className: isOverlay ? `${labelClass} ${overlayClass}` : labelClass, style: {
30
- color,
31
- transform: `translate(calc(${featureLeftPx}px - var(--offset-px)), ${y}px)`,
32
- }, children: text }));
33
- });
34
27
  function FloatingLabel({ text, color, isOverlay, featureLeftPx, featureRightPx, featureId, subfeatureId, labelWidth, y, offsetPx, tooltip, labelClass, overlayClass, }) {
35
28
  const x = calculateFloatingLabelPosition(featureLeftPx, featureRightPx, labelWidth, offsetPx);
36
29
  return (_jsx("div", { "data-feature-id": featureId, "data-subfeature-id": subfeatureId, "data-tooltip": tooltip, className: isOverlay ? `${labelClass} ${overlayClass}` : labelClass, style: { color, transform: `translate(${x}px,${y}px)` }, children: text }));
@@ -41,8 +34,7 @@ const FloatingLabels = observer(function FloatingLabels({ model, }) {
41
34
  const { offsetPx } = view;
42
35
  const featureLabels = model.floatingLabelData;
43
36
  const { onFeatureClick, onFeatureContextMenu, onMouseMove } = model.renderingProps();
44
- const fixedLabels = [];
45
- const floatingLabels = [];
37
+ const labels = [];
46
38
  for (const [key, { leftPx, topPx, totalFeatureHeight, floatingLabels: labelData, featureWidth, },] of featureLabels.entries()) {
47
39
  const featureVisualBottom = topPx + totalFeatureHeight;
48
40
  const featureRightPx = leftPx + featureWidth;
@@ -52,15 +44,10 @@ const FloatingLabels = observer(function FloatingLabels({ model, }) {
52
44
  const y = featureVisualBottom + relativeY;
53
45
  const labelKey = `${key}-${i}`;
54
46
  const featureId = parentFeatureId ?? key;
55
- if (labelWidth > featureWidth) {
56
- fixedLabels.push(_jsx(FixedLabel, { text: text, color: color, isOverlay: isOverlay ?? false, featureLeftPx: leftPx, featureId: featureId, subfeatureId: subfeatureId, y: y, tooltip: tooltip, labelClass: classes.label, overlayClass: classes.overlay }, labelKey));
57
- }
58
- else {
59
- floatingLabels.push(_jsx(FloatingLabel, { text: text, color: color, isOverlay: isOverlay ?? false, featureLeftPx: leftPx, featureRightPx: featureRightPx, featureId: featureId, subfeatureId: subfeatureId, labelWidth: labelWidth, y: y, offsetPx: offsetPx, tooltip: tooltip, labelClass: classes.label, overlayClass: classes.overlay }, labelKey));
60
- }
47
+ labels.push(_jsx(FloatingLabel, { text: text, color: color, isOverlay: isOverlay ?? false, featureLeftPx: leftPx, featureRightPx: featureRightPx, featureId: featureId, subfeatureId: subfeatureId, labelWidth: labelWidth, y: y, offsetPx: offsetPx, tooltip: tooltip, labelClass: classes.label, overlayClass: classes.overlay }, labelKey));
61
48
  }
62
49
  }
63
- return (_jsxs("div", { className: classes.container, style: { '--offset-px': `${offsetPx}px` }, onClick: e => {
50
+ return (_jsx("div", { className: classes.container, onClick: e => {
64
51
  const target = e.target;
65
52
  const subfeatureId = target.dataset.subfeatureId;
66
53
  const featureId = subfeatureId ?? target.dataset.featureId;
@@ -83,6 +70,6 @@ const FloatingLabels = observer(function FloatingLabels({ model, }) {
83
70
  onMouseMove?.(e, featureId, tooltip);
84
71
  model.setSubfeatureIdUnderMouse(subfeatureId);
85
72
  }
86
- }, children: [fixedLabels, floatingLabels] }));
73
+ }, children: labels }));
87
74
  });
88
75
  export default FloatingLabels;
@@ -80,10 +80,11 @@ export function deduplicateFeatureLabels(layoutFeatures, view, assembly, bpPerPx
80
80
  if (leftPx === undefined) {
81
81
  continue;
82
82
  }
83
+ const floorLeftPx = Math.floor(leftPx);
83
84
  const existing = featureLabels.get(key);
84
- if (!existing || leftPx < existing.leftPx) {
85
+ if (!existing || floorLeftPx < existing.leftPx) {
85
86
  featureLabels.set(key, {
86
- leftPx,
87
+ leftPx: floorLeftPx,
87
88
  topPx: effectiveTopPx,
88
89
  totalFeatureHeight,
89
90
  floatingLabels,
@@ -96,11 +97,11 @@ export function deduplicateFeatureLabels(layoutFeatures, view, assembly, bpPerPx
96
97
  export function calculateFloatingLabelPosition(featureLeftPx, featureRightPx, labelWidth, offsetPx) {
97
98
  const featureWidth = featureRightPx - featureLeftPx;
98
99
  if (labelWidth > featureWidth) {
99
- return featureLeftPx - offsetPx;
100
+ return Math.round(featureLeftPx - offsetPx);
100
101
  }
101
102
  const viewportLeft = Math.max(0, offsetPx);
102
103
  const leftPx = Math.max(featureLeftPx, viewportLeft);
103
104
  const naturalX = leftPx - offsetPx;
104
105
  const maxX = featureRightPx - offsetPx - labelWidth;
105
- return clamp(naturalX, 0, maxX);
106
+ return Math.round(clamp(naturalX, 0, maxX));
106
107
  }
@@ -1,6 +1,17 @@
1
1
  import { getSession, localStorageSetItem, parseLocString, } from '@jbrowse/core/util';
2
2
  import { addDisposer } from '@jbrowse/mobx-state-tree';
3
3
  import { autorun, when } from 'mobx';
4
+ function tryParseJson(s) {
5
+ try {
6
+ const v = JSON.parse(s);
7
+ return v && typeof v === 'object'
8
+ ? v
9
+ : undefined;
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ }
4
15
  export function setupInitAutorun(self) {
5
16
  addDisposer(self, autorun(async function initAutorun() {
6
17
  const { init, initialized } = self;
@@ -20,7 +31,14 @@ export function setupInitAutorun(self) {
20
31
  }
21
32
  try {
22
33
  if (init.loc) {
23
- await self.navToLocString(init.loc, init.assembly);
34
+ const asm = await assemblyManager.waitForAssembly(init.assembly);
35
+ if (!asm) {
36
+ throw new Error('Assembly not found');
37
+ }
38
+ await self.navToSearchString({
39
+ input: init.loc,
40
+ assembly: asm,
41
+ });
24
42
  }
25
43
  else {
26
44
  self.showAllRegionsInAssembly(init.assembly);
@@ -58,18 +76,45 @@ export function setupInitAutorun(self) {
58
76
  if (init.nav !== undefined) {
59
77
  self.setHideHeader(!init.nav);
60
78
  }
79
+ if (self.highlight.length) {
80
+ const fallback = self.assemblyNames[0];
81
+ if (fallback) {
82
+ const normalized = self.highlight.map(h => h.assemblyName ? h : { ...h, assemblyName: fallback });
83
+ if (normalized.some((h, i) => h !== self.highlight[i])) {
84
+ self.setHighlight(normalized);
85
+ }
86
+ }
87
+ }
61
88
  if (init.highlight) {
62
89
  for (const h of init.highlight) {
63
- const p = parseLocString(h, refName => assemblyManager.isValidRefName(refName, init.assembly));
64
- const { start, end } = p;
65
- if (start !== undefined && end !== undefined) {
90
+ const json = h.trim().startsWith('{') && tryParseJson(h);
91
+ if (json &&
92
+ typeof json.refName === 'string' &&
93
+ typeof json.start === 'number' &&
94
+ typeof json.end === 'number') {
66
95
  self.addToHighlights({
67
- ...p,
68
- start,
69
- end,
70
- assemblyName: init.assembly,
96
+ refName: json.refName,
97
+ start: json.start,
98
+ end: json.end,
99
+ assemblyName: typeof json.assemblyName === 'string'
100
+ ? json.assemblyName
101
+ : init.assembly,
102
+ color: typeof json.color === 'string' ? json.color : undefined,
103
+ label: typeof json.label === 'string' ? json.label : undefined,
71
104
  });
72
105
  }
106
+ else {
107
+ const p = parseLocString(h, refName => assemblyManager.isValidRefName(refName, init.assembly));
108
+ const { start, end } = p;
109
+ if (start !== undefined && end !== undefined) {
110
+ self.addToHighlights({
111
+ ...p,
112
+ start,
113
+ end,
114
+ assemblyName: init.assembly,
115
+ });
116
+ }
117
+ }
73
118
  }
74
119
  }
75
120
  self.setInit(undefined);
@@ -1,12 +1,8 @@
1
1
  import type { LinearGenomeViewModel } from '../model.ts';
2
+ import type { HighlightType } from '../types.ts';
2
3
  type LGV = LinearGenomeViewModel;
3
4
  declare const Highlight: ({ model, highlight, }: {
4
5
  model: LGV;
5
- highlight: {
6
- assemblyName: string;
7
- refName: string;
8
- start: number;
9
- end: number;
10
- };
6
+ highlight: HighlightType;
11
7
  }) => import("react/jsx-runtime").JSX.Element | null;
12
8
  export default Highlight;
@@ -1,90 +1,42 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRef, useState } from 'react';
3
- import { Menu } from '@jbrowse/core/ui';
2
+ import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton';
4
3
  import { getSession } from '@jbrowse/core/util';
5
4
  import { colord } from '@jbrowse/core/util/colord';
6
- import { makeStyles } from '@jbrowse/core/util/tss-react';
7
5
  import BookmarkIcon from '@mui/icons-material/Bookmark';
8
6
  import CloseIcon from '@mui/icons-material/Close';
9
7
  import LinkIcon from '@mui/icons-material/Link';
10
- import { IconButton, Tooltip } from '@mui/material';
8
+ import { Box, Tooltip, Typography, useTheme } from '@mui/material';
11
9
  import { observer } from 'mobx-react';
12
- const useStyles = makeStyles()(theme => ({
13
- highlight: {
14
- height: '100%',
15
- position: 'absolute',
16
- left: 0,
17
- overflow: 'hidden',
18
- background: colord(theme.palette.highlight.main).alpha(0.35).toRgbString(),
19
- },
20
- linkIcon: {
21
- color: colord(theme.palette.highlight.main).darken(0.2).toRgbString(),
22
- },
23
- z3: {
24
- zIndex: 3,
25
- },
26
- }));
10
+ import HighlightBand from "./HighlightBand.js";
27
11
  const Highlight = observer(function Highlight({ model, highlight, }) {
28
- const { classes } = useStyles();
29
- const [open, setOpen] = useState(false);
30
- const anchorEl = useRef(null);
12
+ const theme = useTheme();
31
13
  const session = getSession(model);
32
- const { assemblyManager } = session;
33
- const dismissHighlight = () => {
34
- model.removeHighlight(highlight);
35
- };
36
- function handleClose() {
37
- setOpen(false);
38
- }
39
- const mapCoords = (r) => {
40
- const s = model.bpToPx({
41
- refName: r.refName,
42
- coord: r.start,
43
- });
44
- const e = model.bpToPx({
45
- refName: r.refName,
46
- coord: r.end,
47
- });
48
- return s && e
49
- ? {
50
- width: Math.max(Math.abs(e.offsetPx - s.offsetPx), 3),
51
- left: Math.min(s.offsetPx, e.offsetPx) - model.offsetPx,
52
- }
53
- : undefined;
54
- };
55
- const asm = assemblyManager.get(highlight.assemblyName);
56
- const h = mapCoords({
57
- ...highlight,
58
- refName: asm?.getCanonicalRefName(highlight.refName) ?? highlight.refName,
59
- });
60
- return h ? (_jsxs("div", { className: classes.highlight, style: {
61
- transform: `translateX(${h.left}px)`,
62
- width: h.width,
63
- }, children: [_jsx(Tooltip, { title: "Highlighted from URL parameter", arrow: true, children: _jsx(IconButton, { ref: anchorEl, className: classes.z3, onClick: () => {
64
- setOpen(true);
65
- }, children: _jsx(LinkIcon, { fontSize: "small", className: classes.linkIcon }) }) }), _jsx(Menu, { anchorEl: anchorEl.current, onMenuItemClick: (_event, callback) => {
66
- callback();
67
- handleClose();
68
- }, open: open, onClose: handleClose, menuItems: [
69
- {
70
- label: 'Dismiss highlight',
71
- icon: CloseIcon,
72
- onClick: () => {
73
- dismissHighlight();
74
- },
14
+ const coords = model.getHighlightCoords(highlight);
15
+ const bandColor = highlight.color
16
+ ? colord(highlight.color)
17
+ : colord(theme.palette.highlight.main).alpha(0.35);
18
+ return coords ? (_jsx(HighlightBand, { coords: coords, background: bandColor.toRgbString(), children: _jsx(CascadingMenuButton, { menuItems: [
19
+ {
20
+ label: 'Dismiss highlight',
21
+ icon: CloseIcon,
22
+ onClick: () => {
23
+ model.removeHighlight(highlight);
75
24
  },
76
- {
77
- label: 'Bookmark highlighted region',
78
- icon: BookmarkIcon,
79
- onClick: () => {
80
- let bookmarkWidget = session.widgets.get('GridBookmark');
81
- if (!bookmarkWidget) {
82
- bookmarkWidget = session.addWidget('GridBookmarkWidget', 'GridBookmark');
83
- }
84
- bookmarkWidget.addBookmark(highlight);
85
- dismissHighlight();
86
- },
25
+ },
26
+ {
27
+ label: 'Bookmark highlighted region',
28
+ icon: BookmarkIcon,
29
+ onClick: () => {
30
+ const bookmarkWidget = (session.widgets.get('GridBookmark') ??
31
+ session.addWidget('GridBookmarkWidget', 'GridBookmark'));
32
+ bookmarkWidget.addBookmark({
33
+ ...highlight,
34
+ assemblyName: highlight.assemblyName ?? model.assemblyNames[0],
35
+ });
36
+ session.showWidget(bookmarkWidget);
37
+ model.removeHighlight(highlight);
87
38
  },
88
- ] })] })) : null;
39
+ },
40
+ ], children: _jsx(Tooltip, { title: highlight.label ?? 'Highlighted region', arrow: true, children: _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: [_jsx(LinkIcon, { fontSize: "small", sx: { color: bandColor.alpha(0.8).toRgbString() } }), highlight.label && model.labelsVisible ? (_jsx(Typography, { variant: "caption", noWrap: true, children: highlight.label })) : null] }) }) }) })) : null;
89
41
  });
90
42
  export default Highlight;
@@ -0,0 +1,8 @@
1
+ export default function HighlightBand({ coords, background, children, }: {
2
+ coords: {
3
+ left: number;
4
+ width: number;
5
+ };
6
+ background: string;
7
+ children?: React.ReactNode;
8
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { makeStyles } from '@jbrowse/core/util/tss-react';
3
+ const useStyles = makeStyles()({
4
+ highlight: {
5
+ height: '100%',
6
+ position: 'absolute',
7
+ left: 0,
8
+ overflow: 'hidden',
9
+ zIndex: 1,
10
+ pointerEvents: 'none',
11
+ },
12
+ chip: {
13
+ pointerEvents: 'auto',
14
+ },
15
+ });
16
+ export default function HighlightBand({ coords, background, children, }) {
17
+ const { classes } = useStyles();
18
+ return (_jsx("div", { className: classes.highlight, style: {
19
+ transform: `translateX(${coords.left}px)`,
20
+ width: coords.width,
21
+ background,
22
+ }, children: coords.width > 3 && children ? (_jsx("div", { className: classes.chip, children: children })) : null }));
23
+ }
@@ -20,7 +20,9 @@ const OverviewHighlight = observer(function OverviewHighlight({ model, overview,
20
20
  const { assemblyManager } = session;
21
21
  return highlight
22
22
  .map(r => {
23
- const asm = assemblyManager.get(r.assemblyName);
23
+ const asm = r.assemblyName
24
+ ? assemblyManager.get(r.assemblyName)
25
+ : undefined;
24
26
  const refName = asm?.getCanonicalRefName(r.refName) ?? r.refName;
25
27
  const s = overview.bpToPx({
26
28
  ...r,
@@ -1,8 +1,5 @@
1
1
  import type { AutocompleteRenderInputParams, TextFieldProps as TFP } from '@mui/material';
2
- export default function AutocompleteTextField({ TextFieldProps, inputBoxVal, params, setInputValue, setCurrentSearch, }: {
2
+ export default function AutocompleteTextField({ TextFieldProps, params, }: {
3
3
  TextFieldProps: TFP;
4
- inputBoxVal: string;
5
4
  params: AutocompleteRenderInputParams;
6
- setInputValue: (arg: string) => void;
7
- setCurrentSearch: (arg: string) => void;
8
5
  }): import("react/jsx-runtime").JSX.Element;
@@ -1,17 +1,13 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { TextField } from '@mui/material';
3
- export default function AutocompleteTextField({ TextFieldProps, inputBoxVal, params, setInputValue, setCurrentSearch, }) {
3
+ export default function AutocompleteTextField({ TextFieldProps, params, }) {
4
4
  const { helperText, slotProps = {} } = TextFieldProps;
5
5
  const { ref: inputRef, ...restInputProps } = params.InputProps;
6
6
  const { InputProps: _InputProps, ...restParams } = params;
7
- return (_jsx(TextField, { onBlur: () => {
8
- setInputValue(inputBoxVal);
9
- }, ...restParams, ...TextFieldProps, inputRef: inputRef, size: "small", helperText: helperText, slotProps: {
7
+ return (_jsx(TextField, { ...restParams, ...TextFieldProps, inputRef: inputRef, size: "small", helperText: helperText, slotProps: {
10
8
  input: {
11
9
  ...restInputProps,
12
10
  ...slotProps.input,
13
11
  },
14
- }, placeholder: "Search for location", onChange: e => {
15
- setCurrentSearch(e.target.value);
16
- } }));
12
+ }, placeholder: "Search for location" }));
17
13
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useState } from 'react';
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
3
  import BaseResult, { RefSequenceResult, } from '@jbrowse/core/TextSearch/BaseResults';
4
4
  import { getSession, measureText, useDebounce } from '@jbrowse/core/util';
5
5
  import { Autocomplete } from '@mui/material';
@@ -11,12 +11,14 @@ const RefNameAutocomplete = observer(function RefNameAutocomplete({ model, onSel
11
11
  const { assemblyManager } = session;
12
12
  const [open, setOpen] = useState(false);
13
13
  const [loaded, setLoaded] = useState(true);
14
- const [currentSearch, setCurrentSearch] = useState('');
15
14
  const [inputValue, setInputValue] = useState('');
15
+ const [searchQuery, setSearchQuery] = useState('');
16
16
  const [searchOptions, setSearchOptions] = useState();
17
- const debouncedSearch = useDebounce(currentSearch, 50);
17
+ const debouncedSearch = useDebounce(searchQuery, 50);
18
18
  const assembly = assemblyName ? assemblyManager.get(assemblyName) : undefined;
19
19
  const { coarseVisibleLocStrings, hasDisplayedRegions } = model;
20
+ const fetchResultsRef = useRef(fetchResults);
21
+ fetchResultsRef.current = fetchResults;
20
22
  useEffect(() => {
21
23
  const isCurrent = { cancelled: false };
22
24
  (async () => {
@@ -25,7 +27,7 @@ const RefNameAutocomplete = observer(function RefNameAutocomplete({ model, onSel
25
27
  return;
26
28
  }
27
29
  setLoaded(false);
28
- const results = await fetchResults(debouncedSearch);
30
+ const results = await fetchResultsRef.current(debouncedSearch);
29
31
  if (!isCurrent.cancelled) {
30
32
  setSearchOptions(getDeduplicatedResult(results));
31
33
  }
@@ -45,7 +47,7 @@ const RefNameAutocomplete = observer(function RefNameAutocomplete({ model, onSel
45
47
  return () => {
46
48
  isCurrent.cancelled = true;
47
49
  };
48
- }, [assemblyName, fetchResults, debouncedSearch, session]);
50
+ }, [assemblyName, debouncedSearch, session]);
49
51
  const inputBoxVal = coarseVisibleLocStrings || value || '';
50
52
  const regions = assembly?.regions;
51
53
  const regionOptions = useMemo(() => regions?.map(region => ({
@@ -59,16 +61,26 @@ const RefNameAutocomplete = observer(function RefNameAutocomplete({ model, onSel
59
61
  return (_jsx(Autocomplete, { "data-testid": "autocomplete", disableListWrap: true, disableClearable: true, disabled: !assemblyName, freeSolo: true, includeInputInList: true, selectOnFocus: true, style: {
60
62
  ...style,
61
63
  width: Math.min(Math.max(measureText(inputBoxVal, 14) + 100, minWidth), maxWidth),
62
- }, value: inputBoxVal, loading: !loaded, inputValue: inputValue, onInputChange: (_event, newInputValue) => {
64
+ }, value: inputBoxVal, loading: !loaded, inputValue: inputValue, onInputChange: (_event, newInputValue, reason) => {
63
65
  setInputValue(newInputValue);
64
- onChange?.(newInputValue);
66
+ if (reason === 'input') {
67
+ setSearchQuery(newInputValue);
68
+ onChange?.(newInputValue);
69
+ }
70
+ else if (reason === 'selectOption' || reason === 'clear') {
71
+ onChange?.(newInputValue);
72
+ setSearchQuery('');
73
+ }
74
+ else if (reason === 'blur') {
75
+ setSearchQuery('');
76
+ }
65
77
  }, loadingText: "loading results", open: open, onOpen: () => {
66
78
  setOpen(true);
67
79
  }, onClose: () => {
68
80
  setOpen(false);
69
81
  setLoaded(true);
70
82
  if (hasDisplayedRegions) {
71
- setCurrentSearch('');
83
+ setSearchQuery('');
72
84
  setSearchOptions(undefined);
73
85
  }
74
86
  }, onChange: (_event, selectedOption) => {
@@ -84,6 +96,6 @@ const RefNameAutocomplete = observer(function RefNameAutocomplete({ model, onSel
84
96
  onSelect?.(selectedOption.result);
85
97
  }
86
98
  setInputValue(inputBoxVal);
87
- }, options: searchOptions?.length ? searchOptions : regionOptions, getOptionDisabled: option => option.group === 'limitOption', filterOptions: (opts, { inputValue }) => getFiltered(opts, inputValue), renderInput: params => (_jsx(AutocompleteTextField, { params: params, inputBoxVal: inputBoxVal, TextFieldProps: TextFieldProps, setCurrentSearch: setCurrentSearch, setInputValue: setInputValue })), getOptionLabel: opt => typeof opt === 'string' ? opt : opt.result.getDisplayString() }));
99
+ }, options: searchOptions?.length ? searchOptions : regionOptions, getOptionDisabled: option => option.group === 'limitOption', filterOptions: opts => getFiltered(opts, searchQuery), renderInput: params => (_jsx(AutocompleteTextField, { params: params, TextFieldProps: TextFieldProps })), getOptionLabel: opt => typeof opt === 'string' ? opt : opt.result.getDisplayString() }));
88
100
  });
89
101
  export default RefNameAutocomplete;
@@ -44,6 +44,6 @@ const TracksContainer = observer(function TracksContainer({ children, model, })
44
44
  } }) }), _jsx(HighlightGroup, { model: model }), additional, children] }));
45
45
  });
46
46
  const HighlightGroup = observer(function HighlightGroup({ model, }) {
47
- return model.highlight.length ? (_jsx(Suspense, { fallback: null, children: model.highlight.map((highlight, idx) => (_jsx(Highlight, { model: model, highlight: highlight }, `${JSON.stringify(highlight)}-${idx}`))) })) : null;
47
+ return model.highlightsVisible && model.highlight.length ? (_jsx(Suspense, { fallback: null, children: model.highlight.map((highlight, idx) => (_jsx(Highlight, { model: model, highlight: highlight }, `${JSON.stringify(highlight)}-${idx}`))) })) : null;
48
48
  });
49
49
  export default TracksContainer;
@@ -2,3 +2,4 @@ import type PluginManager from '@jbrowse/core/PluginManager';
2
2
  export default function LinearGenomeViewF(pluginManager: PluginManager): void;
3
3
  export * from './model.ts';
4
4
  export * from './types.ts';
5
+ export { default as HighlightBand } from './components/HighlightBand.tsx';
@@ -13,3 +13,4 @@ export default function LinearGenomeViewF(pluginManager) {
13
13
  }
14
14
  export * from "./model.js";
15
15
  export * from "./types.js";
16
+ export { default as HighlightBand } from "./components/HighlightBand.js";
@@ -28,6 +28,8 @@ export declare function stateModelFactory(pluginManager: PluginManager): import(
28
28
  trackLabels: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
29
29
  showGridlines: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
30
30
  highlight: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").IArrayType<import("@jbrowse/mobx-state-tree").IType<HighlightType, HighlightType, HighlightType>>, [undefined]>;
31
+ highlightsVisible: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
32
+ labelsVisible: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
31
33
  colorByCDS: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
32
34
  showTrackOutlines: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
33
35
  init: import("@jbrowse/mobx-state-tree").IType<InitState | undefined, InitState | undefined, InitState | undefined>;
@@ -119,6 +121,9 @@ export declare function stateModelFactory(pluginManager: PluginManager): import(
119
121
  addToHighlights(highlight: HighlightType): void;
120
122
  setHighlight(highlight?: HighlightType[]): void;
121
123
  removeHighlight(highlight: HighlightType): void;
124
+ updateHighlight(old: HighlightType, updates: Partial<HighlightType>): void;
125
+ setHighlightsVisible(arg: boolean): void;
126
+ setLabelsVisible(arg: boolean): void;
122
127
  setVolatileGuides(guides: VolatileGuide[]): void;
123
128
  scrollTo(offsetPx: number): number;
124
129
  zoomTo(bpPerPx: number, offset?: number, centerAtOffset?: boolean): number;
@@ -199,6 +204,15 @@ export declare function stateModelFactory(pluginManager: PluginManager): import(
199
204
  index: number;
200
205
  offsetPx: number;
201
206
  } | undefined;
207
+ getHighlightCoords(region: {
208
+ assemblyName?: string;
209
+ refName: string;
210
+ start: number;
211
+ end: number;
212
+ }): {
213
+ width: number;
214
+ left: number;
215
+ } | undefined;
202
216
  centerAt(coord: number, refName: string, regionNumber?: number): void;
203
217
  pxToBp(px: number): {
204
218
  coord: number;
@@ -248,6 +262,8 @@ export declare function stateModelFactory(pluginManager: PluginManager): import(
248
262
  trackLabels: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
249
263
  showGridlines: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
250
264
  highlight: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").IArrayType<import("@jbrowse/mobx-state-tree").IType<HighlightType, HighlightType, HighlightType>>, [undefined]>;
265
+ highlightsVisible: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
266
+ labelsVisible: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
251
267
  colorByCDS: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
252
268
  showTrackOutlines: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
253
269
  init: import("@jbrowse/mobx-state-tree").IType<InitState | undefined, InitState | undefined, InitState | undefined>;
@@ -271,6 +287,8 @@ export declare function stateModelFactory(pluginManager: PluginManager): import(
271
287
  trackLabels: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
272
288
  showGridlines: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
273
289
  highlight: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").IArrayType<import("@jbrowse/mobx-state-tree").IType<HighlightType, HighlightType, HighlightType>>, [undefined]>;
290
+ highlightsVisible: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
291
+ labelsVisible: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
274
292
  colorByCDS: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
275
293
  showTrackOutlines: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
276
294
  init: import("@jbrowse/mobx-state-tree").IType<InitState | undefined, InitState | undefined, InitState | undefined>;
@@ -42,6 +42,8 @@ export function stateModelFactory(pluginManager) {
42
42
  trackLabels: types.optional(types.string, () => localStorageGetItem('lgv-trackLabels') || ''),
43
43
  showGridlines: true,
44
44
  highlight: types.optional(types.array(types.frozen()), []),
45
+ highlightsVisible: types.optional(types.boolean, true),
46
+ labelsVisible: types.optional(types.boolean, true),
45
47
  colorByCDS: types.optional(types.boolean, () => localStorageGetBoolean('lgv-colorByCDS', false)),
46
48
  showTrackOutlines: types.optional(types.boolean, () => localStorageGetBoolean('lgv-showTrackOutlines', true)),
47
49
  init: types.frozen(),
@@ -351,6 +353,18 @@ export function stateModelFactory(pluginManager) {
351
353
  removeHighlight(highlight) {
352
354
  self.highlight.remove(highlight);
353
355
  },
356
+ updateHighlight(old, updates) {
357
+ const idx = self.highlight.indexOf(old);
358
+ if (idx !== -1) {
359
+ self.highlight.splice(idx, 1, { ...old, ...updates });
360
+ }
361
+ },
362
+ setHighlightsVisible(arg) {
363
+ self.highlightsVisible = arg;
364
+ },
365
+ setLabelsVisible(arg) {
366
+ self.labelsVisible = arg;
367
+ },
354
368
  setVolatileGuides(guides) {
355
369
  self.volatileGuides = guides;
356
370
  },
@@ -826,6 +840,21 @@ export function stateModelFactory(pluginManager) {
826
840
  bpToPx({ refName, coord, regionNumber, }) {
827
841
  return bpToPx({ refName, coord, regionNumber, self });
828
842
  },
843
+ getHighlightCoords(region) {
844
+ const { assemblyManager } = getSession(self);
845
+ const asm = region.assemblyName
846
+ ? assemblyManager.get(region.assemblyName)
847
+ : undefined;
848
+ const refName = asm?.getCanonicalRefName(region.refName) ?? region.refName;
849
+ const s = this.bpToPx({ refName, coord: region.start });
850
+ const e = this.bpToPx({ refName, coord: region.end });
851
+ return s && e
852
+ ? {
853
+ width: Math.max(Math.abs(e.offsetPx - s.offsetPx), 3),
854
+ left: Math.min(s.offsetPx, e.offsetPx) - self.offsetPx,
855
+ }
856
+ : undefined;
857
+ },
829
858
  centerAt(coord, refName, regionNumber) {
830
859
  const centerPx = this.bpToPx({
831
860
  refName,
@@ -877,7 +906,7 @@ export function stateModelFactory(pluginManager) {
877
906
  if (!snap) {
878
907
  return snap;
879
908
  }
880
- const { init, offsetPx, bpPerPx, hideHeader, hideHeaderOverview, hideNoTracksActive, showGridlines, trackSelectorType, displayedRegions, highlight, showCenterLine, showCytobandsSetting, trackLabels, colorByCDS, showTrackOutlines, ...rest } = snap;
909
+ const { init, offsetPx, bpPerPx, hideHeader, hideHeaderOverview, hideNoTracksActive, showGridlines, trackSelectorType, displayedRegions, highlight, showCenterLine, showCytobandsSetting, trackLabels, colorByCDS, showTrackOutlines, highlightsVisible, labelsVisible, ...rest } = snap;
881
910
  return {
882
911
  ...rest,
883
912
  ...(offsetPx ? { offsetPx } : {}),
@@ -894,6 +923,8 @@ export function stateModelFactory(pluginManager) {
894
923
  ...(trackLabels ? { trackLabels } : {}),
895
924
  ...(colorByCDS ? { colorByCDS } : {}),
896
925
  ...(!showTrackOutlines ? { showTrackOutlines } : {}),
926
+ ...(!highlightsVisible ? { highlightsVisible } : {}),
927
+ ...(!labelsVisible ? { labelsVisible } : {}),
897
928
  };
898
929
  });
899
930
  }
@@ -29,8 +29,10 @@ export interface ExportSvgOptions {
29
29
  export interface HighlightType {
30
30
  start: number;
31
31
  end: number;
32
- assemblyName: string;
32
+ assemblyName?: string;
33
33
  refName: string;
34
+ color?: string;
35
+ label?: string;
34
36
  }
35
37
  export interface NavLocation {
36
38
  refName: string;
package/esm/index.d.ts CHANGED
@@ -1245,7 +1245,7 @@ export type { BaseLinearDisplayModel, BlockModel, ExportSvgDisplayOptions, Float
1245
1245
  export { configSchemaFactory as linearBareDisplayConfigSchemaFactory, stateModelFactory as linearBareDisplayStateModelFactory, } from './LinearBareDisplay/index.ts';
1246
1246
  export { BaseLinearDisplay, BaseLinearDisplayComponent, BlockMsg, FeatureDensityMixin, FloatingLegend, NonBlockCanvasDisplayComponent, NonBlockCanvasDisplayMixin, SVGLegend, TooLargeMessage, TrackHeightMixin, baseLinearDisplayConfigSchema, calculateSvgLegendWidth, createSubfeatureLabelMetadata, drawCanvasImageData, } from './BaseLinearDisplay/index.ts';
1247
1247
  export type { NonBlockCanvasDisplayMixinType, NonBlockCanvasDisplayModel, } from './BaseLinearDisplay/index.ts';
1248
- export { type LinearGenomeViewModel, type LinearGenomeViewStateModel, RefNameAutocomplete, SearchBox, } from './LinearGenomeView/index.ts';
1248
+ export { HighlightBand, type LinearGenomeViewModel, type LinearGenomeViewStateModel, RefNameAutocomplete, SearchBox, } from './LinearGenomeView/index.ts';
1249
1249
  export { fetchResults } from './searchUtils.ts';
1250
1250
  export type { BpOffset, ExportSvgOptions, HighlightType, InitState, NavLocation, VolatileGuide, } from './LinearGenomeView/types.ts';
1251
1251
  export { SVGGridlines, SVGRuler, SVGTracks, renderToSvg, } from './LinearGenomeView/svgcomponents/SVGLinearGenomeView.tsx';
package/esm/index.js CHANGED
@@ -54,7 +54,7 @@ export default class LinearGenomeViewPlugin extends Plugin {
54
54
  }
55
55
  export { configSchemaFactory as linearBareDisplayConfigSchemaFactory, stateModelFactory as linearBareDisplayStateModelFactory, } from "./LinearBareDisplay/index.js";
56
56
  export { BaseLinearDisplay, BaseLinearDisplayComponent, BlockMsg, FeatureDensityMixin, FloatingLegend, NonBlockCanvasDisplayComponent, NonBlockCanvasDisplayMixin, SVGLegend, TooLargeMessage, TrackHeightMixin, baseLinearDisplayConfigSchema, calculateSvgLegendWidth, createSubfeatureLabelMetadata, drawCanvasImageData, } from "./BaseLinearDisplay/index.js";
57
- export { RefNameAutocomplete, SearchBox, } from "./LinearGenomeView/index.js";
57
+ export { HighlightBand, RefNameAutocomplete, SearchBox, } from "./LinearGenomeView/index.js";
58
58
  export { fetchResults } from "./searchUtils.js";
59
59
  export { SVGGridlines, SVGRuler, SVGTracks, renderToSvg, } from "./LinearGenomeView/svgcomponents/SVGLinearGenomeView.js";
60
60
  export { totalHeight } from "./LinearGenomeView/svgcomponents/util.js";
@@ -58,11 +58,13 @@ export async function fetchResults({ queryString, searchType, searchScope, rankS
58
58
  queryString,
59
59
  searchType,
60
60
  }, searchScope, rankSearchResults);
61
- const refNameResults = assembly?.allRefNames
62
- ?.filter(ref => ref.toLowerCase().startsWith(queryString.toLowerCase()))
63
- .slice(0, 10)
64
- .map(r => new BaseResult({ label: r }));
65
- return dedupe([...(refNameResults || []), ...(textSearchResults || [])], elt => elt.getId());
61
+ const refNameResults = [
62
+ ...new Set(assembly?.allRefNames
63
+ ?.filter(ref => ref.toLowerCase().startsWith(queryString.toLowerCase()))
64
+ .map(ref => assembly.getCanonicalRefName(ref) ?? ref)
65
+ .slice(0, 10)),
66
+ ].map(r => new BaseResult({ label: r, matchedObject: { refName: r } }));
67
+ return dedupe([...refNameResults, ...(textSearchResults ?? [])], elt => elt.getId());
66
68
  }
67
69
  export function splitLast(str, split) {
68
70
  const lastIndex = str.lastIndexOf(split);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/plugin-linear-genome-view",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "type": "module",
5
5
  "description": "JBrowse 2 linear genome view",
6
6
  "keywords": [
@@ -22,15 +22,15 @@
22
22
  ],
23
23
  "dependencies": {
24
24
  "@jbrowse/mobx-state-tree": "^5.6.0",
25
- "@mui/icons-material": "^7.3.10",
26
- "@mui/material": "^7.3.10",
25
+ "@mui/icons-material": "^7.3.11",
26
+ "@mui/material": "^7.3.11",
27
27
  "@types/file-saver-es": "^2.0.3",
28
28
  "copy-to-clipboard": "^3.3.3",
29
29
  "file-saver-es": "^2.0.5",
30
- "mobx": "^6.15.0",
30
+ "mobx": "^6.15.3",
31
31
  "mobx-react": "^9.2.1",
32
- "@jbrowse/core": "^4.2.0",
33
- "@jbrowse/product-core": "^4.2.0"
32
+ "@jbrowse/product-core": "^4.3.0",
33
+ "@jbrowse/core": "^4.3.0"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": ">=18.0.0",