@jbrowse/core 5.0.0-beta.4 → 5.0.0-beta.7

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 (110) hide show
  1. package/README.md +278 -250
  2. package/esm/PluginManager.js +1 -1
  3. package/esm/ReExports/publicUi.d.ts +1 -1
  4. package/esm/ReExports/publicUi.js +1 -1
  5. package/esm/ReExports/publicUtil.d.ts +1 -1
  6. package/esm/ReExports/publicUtil.js +1 -1
  7. package/esm/ReExports/workerNamespaceNames.js +3 -9
  8. package/esm/assemblyManager/assemblyConfigSchema.d.ts +2 -1
  9. package/esm/assemblyManager/assemblyConfigSchema.js +2 -1
  10. package/esm/assemblyManager/expandAssemblyConfigShorthand.d.ts +15 -15
  11. package/esm/assemblyManager/expandAssemblyConfigShorthand.js +34 -18
  12. package/esm/configuration/configurationSchema.d.ts +1 -2
  13. package/esm/configuration/configurationSchema.js +3 -8
  14. package/esm/configuration/configurationSlot.d.ts +0 -47
  15. package/esm/configuration/configurationSlot.js +4 -54
  16. package/esm/configuration/fillLocations.d.ts +14 -0
  17. package/esm/configuration/fillLocations.js +14 -0
  18. package/esm/configuration/fullConfSnapshot.d.ts +0 -15
  19. package/esm/configuration/fullConfSnapshot.js +0 -28
  20. package/esm/configuration/getConf.d.ts +4 -34
  21. package/esm/configuration/getConf.js +3 -38
  22. package/esm/configuration/index.d.ts +3 -4
  23. package/esm/configuration/index.js +3 -2
  24. package/esm/configuration/schemaRegistry.d.ts +1 -2
  25. package/esm/configuration/schemaRegistry.js +1 -2
  26. package/esm/configuration/slotFacade.d.ts +1 -16
  27. package/esm/configuration/slotFacade.js +1 -2
  28. package/esm/configuration/tabixShorthand.js +3 -3
  29. package/esm/configuration/types.d.ts +4 -16
  30. package/esm/data_adapters/CytobandAdapter/configSchema.js +3 -4
  31. package/esm/pluggableElementTypes/DisplayType.d.ts +5 -3
  32. package/esm/pluggableElementTypes/DisplayType.js +3 -1
  33. package/esm/pluggableElementTypes/models/BaseTrackModel.d.ts +30 -0
  34. package/esm/pluggableElementTypes/models/BaseTrackModel.js +45 -17
  35. package/esm/rpc/RpcRegistry.d.ts +7 -1
  36. package/esm/rpc/coreRpcMethods.d.ts +1 -0
  37. package/esm/rpc/coreRpcMethods.js +1 -0
  38. package/esm/rpc/methods/CoreEncodeFeatures.d.ts +15 -0
  39. package/esm/rpc/methods/CoreEncodeFeatures.js +62 -0
  40. package/esm/svg/svgId.d.ts +1 -1
  41. package/esm/svg/svgId.js +1 -1
  42. package/esm/ui/LegendSwatchGlyph.js +26 -21
  43. package/esm/ui/MenuTypes.d.ts +5 -52
  44. package/esm/ui/SizeSliderRow.d.ts +1 -3
  45. package/esm/ui/SizeSliderRow.js +4 -6
  46. package/esm/ui/SvgColorLegend.d.ts +9 -3
  47. package/esm/ui/SvgColorLegend.js +74 -33
  48. package/esm/ui/colorScale.d.ts +64 -0
  49. package/esm/ui/colorScale.js +41 -0
  50. package/esm/ui/colors.d.ts +11 -0
  51. package/esm/ui/colors.js +7 -0
  52. package/esm/ui/index.d.ts +5 -7
  53. package/esm/ui/index.js +3 -13
  54. package/esm/ui/legendMenuItem.d.ts +5 -16
  55. package/esm/ui/legendMenuItem.js +3 -14
  56. package/esm/ui/legendSpec.d.ts +11 -5
  57. package/esm/ui/legendSpec.js +8 -11
  58. package/esm/ui/makeSizeMenu.d.ts +0 -6
  59. package/esm/ui/makeSizeMenu.js +6 -30
  60. package/esm/ui/measureLegendText.js +1 -1
  61. package/esm/ui/menuItemAdornment.d.ts +9 -19
  62. package/esm/ui/menuItemAdornment.js +10 -31
  63. package/esm/ui/menuItems.d.ts +2 -6
  64. package/esm/ui/menuItems.js +0 -4
  65. package/esm/ui/palette.js +2 -1
  66. package/esm/ui/toggleMenuItems.d.ts +6 -13
  67. package/esm/ui/toggleMenuItems.js +13 -29
  68. package/esm/util/colorRamp.d.ts +3 -3
  69. package/esm/util/colorRamp.js +2 -2
  70. package/esm/util/featureTransforms.d.ts +15 -0
  71. package/esm/util/featureTransforms.js +342 -0
  72. package/esm/util/freezeDeep.d.ts +8 -10
  73. package/esm/util/freezeDeep.js +8 -10
  74. package/esm/util/glyphNames.d.ts +8 -0
  75. package/esm/util/glyphNames.js +12 -0
  76. package/esm/util/index.d.ts +0 -1
  77. package/esm/util/index.js +0 -1
  78. package/esm/util/markEncoding.d.ts +76 -0
  79. package/esm/util/markEncoding.js +436 -0
  80. package/esm/util/markEncodingTypes.d.ts +331 -0
  81. package/esm/util/markEncodingTypes.js +8 -0
  82. package/esm/util/renderToStaticMarkup.js +1 -1
  83. package/esm/util/starFusion.d.ts +26 -0
  84. package/esm/util/starFusion.js +37 -0
  85. package/esm/util/trackConfigDelta.d.ts +4 -8
  86. package/esm/util/trackConfigDelta.js +2 -4
  87. package/esm/util/tracks.d.ts +7 -1
  88. package/esm/util/tracks.js +72 -7
  89. package/esm/util/types/index.d.ts +12 -4
  90. package/esm/util/whenViewSettled.d.ts +9 -0
  91. package/esm/util/whenViewSettled.js +12 -4
  92. package/package.json +13 -3
  93. package/esm/configuration/promotableDefaults.d.ts +0 -234
  94. package/esm/configuration/promotableDefaults.js +0 -395
  95. package/esm/configuration/promotablePin.d.ts +0 -65
  96. package/esm/configuration/promotablePin.js +0 -1
  97. package/esm/configuration/promotableResolve.d.ts +0 -108
  98. package/esm/configuration/promotableResolve.js +0 -117
  99. package/esm/configuration/promotableSlots.d.ts +0 -12
  100. package/esm/configuration/promotableSlots.js +0 -45
  101. package/esm/configuration/slotShape.d.ts +0 -12
  102. package/esm/configuration/slotShape.js +0 -84
  103. package/esm/ui/PinAdornment.d.ts +0 -5
  104. package/esm/ui/PinAdornment.js +0 -67
  105. package/esm/ui/SvgGradientLegend.d.ts +0 -26
  106. package/esm/ui/SvgGradientLegend.js +0 -26
  107. package/esm/ui/promotablePinCoverage.d.ts +0 -58
  108. package/esm/ui/promotablePinCoverage.js +0 -124
  109. package/esm/util/openDisplays.d.ts +0 -23
  110. package/esm/util/openDisplays.js +0 -79
@@ -1,25 +1,30 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- // The one place that decides what each `LegendMark` looks like. Returns bare SVG
3
- // shapes rather than a rooted <svg>, so the floating legend (which wraps them in
4
- // an inline <svg>) and the SVG export (which drops them into a row <g>) draw the
5
- // identical glyph — a display's on-screen key and its exported figure disagreeing
6
- // about which color is a curve would be worse than neither having marks.
2
+ import { appendGlyph } from '@jbrowse/render-core/marks/glyphPaint';
3
+ import { DIAMOND_GLYPH_SCALE } from '@jbrowse/render-core/shaders/pointMarkConsts';
4
+ import { GLYPH_CODES } from '../util/glyphNames.js';
5
+ // The point shape's own painter, recorded as SVG path data, so the key draws
6
+ // the glyph the plot draws. The diamond paints wider than its diameter and
7
+ // is scaled down to fit the box.
8
+ function glyphPathData(glyph, size, x, y) {
9
+ const d = [];
10
+ const path = {
11
+ moveTo: (px, py) => d.push(`M${px} ${py}`),
12
+ lineTo: (px, py) => d.push(`L${px} ${py}`),
13
+ closePath: () => d.push('Z'),
14
+ rect: (px, py, w, h) => d.push(`M${px} ${py}h${w}v${h}h${-w}Z`),
15
+ arc: (cx, cy, r) => d.push(`A${r} ${r} 0 1 0 ${cx - r} ${cy}A${r} ${r} 0 1 0 ${cx + r} ${cy}Z`),
16
+ };
17
+ const diameter = glyph === 'diamond' ? size / DIAMOND_GLYPH_SCALE : size;
18
+ appendGlyph(path, GLYPH_CODES[glyph], x + size / 2, y + size / 2, diameter);
19
+ return d.join('');
20
+ }
21
+ // The one place that decides what a legend swatch looks like. Returns a bare
22
+ // shape rather than a rooted <svg>, so the floating legend (which wraps it in an
23
+ // inline <svg>) and the SVG export (which drops it into a row <g>) draw the
24
+ // identical box.
7
25
  //
8
- // Every shape fills the `size` box from (x, y) so a caller can lay swatches out
9
- // on a fixed pitch without knowing which mark it is placing.
26
+ // The shape fills the `size` box from (x, y) so a caller can lay swatches out on
27
+ // a fixed pitch.
10
28
  export function LegendSwatchGlyph({ swatch, size, x = 0, y = 0, }) {
11
- const { color, mark = 'fill' } = swatch;
12
- // Thin enough to read as a stroke next to a solid block, thick enough to
13
- // carry a color at 10-12px.
14
- const strokeWidth = Math.max(2, Math.round(size / 5));
15
- if (mark === 'line') {
16
- return (_jsx("line", { x1: x, x2: x + size, y1: y + size / 2, y2: y + size / 2, stroke: color, strokeWidth: strokeWidth }));
17
- }
18
- if (mark === 'curve') {
19
- // An arc bulging up out of the box's lower corners, which is the shape of
20
- // the connector itself — a pair of endpoints with the curve between them.
21
- const inset = strokeWidth / 2;
22
- return (_jsx("path", { d: `M${x},${y + size - inset} Q${x + size / 2},${y - size / 3} ${x + size},${y + size - inset}`, fill: "none", stroke: color, strokeWidth: strokeWidth }));
23
- }
24
- return _jsx("rect", { x: x, y: y, width: size, height: size, fill: color });
29
+ return swatch.glyph ? (_jsx("path", { d: glyphPathData(swatch.glyph, size, x, y), fill: swatch.color })) : (_jsx("rect", { x: x, y: y, width: size, height: size, fill: swatch.color }));
25
30
  }
@@ -1,4 +1,3 @@
1
- import type { Pin } from '../configuration/promotablePin.ts';
2
1
  /** #menuItem divider | a horizontal rule; not clickable */
3
2
  export interface MenuDivider {
4
3
  priority?: number;
@@ -51,7 +50,7 @@ export interface BaseMenuItem {
51
50
  */
52
51
  keepMenuOpen?: boolean;
53
52
  /**
54
- * #menuField arbitrary trailing content; prefer `pin`
53
+ * #menuField arbitrary trailing content
55
54
  *
56
55
  * Extra content rendered at the trailing (right) edge of the row, after the
57
56
  * checkbox/radio decoration and help icon — e.g. a secondary toggle. The
@@ -59,56 +58,10 @@ export interface BaseMenuItem {
59
58
  * onClick or dismiss the menu.
60
59
  *
61
60
  * An **element**, so a module that sets it drags React and whatever it renders
62
- * into its own graph. That is fine for the one-off it exists for (synteny's
63
- * color swatch) and wrong for the common case, which is why the pin below is
64
- * a description instead. Prefer `pin`; reach for this only when the
65
- * content is genuinely arbitrary.
61
+ * into its own graph. Reach for it only when the content is genuinely
62
+ * arbitrary — synteny's and wiggle's colour swatches are what it exists for.
66
63
  */
67
64
  endAdornment?: React.ReactNode;
68
- /**
69
- * #menuField the "apply this to all open tracks of this type" pin; set it through a row builder's `pin` option
70
- *
71
- * The trailing "default for all tracks of this type" pin, as a **description**
72
- * rather than an element — the renderer builds `PinAdornment` from
73
- * it. Same rule as a `TrackControlProps` icon name (reference/DISPLAYCHROME.md):
74
- * menu-item builders are called from state models and menu modules, which are
75
- * eager, so an element here puts MUI's `ToggleButton`, `Tooltip` and two icons
76
- * into every host's first paint. It did, until 2026-08-05; see
77
- * reference/EAGER_BUNDLE.md.
78
- *
79
- * Set it through the `pin` option of `checkboxItem` / `toggleItem` /
80
- * `radioItem` / `radioItems`, not by hand.
81
- *
82
- * A `type: 'custom'` row (`makePromotableSizeMenu`) draws its own pin inside
83
- * its rendered content and still sets this — the declaration is what lets a
84
- * built menu be asked which promotable slots it offers a pin for at all. Such
85
- * a row is excluded from the trailing-column reservation; see
86
- * `hasMenuItemAdornment`.
87
- */
88
- pin?: MenuItemPin;
89
- }
90
- /**
91
- * A row's pin declaration: the {@link Pin} `makePin` built, plus the one thing
92
- * the row knows and the pin doesn't — what to call the setting.
93
- *
94
- * **`control` holds the pin by reference; don't flatten this into
95
- * `interface MenuItemPin extends Pin`.** That was tried, to save the hop every
96
- * reader spells (`item.pin.control.active`), and it turns the declaration into a
97
- * *copy* of the control — so a `Pin` whose `toggle` resolves anything through
98
- * `this` silently mutates the copy and the real control never changes. Nothing in
99
- * `Pin` promises copy-safety: `makePin` happens to close over what it needs, but
100
- * the interface is a plain object a plugin can build by hand, and the failure is
101
- * a pin that draws correctly and does nothing. Two hops in `pinnedSlots` and
102
- * `menuItemAdornment` is the whole cost of not having that.
103
- */
104
- export interface MenuItemPin {
105
- control: Pin;
106
- /**
107
- * Names the setting in the pin's tooltip and aria-label. Carried here rather
108
- * than read off the row's `label`, which is a `ReactNode` and may not be a
109
- * string; a pin that can't name what it promotes reads as a bug.
110
- */
111
- label: string;
112
65
  }
113
66
  /** #menuItem normal | an action row; the default when `type` is omitted */
114
67
  export interface NormalMenuItem extends BaseMenuItem {
@@ -133,8 +86,8 @@ export interface RadioMenuItem extends BaseMenuItem {
133
86
  * The function form is called when the submenu opens (or a walker asks for its
134
87
  * rows), not when the parent menu is built — for a submenu whose rows cost a
135
88
  * scan of the display's data to name. Read the field through `resolveSubMenu`
136
- * rather than by hand, so the two forms stay one thing to the renderer, the
137
- * pin-coverage walk and every test helper that flattens a menu.
89
+ * rather than by hand, so the two forms stay one thing to the renderer and to
90
+ * every test helper that flattens a menu.
138
91
  */
139
92
  export interface SubMenuItem extends BaseMenuItem {
140
93
  type?: 'subMenu';
@@ -1,6 +1,5 @@
1
- import type { Pin } from '../configuration/promotablePin.ts';
2
1
  import type { SliderScale } from './sliderScale.ts';
3
- export declare const SizeSliderRow: ({ title, getValue, min, max, step, scale, format, isDefault, commitOnRelease, onChange, onReset, pin, }: {
2
+ export declare const SizeSliderRow: ({ title, getValue, min, max, step, scale, format, isDefault, commitOnRelease, onChange, onReset, }: {
4
3
  title: string;
5
4
  getValue: () => number;
6
5
  min: number;
@@ -12,5 +11,4 @@ export declare const SizeSliderRow: ({ title, getValue, min, max, step, scale, f
12
11
  commitOnRelease?: boolean;
13
12
  onChange: (n: number) => void;
14
13
  onReset: () => void;
15
- pin?: Pin;
16
14
  }) => import("react").JSX.Element;
@@ -1,13 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // The drawn half of a `makeSizeMenu` row, split out so the builder can reach
3
3
  // it through `lazy()`. Everything Material UI in a size row is here: the
4
- // slider, the reset button, the pin, the caption. See makeSizeMenu.tsx.
4
+ // slider, the reset button, the caption. See makeSizeMenu.tsx.
5
5
  import { useState } from 'react';
6
6
  import { Typography } from '@mui/material';
7
7
  import { observer } from 'mobx-react';
8
8
  import { makeStyles } from '../util/tss-react/index.js';
9
9
  import { ResetToDefaultButton } from './InlineMenuControls.js';
10
- import { PinAdornment } from './PinAdornment.js';
11
10
  import SingleSlider from './SingleSlider.js';
12
11
  import { INLINE_MENU_ROW_WIDTH } from './inlineMenuRowWidth.js';
13
12
  import { sliderScale } from './sliderScale.js';
@@ -24,16 +23,15 @@ const useStyles = makeStyles()(theme => ({
24
23
  flex: 1,
25
24
  },
26
25
  }));
27
- export const SizeSliderRow = observer(function SizeSliderRow({ title, getValue, min, max, step, scale, format, isDefault, commitOnRelease, onChange, onReset, pin, }) {
26
+ export const SizeSliderRow = observer(function SizeSliderRow({ title, getValue, min, max, step, scale, format, isDefault, commitOnRelease, onChange, onReset, }) {
28
27
  const { classes } = useStyles();
29
- const modelValue = getValue();
30
28
  const [dragValue, setDragValue] = useState(undefined);
31
- const value = dragValue ?? modelValue;
29
+ const value = dragValue ?? getValue();
32
30
  const { toSlider, fromSlider, sliderStep } = sliderScale(scale);
33
31
  const slug = title.toLowerCase().replaceAll(' ', '-');
34
32
  return (_jsxs("div", { className: classes.root, children: [_jsxs("div", { className: classes.header, children: [_jsxs(Typography, { variant: "caption", color: "textSecondary", className: classes.label, children: [title, ": ", format(value)] }), _jsx(ResetToDefaultButton, { disabled: isDefault, onClick: () => {
35
33
  onReset();
36
- } }), pin ? (_jsx(PinAdornment, { pin: { control: pin, label: `${title} (${format(modelValue)})` } })) : null] }), _jsx(SingleSlider, { value: toSlider(value), min: toSlider(min), max: toSlider(max), step: sliderStep ?? step, size: "small", "aria-label": title.toLowerCase(), "data-testid": `${slug}-slider`, valueLabelDisplay: "auto", valueLabelFormat: (v) => format(fromSlider(v)), sx: { py: 0.5, display: 'block' }, onChange: v => {
34
+ } })] }), _jsx(SingleSlider, { value: toSlider(value), min: toSlider(min), max: toSlider(max), step: sliderStep ?? step, size: "small", "aria-label": title.toLowerCase(), "data-testid": `${slug}-slider`, valueLabelDisplay: "auto", valueLabelFormat: (v) => format(fromSlider(v)), sx: { py: 0.5, display: 'block' }, onChange: v => {
37
35
  const n = fromSlider(v);
38
36
  if (commitOnRelease) {
39
37
  setDragValue(n);
@@ -1,13 +1,19 @@
1
1
  import type { ColorLegendEntry } from './legendSpec.ts';
2
- import type { ReactNode } from 'react';
3
2
  export type { ColorLegendEntry } from './legendSpec.ts';
4
3
  export declare const LEGEND_ROW_HEIGHT = 14;
5
4
  export declare const LEGEND_SWATCH = 10;
6
- export default function SvgColorLegend({ entries, canvasWidth, maxHeight, onDismiss, children, testid, }: {
5
+ /**
6
+ * Horizontal space the LGV export reserves beside the plot for a display whose
7
+ * legend asked to sit off the plot (`LegendMixin.svgLegendWidth`): the widest
8
+ * row this draws for a ramp, plus a margin either side.
9
+ */
10
+ export declare const LEGEND_SVG_GUTTER_WIDTH: number;
11
+ export default function SvgColorLegend({ entries, canvasWidth, x, maxHeight, onDismiss, testid, idPrefix, }: {
7
12
  entries: ColorLegendEntry[];
8
13
  canvasWidth: number;
14
+ x?: number;
9
15
  maxHeight?: number;
10
16
  onDismiss?: () => void;
11
- children?: ReactNode;
17
+ idPrefix?: string;
12
18
  testid?: string;
13
19
  }): import("react").JSX.Element | null;
@@ -1,35 +1,68 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /* eslint-disable react-refresh/only-export-components -- geometry constants belong with this leaf SVG primitive; no component state to fast-refresh */
3
+ import { useId } from 'react';
4
+ import { svgSafeId } from '../svg/svgId.js';
3
5
  import { LegendSwatchGlyph } from './LegendSwatchGlyph.js';
4
6
  import { measureLegendText } from './measureLegendText.js';
5
7
  const FONT_SIZE = 10;
6
8
  const SWATCH_LEFT = 2;
7
9
  const SWATCH_GAP = 2;
8
- // Row geometry, exported so callers positioning `children` (or sizing an outer
9
- // container) stay in lockstep with what this draws.
10
+ // Row geometry, exported so callers sizing an outer container stay in lockstep
11
+ // with what this draws.
10
12
  export const LEGEND_ROW_HEIGHT = 14;
11
13
  export const LEGEND_SWATCH = 10;
14
+ const GRADIENT_BAR_WIDTH = 100;
15
+ const GRADIENT_BAR_HEIGHT = 10;
16
+ // the bar plus the row of domain labels under it
17
+ const GRADIENT_ROW_HEIGHT = GRADIENT_BAR_HEIGHT + 4 + LEGEND_ROW_HEIGHT;
18
+ /**
19
+ * Horizontal space the LGV export reserves beside the plot for a display whose
20
+ * legend asked to sit off the plot (`LegendMixin.svgLegendWidth`): the widest
21
+ * row this draws for a ramp, plus a margin either side.
22
+ */
23
+ export const LEGEND_SVG_GUTTER_WIDTH = GRADIENT_BAR_WIDTH + 40;
12
24
  // narrow right-side gutter reserved for the small dismiss "×"
13
25
  const DISMISS_GUTTER = 11;
14
- // A row's marks, with the flat `color` shorthand read as the single filled
26
+ // A row's swatches, with the flat `color` shorthand read as the single
15
27
  // square it has always drawn.
16
28
  function entrySwatches(entry) {
17
29
  return (entry.swatches ??
18
30
  (entry.color === undefined ? [] : [{ color: entry.color }]));
19
31
  }
20
- // Shared SVG categorical color key: one translucent row per entry, each a swatch
21
- // + label, right-aligned within canvasWidth. Used by any display that colors by
22
- // a discrete vocabulary (wiggle multi-source overlays, multi-row per-feature
23
- // paintings). A <g> so the on-screen overlay and the SVG export share one
24
- // renderer.
25
- //
26
- // Two escape hatches keep it usable for less-uniform legends without a second
27
- // component: a per-entry `marker` overrides the color square, and `children`
28
- // render inside the positioned box below the rows (from the box's top-left; the
29
- // rows occupy `shownEntryCount * LEGEND_ROW_HEIGHT`). Draws nothing when there's
30
- // neither an entry nor a child.
32
+ // A ramp row is the bar and its labels, captioned when the entry has a label.
33
+ function rowHeight(entry) {
34
+ return entry.gradient
35
+ ? GRADIENT_ROW_HEIGHT + (entry.label ? LEGEND_ROW_HEIGHT : 0)
36
+ : LEGEND_ROW_HEIGHT;
37
+ }
38
+ // The rows that fit `maxHeight`, with the last fitting row given over to a
39
+ // "+N more" summary when they do not all fit.
40
+ function fitEntries(entries, maxHeight) {
41
+ let used = 0;
42
+ const shown = [];
43
+ for (const [idx, entry] of entries.entries()) {
44
+ const rest = entries.length - idx - 1;
45
+ const reserve = rest > 0 ? LEGEND_ROW_HEIGHT : 0;
46
+ if (used + rowHeight(entry) + reserve > maxHeight && shown.length > 0) {
47
+ break;
48
+ }
49
+ used += rowHeight(entry);
50
+ shown.push(entry);
51
+ }
52
+ return shown;
53
+ }
54
+ function GradientRow({ entry, width, gradientId, }) {
55
+ const { stops, minLabel, maxLabel } = entry.gradient;
56
+ const barTop = (entry.label ? LEGEND_ROW_HEIGHT : 0) + 2;
57
+ const labelY = barTop + GRADIENT_BAR_HEIGHT + 2 + FONT_SIZE;
58
+ return (_jsxs(_Fragment, { children: [_jsx("rect", { x: 0, y: 0, width: width, height: rowHeight(entry), fill: "rgba(255,255,255,0.95)" }), entry.label ? (_jsx("text", { x: SWATCH_LEFT, y: 11, fontSize: FONT_SIZE, fill: "black", children: entry.label })) : null, _jsx("defs", { children: _jsx("linearGradient", { id: gradientId, x1: "0%", y1: "0%", x2: "100%", y2: "0%", children: stops.map(stop => (_jsx("stop", { offset: stop.offset, style: { stopColor: stop.color, stopOpacity: stop.opacity ?? 1 } }, stop.offset))) }) }), _jsx("rect", { x: SWATCH_LEFT, y: barTop, width: GRADIENT_BAR_WIDTH, height: GRADIENT_BAR_HEIGHT, fill: `url(#${gradientId})`, rx: 2 }), _jsx("text", { x: SWATCH_LEFT, y: labelY, fontSize: FONT_SIZE, fill: "black", children: minLabel }), _jsx("text", { x: SWATCH_LEFT + GRADIENT_BAR_WIDTH, y: labelY, fontSize: FONT_SIZE, fill: "black", textAnchor: "end", children: maxLabel })] }));
59
+ }
60
+ // Shared SVG color key: one translucent row per entry — a swatch and a label,
61
+ // or a ramp bar with its domain ends — right-aligned within `canvasWidth`, or
62
+ // at `x` when the caller places it (the LGV export's legend gutter). A <g>, so
63
+ // an on-screen SVG overlay and the export share one renderer.
31
64
  //
32
- // `maxHeight` (e.g. the display height) caps the box: entries past what fits
65
+ // `maxHeight` (the display height) caps the box: entries past what fits
33
66
  // collapse into a trailing "+N more" summary row, so the legend never overflows
34
67
  // its display — the full list stays reachable via the track menu.
35
68
  //
@@ -37,29 +70,37 @@ function entrySwatches(entry) {
37
70
  // pointer-events so it works under a pointer-events:none overlay). Pass it only
38
71
  // on interactive paths where the legend can be re-shown — never on the SVG
39
72
  // export, which has no way to click it.
40
- export default function SvgColorLegend({ entries, canvasWidth, maxHeight, onDismiss, children, testid, }) {
41
- const fit = maxHeight === undefined
42
- ? entries.length
43
- : Math.max(1, Math.floor(maxHeight / LEGEND_ROW_HEIGHT));
44
- // reserve the last fitting row for the summary when truncating
45
- const shown = entries.length > fit ? entries.slice(0, fit - 1) : entries;
46
- const overflowLabel = entries.length > fit ? `+${entries.length - shown.length} more` : undefined;
73
+ export default function SvgColorLegend({ entries, canvasWidth, x, maxHeight, onDismiss, testid, idPrefix, }) {
74
+ const reactId = useId();
75
+ const legendId = idPrefix ?? reactId;
76
+ const shown = maxHeight === undefined ? entries : fitEntries(entries, maxHeight);
77
+ const overflowLabel = entries.length > shown.length
78
+ ? `+${entries.length - shown.length} more`
79
+ : undefined;
47
80
  // Labels line up across rows, so the swatch column is sized by the row with
48
- // the most marks — otherwise a two-mark row shoves its own label out of the
49
- // column the others share. One mark reproduces the original 16px inset.
81
+ // the most swatches — otherwise a two-box row shoves its own label out of the
82
+ // column the others share. One box reproduces the original 16px inset.
50
83
  const swatchColumns = Math.max(1, ...shown.map(e => entrySwatches(e).length));
51
84
  const textLeft = SWATCH_LEFT + swatchColumns * (LEGEND_SWATCH + SWATCH_GAP) + 2;
52
- let maxLabelWidth = 0;
85
+ let contentRight = 0;
53
86
  for (const entry of shown) {
54
- maxLabelWidth = Math.max(maxLabelWidth, measureLegendText(entry.label, FONT_SIZE));
87
+ contentRight = Math.max(contentRight, entry.gradient
88
+ ? SWATCH_LEFT +
89
+ Math.max(GRADIENT_BAR_WIDTH, measureLegendText(entry.label, FONT_SIZE))
90
+ : textLeft + measureLegendText(entry.label, FONT_SIZE));
55
91
  }
56
92
  if (overflowLabel !== undefined) {
57
- maxLabelWidth = Math.max(maxLabelWidth, measureLegendText(overflowLabel, FONT_SIZE));
93
+ contentRight = Math.max(contentRight, textLeft + measureLegendText(overflowLabel, FONT_SIZE));
94
+ }
95
+ const totalWidth = contentRight + 6 + (onDismiss ? DISMISS_GUTTER : 0);
96
+ const left = x ?? Math.max(0, canvasWidth - totalWidth - 4);
97
+ const rowTops = [];
98
+ let y = 0;
99
+ for (const entry of shown) {
100
+ rowTops.push(y);
101
+ y += rowHeight(entry);
58
102
  }
59
- const totalWidth = textLeft + maxLabelWidth + 6 + (onDismiss ? DISMISS_GUTTER : 0);
60
- const x = Math.max(0, canvasWidth - totalWidth - 4);
61
- return shown.length || overflowLabel || children ? (_jsxs("g", { transform: `translate(${x} 0)`, "data-testid": testid, children: [shown.map((entry, idx) => (_jsxs("g", { transform: `translate(0 ${idx * LEGEND_ROW_HEIGHT})`, children: [_jsx("rect", { x: 0, y: 0, width: totalWidth, height: LEGEND_ROW_HEIGHT, fill: "rgba(255,255,255,0.95)" }), _jsxs("g", { opacity: entry.hidden ? 0.35 : 1, children: [entry.marker ??
62
- entrySwatches(entry).map((swatch, i) => (_jsx(LegendSwatchGlyph, { swatch: swatch, size: LEGEND_SWATCH, x: SWATCH_LEFT + i * (LEGEND_SWATCH + SWATCH_GAP), y: 2 }, `${swatch.color}-${swatch.mark ?? 'fill'}`))), _jsx("text", { x: textLeft, y: 11, fontSize: FONT_SIZE, fill: "black", textDecoration: entry.hidden ? 'line-through' : undefined, children: entry.label })] })] }, entry.key))), overflowLabel === undefined ? null : (_jsxs("g", { transform: `translate(0 ${shown.length * LEGEND_ROW_HEIGHT})`, children: [_jsx("rect", { x: 0, y: 0, width: totalWidth, height: LEGEND_ROW_HEIGHT, fill: "rgba(255,255,255,0.95)" }), _jsx("text", { x: textLeft, y: 11, fontSize: FONT_SIZE, fill: "#555", children: overflowLabel })] })), onDismiss === undefined ? null : (_jsxs("g", { transform: `translate(${totalWidth - DISMISS_GUTTER} 0)`, style: { cursor: 'pointer', pointerEvents: 'auto' }, "data-gesture-owner": "true", onClick: () => {
103
+ return shown.length || overflowLabel ? (_jsxs("g", { transform: `translate(${left} 0)`, "data-testid": testid, children: [shown.map((entry, idx) => (_jsx("g", { transform: `translate(0 ${rowTops[idx]})`, children: entry.gradient ? (_jsx(GradientRow, { entry: entry, width: totalWidth, gradientId: svgSafeId(`${legendId}-${entry.key}`) })) : (_jsxs(_Fragment, { children: [_jsx("rect", { x: 0, y: 0, width: totalWidth, height: LEGEND_ROW_HEIGHT, fill: "rgba(255,255,255,0.95)" }), _jsxs("g", { opacity: entry.hidden ? 0.35 : 1, children: [entrySwatches(entry).map((swatch, i) => (_jsx(LegendSwatchGlyph, { swatch: swatch, size: LEGEND_SWATCH, x: SWATCH_LEFT + i * (LEGEND_SWATCH + SWATCH_GAP), y: 2 }, swatch.color))), _jsx("text", { x: textLeft, y: 11, fontSize: FONT_SIZE, fill: "black", textDecoration: entry.hidden ? 'line-through' : undefined, children: entry.label })] })] })) }, entry.key))), overflowLabel === undefined ? null : (_jsxs("g", { transform: `translate(0 ${y})`, children: [_jsx("rect", { x: 0, y: 0, width: totalWidth, height: LEGEND_ROW_HEIGHT, fill: "rgba(255,255,255,0.95)" }), _jsx("text", { x: textLeft, y: 11, fontSize: FONT_SIZE, fill: "#555", children: overflowLabel })] })), onDismiss === undefined ? null : (_jsxs("g", { transform: `translate(${totalWidth - DISMISS_GUTTER} 0)`, style: { cursor: 'pointer', pointerEvents: 'auto' }, "data-gesture-owner": "true", onClick: () => {
63
104
  onDismiss();
64
- }, children: [_jsx("title", { children: "Hide legend" }), _jsx("rect", { x: 0, y: 0, width: DISMISS_GUTTER, height: LEGEND_ROW_HEIGHT, fill: "transparent" }), _jsx("text", { x: DISMISS_GUTTER / 2, y: 9, fontSize: 9, fill: "#777", textAnchor: "middle", children: "\u00D7" })] })), children] })) : null;
105
+ }, children: [_jsx("title", { children: "Hide legend" }), _jsx("rect", { x: 0, y: 0, width: DISMISS_GUTTER, height: LEGEND_ROW_HEIGHT, fill: "transparent" }), _jsx("text", { x: DISMISS_GUTTER / 2, y: 9, fontSize: 9, fill: "#777", textAnchor: "middle", children: "\u00D7" })] }))] })) : null;
65
106
  }
@@ -0,0 +1,64 @@
1
+ import type { LegendSpec, LegendSwatch } from './legendSpec.ts';
2
+ /**
3
+ * One value a categorical scale colors. `value` is the datum the color
4
+ * classifies (an SV type, a sample group, an impact tier), `label` how the key
5
+ * names it. A row with no `color` is a note in the key rather than a swatch —
6
+ * "+N more", or a rule the swatches cannot state.
7
+ */
8
+ export interface CategoricalEntry {
9
+ value: string;
10
+ label: string;
11
+ color?: string;
12
+ swatches?: LegendSwatch[];
13
+ hidden?: boolean;
14
+ }
15
+ /**
16
+ * A color vocabulary: a finite set of values, each drawn in one color. ADR-094's
17
+ * palette-index and per-instance-value cardinalities both resolve to this once
18
+ * the values are enumerated.
19
+ */
20
+ export interface CategoricalScale {
21
+ kind: 'categorical';
22
+ id: string;
23
+ title?: string;
24
+ entries: CategoricalEntry[];
25
+ }
26
+ /** One stop of a continuous ramp, `offset` in `[0, 1]`. */
27
+ export interface RampStop {
28
+ offset: number;
29
+ color: string;
30
+ opacity?: number;
31
+ }
32
+ /**
33
+ * A scalar through a color ramp: ADR-094's scalar-through-a-scale
34
+ * cardinality. `stops` are what the painter samples — read them out of the
35
+ * same LUT the GPU uploads (`stopsFromRampLut`) so the bar is byte-identical
36
+ * to the plot. `format` prints the domain ends; `toLocale` by default.
37
+ */
38
+ export interface RampScale {
39
+ kind: 'ramp';
40
+ id: string;
41
+ title?: string;
42
+ domain: [number, number];
43
+ stops: RampStop[];
44
+ format?: (value: number) => string;
45
+ }
46
+ /**
47
+ * What a display says about one of its color channels. A display declares a
48
+ * list of these (`LegendMixin.colorScales`) and the legend derives from it, so
49
+ * the key cannot list a color nothing painted.
50
+ */
51
+ export type ColorScale = CategoricalScale | RampScale;
52
+ /**
53
+ * The one derivation of a legend from a display's color scales: a section per
54
+ * scale, in the order declared. On screen `FloatingLegend` renders the result
55
+ * and the export flattens it through `legendEntries`, so the two cannot
56
+ * describe different colors.
57
+ *
58
+ * A lone scale names itself: section titles draw only beside other sections,
59
+ * so a lone categorical scale's title becomes the box's (Manhattan's field
60
+ * name over its values) and a lone ramp's goes on its row.
61
+ */
62
+ export declare function legendSpecOf(scales: ColorScale[]): LegendSpec;
63
+ /** Whether a scale has anything for a key to show. */
64
+ export declare function colorScaleIsEmpty(scale: ColorScale): boolean;
@@ -0,0 +1,41 @@
1
+ import { toLocale } from '../util/numericUtils.js';
2
+ // A lone ramp names itself on its row: `FloatingLegend` and `legendEntries`
3
+ // draw a section's title only beside other sections, and a bar of numbers
4
+ // with nothing saying what they count is not a key.
5
+ function rampItem({ title, domain, stops, format = toLocale }, lone) {
6
+ const [min, max] = domain;
7
+ return {
8
+ label: lone ? (title ?? '') : '',
9
+ gradient: { stops, minLabel: format(min), maxLabel: format(max) },
10
+ };
11
+ }
12
+ function sectionOf(scale, lone) {
13
+ return {
14
+ id: scale.id,
15
+ title: scale.title,
16
+ items: scale.kind === 'categorical'
17
+ ? scale.entries.map(entry => ({ ...entry }))
18
+ : [rampItem(scale, lone)],
19
+ };
20
+ }
21
+ /**
22
+ * The one derivation of a legend from a display's color scales: a section per
23
+ * scale, in the order declared. On screen `FloatingLegend` renders the result
24
+ * and the export flattens it through `legendEntries`, so the two cannot
25
+ * describe different colors.
26
+ *
27
+ * A lone scale names itself: section titles draw only beside other sections,
28
+ * so a lone categorical scale's title becomes the box's (Manhattan's field
29
+ * name over its values) and a lone ramp's goes on its row.
30
+ */
31
+ export function legendSpecOf(scales) {
32
+ const lone = scales.length === 1 ? scales[0] : undefined;
33
+ return {
34
+ title: lone?.kind === 'categorical' ? lone.title : undefined,
35
+ sections: scales.map(scale => sectionOf(scale, scales.length === 1)),
36
+ };
37
+ }
38
+ /** Whether a scale has anything for a key to show. */
39
+ export function colorScaleIsEmpty(scale) {
40
+ return scale.kind === 'categorical' && scale.entries.length === 0;
41
+ }
@@ -31,6 +31,17 @@ export declare const paletteColors: {
31
31
  * re-palettes), while a by-position painter has no value to hash and wraps.
32
32
  */
33
33
  export declare const categoricalPalette: string[];
34
+ /**
35
+ * The palette color a categorical value paints, derived from the value rather
36
+ * than from the order it was met: values stream in as regions load, and a
37
+ * discovery-order slot would let one region paint `chr2` blue and the next
38
+ * paint it pink. A non-negative integer takes the slot it names, anchored at
39
+ * 1, so `1`, `2`, `3` walk the palette in order and stay distinct; anything
40
+ * else hashes in, stable for the same reason at the cost of an occasional
41
+ * collision.
42
+ */
43
+ export declare function categoricalValueColor(value: string): string;
44
+ export declare function categoricalValueColor<T>(value: string, palette: readonly T[]): T;
34
45
  /**
35
46
  * Deterministic non-negative 32-bit hash of a string.
36
47
  */
package/esm/ui/colors.js CHANGED
@@ -112,6 +112,13 @@ export const paletteColors = {
112
112
  export const categoricalPalette = [
113
113
  ...new Set([...tableau10, ...set1, ...dark2, ...set2, ...category10].filter(hex => !NEUTRALS.has(hex.toLowerCase()))),
114
114
  ];
115
+ export function categoricalValueColor(value, palette = categoricalPalette) {
116
+ const n = palette.length;
117
+ const num = Number(value);
118
+ return value !== '' && Number.isInteger(num) && num >= 0
119
+ ? palette[(num + n - 1) % n]
120
+ : palette[hashString(value) % n];
121
+ }
115
122
  // only category10 and set1 are imported by name; the rest are reached through
116
123
  // paletteColors above
117
124
  /**
package/esm/ui/index.d.ts CHANGED
@@ -37,7 +37,7 @@ export { default as MonospaceTextField } from './MonospaceTextField.tsx';
37
37
  export { default as ShareLinkField } from './ShareLinkField.tsx';
38
38
  export { default as SingleSlider } from './SingleSlider.tsx';
39
39
  export { default as SliderTooltip } from './SliderTooltip.tsx';
40
- export { makePromotableSizeMenu, makeSizeMenu, makeSizeSubMenu, } from './makeSizeMenu.tsx';
40
+ export { makeSizeMenu, makeSizeSubMenu } from './makeSizeMenu.tsx';
41
41
  export { INLINE_MENU_ROW_WIDTH, ResetToDefaultButton, } from './InlineMenuControls.tsx';
42
42
  export { sliderScale } from './sliderScale.ts';
43
43
  export type { SliderScale } from './sliderScale.ts';
@@ -78,15 +78,13 @@ export { default as Crosshairs } from './Crosshairs.tsx';
78
78
  export { hoverBoxStyle } from './hoverBoxStyle.ts';
79
79
  export { useMouseState, useMouseTracking } from './useMouseTracking.ts';
80
80
  export type { MouseState, MouseTracker } from './useMouseTracking.ts';
81
- export { PinAdornment } from './PinAdornment.tsx';
82
- export { displayTypesWithPromotableSlots, misKindedPins, promotableSlotsWithoutPin, } from './promotablePinCoverage.ts';
83
81
  export declare const VIEW_HEADER_HEIGHT = 28;
84
- export { GRADIENT_LEGEND_HEIGHT, GRADIENT_LEGEND_SVG_AREA_WIDTH, GRADIENT_LEGEND_WIDTH, default as SvgGradientLegend, } from './SvgGradientLegend.tsx';
85
- export type { GradientLabel, GradientStop } from './SvgGradientLegend.tsx';
86
- export { LEGEND_ROW_HEIGHT, LEGEND_SWATCH, default as SvgColorLegend, } from './SvgColorLegend.tsx';
82
+ export { LEGEND_ROW_HEIGHT, LEGEND_SVG_GUTTER_WIDTH, LEGEND_SWATCH, default as SvgColorLegend, } from './SvgColorLegend.tsx';
87
83
  export type { ColorLegendEntry } from './SvgColorLegend.tsx';
88
84
  export { measureLegendText } from './measureLegendText.ts';
89
85
  export { LegendSwatchGlyph } from './LegendSwatchGlyph.tsx';
90
86
  export { MAX_LEGEND_ITEMS, legendEntries, legendIsReadable, legendSwatches, nonEmptyLegendSections, } from './legendSpec.ts';
91
- export type { LegendItem, LegendMark, LegendSection, LegendSpec, LegendSwatch, } from './legendSpec.ts';
87
+ export type { LegendGradient, LegendItem, LegendSection, LegendSpec, LegendSwatch, } from './legendSpec.ts';
88
+ export { colorScaleIsEmpty, legendSpecOf } from './colorScale.ts';
89
+ export type { CategoricalEntry, CategoricalScale, ColorScale, RampScale, RampStop, } from './colorScale.ts';
92
90
  export { checkboxItem, radioItems } from './toggleMenuItems.ts';
package/esm/ui/index.js CHANGED
@@ -34,7 +34,7 @@ export { default as MonospaceTextField } from './MonospaceTextField.js';
34
34
  export { default as ShareLinkField } from './ShareLinkField.js';
35
35
  export { default as SingleSlider } from './SingleSlider.js';
36
36
  export { default as SliderTooltip } from './SliderTooltip.js';
37
- export { makePromotableSizeMenu, makeSizeMenu, makeSizeSubMenu, } from './makeSizeMenu.js';
37
+ export { makeSizeMenu, makeSizeSubMenu } from './makeSizeMenu.js';
38
38
  export { INLINE_MENU_ROW_WIDTH, ResetToDefaultButton, } from './InlineMenuControls.js';
39
39
  export { sliderScale } from './sliderScale.js';
40
40
  export { default as ErrorBar } from './ErrorBar.js';
@@ -72,20 +72,10 @@ export { launchTargetsMenuItem } from './launchTargetsMenuItem.js';
72
72
  export { default as Crosshairs } from './Crosshairs.js';
73
73
  export { hoverBoxStyle } from './hoverBoxStyle.js';
74
74
  export { useMouseState, useMouseTracking } from './useMouseTracking.js';
75
- export { PinAdornment } from './PinAdornment.js';
76
- // Not on the React-free `menuItems.ts` entry beside the row builders: this
77
- // reads a config's slot table, so it pulls `configuration/promotableSlots.ts`
78
- // in, and that entry exists precisely to stay out of every plugin's eager graph.
79
- //
80
- // `pinnedSlots`, the walk underneath it, stays module-private: the question a
81
- // caller has is "which promotable slots does this menu miss", and answering the
82
- // half of it that lists what a menu *does* offer needs the schema's slot table
83
- // to mean anything.
84
- export { displayTypesWithPromotableSlots, misKindedPins, promotableSlotsWithoutPin, } from './promotablePinCoverage.js';
85
75
  export const VIEW_HEADER_HEIGHT = 28;
86
- export { GRADIENT_LEGEND_HEIGHT, GRADIENT_LEGEND_SVG_AREA_WIDTH, GRADIENT_LEGEND_WIDTH, default as SvgGradientLegend, } from './SvgGradientLegend.js';
87
- export { LEGEND_ROW_HEIGHT, LEGEND_SWATCH, default as SvgColorLegend, } from './SvgColorLegend.js';
76
+ export { LEGEND_ROW_HEIGHT, LEGEND_SVG_GUTTER_WIDTH, LEGEND_SWATCH, default as SvgColorLegend, } from './SvgColorLegend.js';
88
77
  export { measureLegendText } from './measureLegendText.js';
89
78
  export { LegendSwatchGlyph } from './LegendSwatchGlyph.js';
90
79
  export { MAX_LEGEND_ITEMS, legendEntries, legendIsReadable, legendSwatches, nonEmptyLegendSections, } from './legendSpec.js';
80
+ export { colorScaleIsEmpty, legendSpecOf } from './colorScale.js';
91
81
  export { checkboxItem, radioItems } from './toggleMenuItems.js';
@@ -1,5 +1,5 @@
1
1
  import type { MenuItem } from './MenuTypes.ts';
2
- import type { CheckboxRowOptions } from './toggleMenuItems.ts';
2
+ import type { SettingRowOptions } from './toggleMenuItems.ts';
3
3
  /**
4
4
  * The "Show legend" checkbox, which seven displays build by hand — Hi-C, LD,
5
5
  * alignments, LGVSynteny, multi-wiggle, multi-row features, both multi-sample
@@ -10,23 +10,12 @@ import type { CheckboxRowOptions } from './toggleMenuItems.ts';
10
10
  * and would otherwise need an adapter object at the call site. Same argument
11
11
  * order as `checkboxItem`, which is all this adds a label to.
12
12
  *
13
- * The `showLegend` **config slots** stay per display: their `promotedBase`
14
- * values legitimately differ (a Hi-C color scale is off by default, a variant
15
- * genotype key on), and their descriptions describe genuinely different legends.
16
- * What they now share is being *promotable* — pass `opts.pin` and the row gains
17
- * the pin that toggles the legend on every open track of this display type,
18
- * offering the new state as the display-type default in its snackbar. Every
19
- * display whose legend is backed by a config slot passes one, and gets it from
20
- * `LegendMixin`'s `showLegendDisplayTypeDefault` rather than calling
21
- * `makeTogglePin` itself: the slot is the per-display half, the accessors over
22
- * it are not.
23
- *
24
- * `pin` is optional because two callers have no slot to promote: the Manhattan
25
- * plot's LD legend and `LinearBasicDisplay`'s color key are a volatile and a
26
- * per-legend `dismissed` flag respectively, neither of which is config at all.
13
+ * The `showLegend` **config slots** stay per display: their default values
14
+ * legitimately differ (a Hi-C color scale is off by default, a variant genotype
15
+ * key on), and their descriptions describe genuinely different legends.
27
16
  *
28
17
  * `synteny-core`'s color-by legend has no toggle at all: it comes up with the
29
18
  * modes that have a key and is dismissed from its own close button.
30
19
  */
31
20
  /** #menuBuilder showLegendCheckboxItem | the shared "Show legend" checkbox */
32
- export declare function showLegendCheckboxItem(checked: boolean, onToggle: () => void, opts?: CheckboxRowOptions): MenuItem;
21
+ export declare function showLegendCheckboxItem(checked: boolean, onToggle: () => void, opts?: SettingRowOptions): MenuItem;
@@ -9,20 +9,9 @@ import { checkboxItem } from './toggleMenuItems.js';
9
9
  * and would otherwise need an adapter object at the call site. Same argument
10
10
  * order as `checkboxItem`, which is all this adds a label to.
11
11
  *
12
- * The `showLegend` **config slots** stay per display: their `promotedBase`
13
- * values legitimately differ (a Hi-C color scale is off by default, a variant
14
- * genotype key on), and their descriptions describe genuinely different legends.
15
- * What they now share is being *promotable* — pass `opts.pin` and the row gains
16
- * the pin that toggles the legend on every open track of this display type,
17
- * offering the new state as the display-type default in its snackbar. Every
18
- * display whose legend is backed by a config slot passes one, and gets it from
19
- * `LegendMixin`'s `showLegendDisplayTypeDefault` rather than calling
20
- * `makeTogglePin` itself: the slot is the per-display half, the accessors over
21
- * it are not.
22
- *
23
- * `pin` is optional because two callers have no slot to promote: the Manhattan
24
- * plot's LD legend and `LinearBasicDisplay`'s color key are a volatile and a
25
- * per-legend `dismissed` flag respectively, neither of which is config at all.
12
+ * The `showLegend` **config slots** stay per display: their default values
13
+ * legitimately differ (a Hi-C color scale is off by default, a variant genotype
14
+ * key on), and their descriptions describe genuinely different legends.
26
15
  *
27
16
  * `synteny-core`'s color-by legend has no toggle at all: it comes up with the
28
17
  * modes that have a key and is dismissed from its own close button.