gramene-search 2.3.0 → 2.4.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.
- package/dist/index.js +315 -78
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16885,6 +16885,14 @@ var $597fe213417ee6ca$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
|
|
|
16885
16885
|
// data.hostData.bins = { genomesByTaxon: { [taxonId]: genome }, maxScore }
|
|
16886
16886
|
// where `genome` is a gramene-bins-client genome (fullGenomeSize,
|
|
16887
16887
|
// _regionsArray, region.bin(i)/binCount()/size/name, bin.results.count).
|
|
16888
|
+
//
|
|
16889
|
+
// Pan/zoom: a single shared horizontal transform { scale, leftFrac } (fraction
|
|
16890
|
+
// space, so it's width-independent and aligns every genome to the same relative
|
|
16891
|
+
// window) lives in the ephemeral binsUI store. The header toggles between two
|
|
16892
|
+
// interaction modes: 'select' (drag selects a region to count/filter genes,
|
|
16893
|
+
// the original gesture) and 'panzoom' (drag pans, wheel zooms toward the
|
|
16894
|
+
// cursor). All hit-testing/coordinates are in genome-fraction [0,1] so
|
|
16895
|
+
// hovers/selections stay anchored to the genome as you pan and zoom.
|
|
16888
16896
|
|
|
16889
16897
|
|
|
16890
16898
|
|
|
@@ -16967,28 +16975,66 @@ function $bfd29d54a0f0e853$var$updateScore(currentScore, baseCount, binScore, bi
|
|
|
16967
16975
|
if (typeof currentScore === 'number') return (currentScore * baseCount + binScore * binBasesUsed) / (binBasesUsed + baseCount);
|
|
16968
16976
|
return binScore;
|
|
16969
16977
|
}
|
|
16970
|
-
|
|
16971
|
-
|
|
16972
|
-
|
|
16978
|
+
const $bfd29d54a0f0e853$var$clamp = (v, lo, hi)=>Math.max(lo, Math.min(hi, v));
|
|
16979
|
+
const $bfd29d54a0f0e853$var$MAX_SCALE = 1000; // zoom far enough to isolate a single bin
|
|
16980
|
+
// Locate the region/bin cursor at a target base offset, so a zoomed draw can
|
|
16981
|
+
// start partway into the genome without iterating skipped pixels. Returns null
|
|
16982
|
+
// past the end of the genome.
|
|
16983
|
+
function $bfd29d54a0f0e853$var$seekCursor(regions, targetBase) {
|
|
16984
|
+
let acc = 0;
|
|
16985
|
+
for(let regionIdx = 0; regionIdx < regions.length; regionIdx++){
|
|
16986
|
+
const region = regions[regionIdx];
|
|
16987
|
+
const n = region.binCount();
|
|
16988
|
+
for(let binIdx = 0; binIdx < n; binIdx++){
|
|
16989
|
+
const bin = region.bin(binIdx);
|
|
16990
|
+
const size = bin.end - bin.start + 1;
|
|
16991
|
+
if (acc + size > targetBase) return {
|
|
16992
|
+
regionIdx: regionIdx,
|
|
16993
|
+
binIdx: binIdx,
|
|
16994
|
+
basesInBinUsedAlready: Math.max(0, targetBase - acc)
|
|
16995
|
+
};
|
|
16996
|
+
acc += size;
|
|
16997
|
+
}
|
|
16998
|
+
}
|
|
16999
|
+
return null;
|
|
17000
|
+
}
|
|
17001
|
+
// Draw one genome's distribution into the visible [0, widthPx) strip at
|
|
17002
|
+
// vertical [y, y+height), under the shared { scale, leftFrac } transform. Only
|
|
17003
|
+
// the visible pixels are drawn (the cursor is fast-forwarded to the window's
|
|
17004
|
+
// left edge); consecutive equal-colour columns are batched into one fillRect,
|
|
17005
|
+
// which makes zoomed-in draws (where each bin spans many px) cheap. Ported from
|
|
17006
|
+
// drawGenome() in gramene-search-vis, generalised for pan/zoom.
|
|
17007
|
+
function $bfd29d54a0f0e853$var$drawGenomeRow(ctx, genome, y, widthPx, height, maxScore, scale, leftFrac) {
|
|
16973
17008
|
const regions = genome && genome._regionsArray;
|
|
16974
|
-
|
|
16975
|
-
|
|
17009
|
+
const full = genome && genome.fullGenomeSize;
|
|
17010
|
+
if (!regions || regions.length === 0 || !full) return;
|
|
17011
|
+
const virtualWidth = widthPx * scale; // full genome spans this many px when zoomed
|
|
17012
|
+
const basesPerPx = full / virtualWidth;
|
|
16976
17013
|
if (!(basesPerPx > 0)) return;
|
|
16977
|
-
|
|
16978
|
-
|
|
16979
|
-
let regionIdx =
|
|
17014
|
+
const cursor = $bfd29d54a0f0e853$var$seekCursor(regions, leftFrac * full);
|
|
17015
|
+
if (!cursor) return;
|
|
17016
|
+
let { regionIdx: regionIdx, binIdx: binIdx, basesInBinUsedAlready: basesInBinUsedAlready } = cursor;
|
|
16980
17017
|
let region = regions[regionIdx];
|
|
16981
17018
|
let regionUnanchored = region.name === 'UNANCHORED';
|
|
16982
|
-
|
|
17019
|
+
let runColor = null;
|
|
17020
|
+
let runStart = 0;
|
|
17021
|
+
const flush = (endExclusive)=>{
|
|
17022
|
+
if (runColor !== null && endExclusive > runStart) {
|
|
17023
|
+
ctx.fillStyle = runColor;
|
|
17024
|
+
ctx.fillRect(runStart, y, endExclusive - runStart, height);
|
|
17025
|
+
}
|
|
17026
|
+
};
|
|
17027
|
+
let px = 0;
|
|
17028
|
+
for(; px < widthPx; px++){
|
|
16983
17029
|
let baseCount = 0;
|
|
16984
17030
|
let score;
|
|
16985
|
-
let
|
|
17031
|
+
let ended = false;
|
|
16986
17032
|
while(baseCount < basesPerPx){
|
|
16987
17033
|
const basesNeededByThisPixel = basesPerPx - baseCount;
|
|
16988
17034
|
const bin = region.bin(binIdx);
|
|
16989
17035
|
const binSize = bin.end - bin.start + 1;
|
|
16990
17036
|
const binScore = maxScore ? (bin.results ? bin.results.count : 0) / maxScore : 0;
|
|
16991
|
-
basesAvailableInBin = binSize - basesInBinUsedAlready;
|
|
17037
|
+
const basesAvailableInBin = binSize - basesInBinUsedAlready;
|
|
16992
17038
|
let binBasesUsed;
|
|
16993
17039
|
if (basesAvailableInBin <= basesNeededByThisPixel) {
|
|
16994
17040
|
binIdx++;
|
|
@@ -17003,14 +17049,30 @@ function $bfd29d54a0f0e853$var$drawGenomeRow(ctx, genome, x, y, width, height, m
|
|
|
17003
17049
|
if (binIdx === region.binCount()) {
|
|
17004
17050
|
binIdx = 0;
|
|
17005
17051
|
regionIdx++;
|
|
17006
|
-
if (regionIdx === regions.length)
|
|
17052
|
+
if (regionIdx === regions.length) {
|
|
17053
|
+
ended = true;
|
|
17054
|
+
break;
|
|
17055
|
+
}
|
|
17007
17056
|
region = regions[regionIdx];
|
|
17008
17057
|
regionUnanchored = region.name === 'UNANCHORED';
|
|
17009
17058
|
}
|
|
17010
17059
|
}
|
|
17011
|
-
|
|
17012
|
-
|
|
17060
|
+
const idxForColor = regionIdx >= regions.length ? regions.length - 1 : regionIdx;
|
|
17061
|
+
const color = $bfd29d54a0f0e853$var$binColor(idxForColor, score || 0, regionUnanchored);
|
|
17062
|
+
if (runColor === null) {
|
|
17063
|
+
runColor = color;
|
|
17064
|
+
runStart = px;
|
|
17065
|
+
} else if (color !== runColor) {
|
|
17066
|
+
flush(px);
|
|
17067
|
+
runColor = color;
|
|
17068
|
+
runStart = px;
|
|
17069
|
+
}
|
|
17070
|
+
if (ended) {
|
|
17071
|
+
px++;
|
|
17072
|
+
break;
|
|
17073
|
+
}
|
|
17013
17074
|
}
|
|
17075
|
+
flush(px);
|
|
17014
17076
|
}
|
|
17015
17077
|
function $bfd29d54a0f0e853$export$390e10a52782f018(taxDist) {
|
|
17016
17078
|
if (!taxDist || typeof taxDist.leafNodes !== 'function') return null;
|
|
@@ -17030,12 +17092,18 @@ function $bfd29d54a0f0e853$export$390e10a52782f018(taxDist) {
|
|
|
17030
17092
|
}
|
|
17031
17093
|
// ── interaction store (shared between Header and Body via hostData) ──────────
|
|
17032
17094
|
// A tiny pub/sub kept OUT of the controlled tbrowse viewState so per-mousemove
|
|
17033
|
-
// hover/drag updates don't churn the whole tree. Holds the hovered
|
|
17034
|
-
// the in-progress drag,
|
|
17095
|
+
// hover/drag/pan/zoom updates don't churn the whole tree. Holds the hovered
|
|
17096
|
+
// chromosome, the in-progress select-drag, the committed drag-selections, and
|
|
17097
|
+
// the shared horizontal pan/zoom transform.
|
|
17098
|
+
const $bfd29d54a0f0e853$var$DEFAULT_TRANSFORM = {
|
|
17099
|
+
scale: 1,
|
|
17100
|
+
leftFrac: 0
|
|
17101
|
+
};
|
|
17035
17102
|
const $bfd29d54a0f0e853$var$EMPTY_UI = {
|
|
17036
17103
|
hovered: null,
|
|
17037
17104
|
inProgress: null,
|
|
17038
|
-
selections: []
|
|
17105
|
+
selections: [],
|
|
17106
|
+
transform: $bfd29d54a0f0e853$var$DEFAULT_TRANSFORM
|
|
17039
17107
|
};
|
|
17040
17108
|
const $bfd29d54a0f0e853$var$NOOP_SUB = ()=>()=>{};
|
|
17041
17109
|
const $bfd29d54a0f0e853$var$NOOP_GET = ()=>$bfd29d54a0f0e853$var$EMPTY_UI;
|
|
@@ -17063,21 +17131,35 @@ function $bfd29d54a0f0e853$export$f06340802363875a() {
|
|
|
17063
17131
|
};
|
|
17064
17132
|
emit();
|
|
17065
17133
|
},
|
|
17134
|
+
setTransform: (t)=>{
|
|
17135
|
+
state = {
|
|
17136
|
+
...state,
|
|
17137
|
+
transform: t
|
|
17138
|
+
};
|
|
17139
|
+
emit();
|
|
17140
|
+
},
|
|
17141
|
+
resetTransform: ()=>{
|
|
17142
|
+
state = {
|
|
17143
|
+
...state,
|
|
17144
|
+
transform: $bfd29d54a0f0e853$var$DEFAULT_TRANSFORM
|
|
17145
|
+
};
|
|
17146
|
+
emit();
|
|
17147
|
+
},
|
|
17066
17148
|
// Add a selection, merging it with any existing selection on the SAME
|
|
17067
|
-
// genome whose
|
|
17149
|
+
// genome whose fraction range overlaps — so overlapping drags become one
|
|
17068
17150
|
// region and shared bins aren't double-counted. Bins are unioned by their
|
|
17069
17151
|
// global index.
|
|
17070
17152
|
addSelection: (sel)=>{
|
|
17071
17153
|
let merged = sel;
|
|
17072
17154
|
const rest = [];
|
|
17073
|
-
for (const s of state.selections)if (s.taxonId === merged.taxonId && s.
|
|
17155
|
+
for (const s of state.selections)if (s.taxonId === merged.taxonId && s.f0 <= merged.f1 && s.f1 >= merged.f0) {
|
|
17074
17156
|
const byIdx = new Map();
|
|
17075
17157
|
for (const b of s.bins)byIdx.set(b.idx, b.count);
|
|
17076
17158
|
for (const b of merged.bins)byIdx.set(b.idx, b.count);
|
|
17077
17159
|
merged = {
|
|
17078
17160
|
taxonId: merged.taxonId,
|
|
17079
|
-
|
|
17080
|
-
|
|
17161
|
+
f0: Math.min(s.f0, merged.f0),
|
|
17162
|
+
f1: Math.max(s.f1, merged.f1),
|
|
17081
17163
|
bins: [
|
|
17082
17164
|
...byIdx
|
|
17083
17165
|
].map(([idx, count])=>({
|
|
@@ -17095,7 +17177,17 @@ function $bfd29d54a0f0e853$export$f06340802363875a() {
|
|
|
17095
17177
|
};
|
|
17096
17178
|
emit();
|
|
17097
17179
|
},
|
|
17180
|
+
// Clear hover/selections but keep the current zoom/pan transform — clearing
|
|
17181
|
+
// selections shouldn't yank the user back out of their zoom.
|
|
17098
17182
|
clear: ()=>{
|
|
17183
|
+
state = {
|
|
17184
|
+
...$bfd29d54a0f0e853$var$EMPTY_UI,
|
|
17185
|
+
transform: state.transform
|
|
17186
|
+
};
|
|
17187
|
+
emit();
|
|
17188
|
+
},
|
|
17189
|
+
// Full reset (new result set): drop everything including the transform.
|
|
17190
|
+
reset: ()=>{
|
|
17099
17191
|
state = $bfd29d54a0f0e853$var$EMPTY_UI;
|
|
17100
17192
|
emit();
|
|
17101
17193
|
}
|
|
@@ -17115,55 +17207,56 @@ function $bfd29d54a0f0e853$export$861100744513cc3(selections) {
|
|
|
17115
17207
|
...set
|
|
17116
17208
|
];
|
|
17117
17209
|
}
|
|
17118
|
-
// Per-genome
|
|
17210
|
+
// Per-genome layout in genome-fraction space [0,1]: region spans (for
|
|
17119
17211
|
// chromosome hit-testing) and bin spans with gene counts (for summing a
|
|
17120
|
-
// drag-selected range).
|
|
17121
|
-
|
|
17212
|
+
// drag-selected range). Width/zoom-independent — the transform maps fractions
|
|
17213
|
+
// to screen pixels at render time.
|
|
17214
|
+
function $bfd29d54a0f0e853$var$buildGenomeLayout(genome) {
|
|
17122
17215
|
const out = {
|
|
17123
17216
|
regions: []
|
|
17124
17217
|
};
|
|
17125
17218
|
const regions = genome && genome._regionsArray;
|
|
17126
|
-
|
|
17127
|
-
|
|
17128
|
-
|
|
17129
|
-
let px = 0;
|
|
17219
|
+
const full = genome && genome.fullGenomeSize;
|
|
17220
|
+
if (!regions || !full) return out;
|
|
17221
|
+
let base = 0;
|
|
17130
17222
|
for (const region of regions){
|
|
17131
|
-
const
|
|
17223
|
+
const f0 = base / full;
|
|
17132
17224
|
const binsArr = [];
|
|
17133
17225
|
const n = region.binCount();
|
|
17134
17226
|
for(let i = 0; i < n; i++){
|
|
17135
17227
|
const bin = region.bin(i);
|
|
17136
|
-
const
|
|
17228
|
+
const size = bin.end - bin.start + 1;
|
|
17229
|
+
const bf0 = base / full;
|
|
17230
|
+
base += size;
|
|
17137
17231
|
binsArr.push({
|
|
17138
|
-
|
|
17139
|
-
|
|
17232
|
+
f0: bf0,
|
|
17233
|
+
f1: base / full,
|
|
17140
17234
|
count: bin.results && bin.results.count || 0,
|
|
17141
17235
|
idx: bin.idx
|
|
17142
17236
|
});
|
|
17143
|
-
px += bw;
|
|
17144
17237
|
}
|
|
17145
17238
|
out.regions.push({
|
|
17146
17239
|
name: region.name,
|
|
17147
|
-
|
|
17148
|
-
|
|
17240
|
+
f0: f0,
|
|
17241
|
+
f1: base / full,
|
|
17149
17242
|
bins: binsArr
|
|
17150
17243
|
});
|
|
17151
17244
|
}
|
|
17152
17245
|
return out;
|
|
17153
17246
|
}
|
|
17154
|
-
function $bfd29d54a0f0e853$var$
|
|
17155
|
-
for (const r of layout.regions)if (
|
|
17247
|
+
function $bfd29d54a0f0e853$var$regionAtFrac(layout, f) {
|
|
17248
|
+
for (const r of layout.regions)if (f >= r.f0 && f < r.f1) return r;
|
|
17156
17249
|
return null;
|
|
17157
17250
|
}
|
|
17158
|
-
// Non-empty bins overlapping a
|
|
17251
|
+
// Non-empty bins overlapping a fraction range, as {idx, count}. Empty bins are
|
|
17159
17252
|
// excluded by design (the selection / filter only covers bins with genes).
|
|
17160
|
-
function $bfd29d54a0f0e853$var$
|
|
17253
|
+
function $bfd29d54a0f0e853$var$selectedBinsInFracRange(layout, a, b) {
|
|
17161
17254
|
const lo = Math.min(a, b);
|
|
17162
17255
|
const hi = Math.max(a, b);
|
|
17163
17256
|
const bins = [];
|
|
17164
17257
|
for (const r of layout.regions){
|
|
17165
|
-
if (r.
|
|
17166
|
-
for (const bin of r.bins)if (bin.
|
|
17258
|
+
if (r.f1 < lo || r.f0 > hi) continue;
|
|
17259
|
+
for (const bin of r.bins)if (bin.f1 > lo && bin.f0 < hi && bin.count > 0) bins.push({
|
|
17167
17260
|
idx: bin.idx,
|
|
17168
17261
|
count: bin.count
|
|
17169
17262
|
});
|
|
@@ -17177,11 +17270,46 @@ const $bfd29d54a0f0e853$var$HEADER_BTN = {
|
|
|
17177
17270
|
padding: '1px 6px',
|
|
17178
17271
|
cursor: 'pointer'
|
|
17179
17272
|
};
|
|
17273
|
+
function $bfd29d54a0f0e853$var$ModeToggle({ mode: mode, setMode: setMode }) {
|
|
17274
|
+
const seg = (id, label)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("button", {
|
|
17275
|
+
type: "button",
|
|
17276
|
+
onClick: ()=>setMode(id),
|
|
17277
|
+
title: id === 'select' ? 'Drag to select a region' : 'Drag to pan, scroll to zoom',
|
|
17278
|
+
style: {
|
|
17279
|
+
fontSize: 11,
|
|
17280
|
+
lineHeight: 1,
|
|
17281
|
+
padding: '2px 7px',
|
|
17282
|
+
cursor: 'pointer',
|
|
17283
|
+
border: 'none',
|
|
17284
|
+
background: mode === id ? 'var(--tbrowse-accent, #2878dc)' : 'transparent',
|
|
17285
|
+
color: mode === id ? '#fff' : 'var(--tbrowse-text-muted)'
|
|
17286
|
+
},
|
|
17287
|
+
children: label
|
|
17288
|
+
});
|
|
17289
|
+
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
|
|
17290
|
+
style: {
|
|
17291
|
+
display: 'inline-flex',
|
|
17292
|
+
border: '1px solid var(--tbrowse-border-soft)',
|
|
17293
|
+
borderRadius: 4,
|
|
17294
|
+
overflow: 'hidden'
|
|
17295
|
+
},
|
|
17296
|
+
children: [
|
|
17297
|
+
seg('select', 'Select'),
|
|
17298
|
+
seg('panzoom', 'Pan/Zoom')
|
|
17299
|
+
]
|
|
17300
|
+
});
|
|
17301
|
+
}
|
|
17180
17302
|
const $bfd29d54a0f0e853$var$BinsHeader = ({ data: data, zoneState: zoneState, setZoneState: setZoneState })=>{
|
|
17181
17303
|
const store = data.hostData && data.hostData.binsUI;
|
|
17182
17304
|
const snap = (0, $gXNCa$react.useSyncExternalStore)(store ? store.subscribe : $bfd29d54a0f0e853$var$NOOP_SUB, store ? store.getState : $bfd29d54a0f0e853$var$NOOP_GET);
|
|
17183
17305
|
const total = $bfd29d54a0f0e853$export$cf7300886d7d9e1e(snap.selections);
|
|
17184
17306
|
const hovered = snap.hovered;
|
|
17307
|
+
const scale = snap.transform ? snap.transform.scale : 1;
|
|
17308
|
+
const mode = zoneState && zoneState.mode || 'select';
|
|
17309
|
+
const setMode = (m)=>setZoneState((s)=>({
|
|
17310
|
+
...s ?? {},
|
|
17311
|
+
mode: m
|
|
17312
|
+
}));
|
|
17185
17313
|
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
|
|
17186
17314
|
style: {
|
|
17187
17315
|
height: '100%',
|
|
@@ -17199,7 +17327,8 @@ const $bfd29d54a0f0e853$var$BinsHeader = ({ data: data, zoneState: zoneState, se
|
|
|
17199
17327
|
display: 'flex',
|
|
17200
17328
|
alignItems: 'center',
|
|
17201
17329
|
gap: 8,
|
|
17202
|
-
minHeight: 0
|
|
17330
|
+
minHeight: 0,
|
|
17331
|
+
flexWrap: 'wrap'
|
|
17203
17332
|
},
|
|
17204
17333
|
children: [
|
|
17205
17334
|
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$tbrowse.EditableZoneName), {
|
|
@@ -17210,6 +17339,32 @@ const $bfd29d54a0f0e853$var$BinsHeader = ({ data: data, zoneState: zoneState, se
|
|
|
17210
17339
|
name: next
|
|
17211
17340
|
}))
|
|
17212
17341
|
}),
|
|
17342
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($bfd29d54a0f0e853$var$ModeToggle, {
|
|
17343
|
+
mode: mode,
|
|
17344
|
+
setMode: setMode
|
|
17345
|
+
}),
|
|
17346
|
+
scale > 1.0001 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
|
|
17347
|
+
children: [
|
|
17348
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
|
|
17349
|
+
style: {
|
|
17350
|
+
color: 'var(--tbrowse-text-muted)',
|
|
17351
|
+
fontSize: 11,
|
|
17352
|
+
whiteSpace: 'nowrap'
|
|
17353
|
+
},
|
|
17354
|
+
children: [
|
|
17355
|
+
scale.toFixed(scale < 10 ? 1 : 0),
|
|
17356
|
+
"\xd7"
|
|
17357
|
+
]
|
|
17358
|
+
}),
|
|
17359
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("button", {
|
|
17360
|
+
type: "button",
|
|
17361
|
+
style: $bfd29d54a0f0e853$var$HEADER_BTN,
|
|
17362
|
+
title: "Reset zoom",
|
|
17363
|
+
onClick: ()=>store && store.resetTransform(),
|
|
17364
|
+
children: "reset zoom"
|
|
17365
|
+
})
|
|
17366
|
+
]
|
|
17367
|
+
}),
|
|
17213
17368
|
snap.selections.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
|
|
17214
17369
|
children: [
|
|
17215
17370
|
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
|
|
@@ -17275,7 +17430,7 @@ function $bfd29d54a0f0e853$var$rowHighlight(isSelected, isExactHover, isInHovere
|
|
|
17275
17430
|
return 'transparent';
|
|
17276
17431
|
}
|
|
17277
17432
|
const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
|
|
17278
|
-
const { visibleRows: visibleRows, rowRange: rowRange, width: width, data: data, hoveredNodeId: hoveredNodeId, hoveredSubtreeIds: hoveredSubtreeIds, selectedNodeId: selectedNodeId, onHoverNode: onHoverNode, onSelectNode: onSelectNode } = props;
|
|
17433
|
+
const { visibleRows: visibleRows, rowRange: rowRange, width: width, data: data, zoneState: zoneState, hoveredNodeId: hoveredNodeId, hoveredSubtreeIds: hoveredSubtreeIds, selectedNodeId: selectedNodeId, onHoverNode: onHoverNode, onSelectNode: onSelectNode } = props;
|
|
17279
17434
|
const canvasRef = (0, $gXNCa$react.useRef)(null);
|
|
17280
17435
|
const containerRef = (0, $gXNCa$react.useRef)(null);
|
|
17281
17436
|
const dragRef = (0, $gXNCa$react.useRef)(null);
|
|
@@ -17285,27 +17440,33 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
|
|
|
17285
17440
|
const suppressClickRef = (0, $gXNCa$react.useRef)(false);
|
|
17286
17441
|
const store = data.hostData && data.hostData.binsUI;
|
|
17287
17442
|
const snap = (0, $gXNCa$react.useSyncExternalStore)(store ? store.subscribe : $bfd29d54a0f0e853$var$NOOP_SUB, store ? store.getState : $bfd29d54a0f0e853$var$NOOP_GET);
|
|
17443
|
+
const transform = snap.transform || $bfd29d54a0f0e853$var$DEFAULT_TRANSFORM;
|
|
17444
|
+
const mode = zoneState && zoneState.mode || 'select';
|
|
17288
17445
|
const totalHeight = visibleRows.length ? visibleRows[visibleRows.length - 1].y + visibleRows[visibleRows.length - 1].height : 0;
|
|
17289
17446
|
const bins = data.hostData && data.hostData.bins;
|
|
17290
17447
|
const rows = visibleRows.slice(rowRange.startIndex, rowRange.endIndex);
|
|
17291
17448
|
const w = Math.max(1, Math.floor(width));
|
|
17292
|
-
//
|
|
17449
|
+
// Fraction-space layout per visible genome row (regions + bins), for
|
|
17450
|
+
// hit-testing. Independent of width/zoom, so it only rebuilds when the row
|
|
17451
|
+
// set changes.
|
|
17293
17452
|
const layouts = (0, $gXNCa$react.useMemo)(()=>{
|
|
17294
17453
|
const m = {};
|
|
17295
17454
|
if (bins) for (const r of rows){
|
|
17296
17455
|
if (r.kind !== 'leaf') continue;
|
|
17297
17456
|
const g = bins.genomesByTaxon[r.nodeId];
|
|
17298
|
-
if (g) m[r.nodeId] = $bfd29d54a0f0e853$var$buildGenomeLayout(g
|
|
17457
|
+
if (g) m[r.nodeId] = $bfd29d54a0f0e853$var$buildGenomeLayout(g);
|
|
17299
17458
|
}
|
|
17300
17459
|
return m;
|
|
17301
17460
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
17302
17461
|
}, [
|
|
17303
17462
|
bins,
|
|
17304
|
-
w,
|
|
17305
17463
|
rowRange.startIndex,
|
|
17306
17464
|
rowRange.endIndex,
|
|
17307
17465
|
visibleRows
|
|
17308
17466
|
]);
|
|
17467
|
+
// Fraction <-> screen-pixel helpers under the current transform.
|
|
17468
|
+
const fracToScreen = (f)=>(f - transform.leftFrac) * transform.scale * w;
|
|
17469
|
+
const screenToFrac = (px)=>transform.leftFrac + px / w / transform.scale;
|
|
17309
17470
|
(0, $gXNCa$react.useEffect)(()=>{
|
|
17310
17471
|
const canvas = canvasRef.current;
|
|
17311
17472
|
if (!canvas || !bins) return;
|
|
@@ -17321,7 +17482,7 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
|
|
|
17321
17482
|
const genome = bins.genomesByTaxon[r.nodeId];
|
|
17322
17483
|
if (!genome) continue;
|
|
17323
17484
|
const innerH = Math.max(1, r.height - 2 * $bfd29d54a0f0e853$var$ROW_PAD_Y);
|
|
17324
|
-
$bfd29d54a0f0e853$var$drawGenomeRow(ctx, genome,
|
|
17485
|
+
$bfd29d54a0f0e853$var$drawGenomeRow(ctx, genome, r.y + $bfd29d54a0f0e853$var$ROW_PAD_Y, w, innerH, bins.maxScore, transform.scale, transform.leftFrac);
|
|
17325
17486
|
}
|
|
17326
17487
|
}, [
|
|
17327
17488
|
visibleRows,
|
|
@@ -17329,21 +17490,53 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
|
|
|
17329
17490
|
rowRange.endIndex,
|
|
17330
17491
|
w,
|
|
17331
17492
|
totalHeight,
|
|
17332
|
-
bins
|
|
17493
|
+
bins,
|
|
17494
|
+
transform.scale,
|
|
17495
|
+
transform.leftFrac
|
|
17333
17496
|
]);
|
|
17334
17497
|
const pxFromEvent = (e)=>{
|
|
17335
17498
|
const el = containerRef.current;
|
|
17336
17499
|
if (!el) return 0;
|
|
17337
17500
|
const rect = el.getBoundingClientRect();
|
|
17338
|
-
return
|
|
17501
|
+
return $bfd29d54a0f0e853$var$clamp(e.clientX - rect.left, 0, w);
|
|
17339
17502
|
};
|
|
17503
|
+
// Wheel-to-zoom (panzoom mode only), centred on the cursor. Native non-passive
|
|
17504
|
+
// listener so we can preventDefault the page scroll; re-attached when mode/
|
|
17505
|
+
// width change. Transform is read live from the store to avoid stale closures.
|
|
17506
|
+
(0, $gXNCa$react.useEffect)(()=>{
|
|
17507
|
+
const el = containerRef.current;
|
|
17508
|
+
if (!el || !store) return undefined;
|
|
17509
|
+
const onWheel = (e)=>{
|
|
17510
|
+
if (mode !== 'panzoom') return; // let the page scroll normally
|
|
17511
|
+
e.preventDefault();
|
|
17512
|
+
const t = store.getState().transform || $bfd29d54a0f0e853$var$DEFAULT_TRANSFORM;
|
|
17513
|
+
const rect = el.getBoundingClientRect();
|
|
17514
|
+
const cursorPx = $bfd29d54a0f0e853$var$clamp(e.clientX - rect.left, 0, w);
|
|
17515
|
+
const fracAtCursor = t.leftFrac + cursorPx / w / t.scale;
|
|
17516
|
+
const factor = Math.exp(-e.deltaY * 0.0015);
|
|
17517
|
+
const newScale = $bfd29d54a0f0e853$var$clamp(t.scale * factor, 1, $bfd29d54a0f0e853$var$MAX_SCALE);
|
|
17518
|
+
const newLeft = $bfd29d54a0f0e853$var$clamp(fracAtCursor - cursorPx / w / newScale, 0, 1 - 1 / newScale);
|
|
17519
|
+
store.setTransform({
|
|
17520
|
+
scale: newScale,
|
|
17521
|
+
leftFrac: newLeft
|
|
17522
|
+
});
|
|
17523
|
+
};
|
|
17524
|
+
el.addEventListener('wheel', onWheel, {
|
|
17525
|
+
passive: false
|
|
17526
|
+
});
|
|
17527
|
+
return ()=>el.removeEventListener('wheel', onWheel);
|
|
17528
|
+
}, [
|
|
17529
|
+
store,
|
|
17530
|
+
w,
|
|
17531
|
+
mode
|
|
17532
|
+
]);
|
|
17340
17533
|
const genomeName = (taxonId)=>{
|
|
17341
17534
|
const tax = data.taxonomy && data.taxonomy[taxonId];
|
|
17342
17535
|
return tax && (tax.commonName || tax.scientificName) || String(taxonId);
|
|
17343
17536
|
};
|
|
17344
17537
|
const onRowMouseMove = (e, r)=>{
|
|
17345
17538
|
if (!store || dragRef.current || !layouts[r.nodeId]) return;
|
|
17346
|
-
const reg = $bfd29d54a0f0e853$var$
|
|
17539
|
+
const reg = $bfd29d54a0f0e853$var$regionAtFrac(layouts[r.nodeId], screenToFrac(pxFromEvent(e)));
|
|
17347
17540
|
if (!reg) {
|
|
17348
17541
|
store.setHovered(null);
|
|
17349
17542
|
return;
|
|
@@ -17354,33 +17547,60 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
|
|
|
17354
17547
|
genomeName: genomeName(r.nodeId),
|
|
17355
17548
|
regionName: reg.name,
|
|
17356
17549
|
geneCount: geneCount,
|
|
17357
|
-
|
|
17358
|
-
|
|
17550
|
+
f0: reg.f0,
|
|
17551
|
+
f1: reg.f1
|
|
17359
17552
|
});
|
|
17360
17553
|
};
|
|
17361
|
-
|
|
17362
|
-
|
|
17554
|
+
// Pan drag (panzoom mode): translate leftFrac by the cursor delta, scaled by
|
|
17555
|
+
// the current zoom. Shared transform → every genome row pans together.
|
|
17556
|
+
const startPan = (e)=>{
|
|
17363
17557
|
e.preventDefault();
|
|
17364
17558
|
const startPx = pxFromEvent(e);
|
|
17559
|
+
const t0 = store.getState().transform || $bfd29d54a0f0e853$var$DEFAULT_TRANSFORM;
|
|
17560
|
+
let moved = false;
|
|
17561
|
+
const onMove = (ev)=>{
|
|
17562
|
+
const dx = pxFromEvent(ev) - startPx;
|
|
17563
|
+
if (Math.abs(dx) > 2) moved = true;
|
|
17564
|
+
const t = store.getState().transform || $bfd29d54a0f0e853$var$DEFAULT_TRANSFORM;
|
|
17565
|
+
const newLeft = $bfd29d54a0f0e853$var$clamp(t0.leftFrac - dx / w / t.scale, 0, 1 - 1 / t.scale);
|
|
17566
|
+
store.setTransform({
|
|
17567
|
+
scale: t.scale,
|
|
17568
|
+
leftFrac: newLeft
|
|
17569
|
+
});
|
|
17570
|
+
};
|
|
17571
|
+
const onUp = ()=>{
|
|
17572
|
+
document.removeEventListener('pointermove', onMove);
|
|
17573
|
+
document.removeEventListener('pointerup', onUp);
|
|
17574
|
+
if (moved) suppressClickRef.current = true;
|
|
17575
|
+
};
|
|
17576
|
+
document.addEventListener('pointermove', onMove);
|
|
17577
|
+
document.addEventListener('pointerup', onUp);
|
|
17578
|
+
};
|
|
17579
|
+
// Select drag (select mode): brush a fraction range and add the non-empty
|
|
17580
|
+
// bins under it as a committed selection.
|
|
17581
|
+
const startSelect = (e, r)=>{
|
|
17582
|
+
if (!layouts[r.nodeId]) return;
|
|
17583
|
+
e.preventDefault();
|
|
17584
|
+
const startFrac = screenToFrac(pxFromEvent(e));
|
|
17365
17585
|
dragRef.current = {
|
|
17366
17586
|
taxonId: r.nodeId,
|
|
17367
|
-
|
|
17587
|
+
startFrac: startFrac,
|
|
17368
17588
|
moved: false
|
|
17369
17589
|
};
|
|
17370
17590
|
store.setInProgress({
|
|
17371
17591
|
taxonId: r.nodeId,
|
|
17372
|
-
|
|
17373
|
-
|
|
17592
|
+
f0: startFrac,
|
|
17593
|
+
f1: startFrac
|
|
17374
17594
|
});
|
|
17375
17595
|
const onMove = (ev)=>{
|
|
17376
17596
|
const d = dragRef.current;
|
|
17377
17597
|
if (!d) return;
|
|
17378
|
-
const cur = pxFromEvent(ev);
|
|
17379
|
-
if (Math.abs(cur - d.
|
|
17598
|
+
const cur = screenToFrac(pxFromEvent(ev));
|
|
17599
|
+
if (Math.abs(cur - d.startFrac) * w * transform.scale > 2) d.moved = true;
|
|
17380
17600
|
store.setInProgress({
|
|
17381
17601
|
taxonId: d.taxonId,
|
|
17382
|
-
|
|
17383
|
-
|
|
17602
|
+
f0: Math.min(d.startFrac, cur),
|
|
17603
|
+
f1: Math.max(d.startFrac, cur)
|
|
17384
17604
|
});
|
|
17385
17605
|
};
|
|
17386
17606
|
const onUp = (ev)=>{
|
|
@@ -17391,17 +17611,17 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
|
|
|
17391
17611
|
store.setInProgress(null);
|
|
17392
17612
|
if (d && d.moved) {
|
|
17393
17613
|
suppressClickRef.current = true; // swallow the click that follows a drag
|
|
17394
|
-
const cur = pxFromEvent(ev);
|
|
17395
|
-
const
|
|
17396
|
-
const
|
|
17614
|
+
const cur = screenToFrac(pxFromEvent(ev));
|
|
17615
|
+
const f0 = Math.min(d.startFrac, cur);
|
|
17616
|
+
const f1 = Math.max(d.startFrac, cur);
|
|
17397
17617
|
const lay = layouts[d.taxonId];
|
|
17398
17618
|
if (lay) {
|
|
17399
|
-
const
|
|
17400
|
-
if (
|
|
17619
|
+
const selBins = $bfd29d54a0f0e853$var$selectedBinsInFracRange(lay, f0, f1);
|
|
17620
|
+
if (selBins.length) store.addSelection({
|
|
17401
17621
|
taxonId: d.taxonId,
|
|
17402
|
-
|
|
17403
|
-
|
|
17404
|
-
bins:
|
|
17622
|
+
f0: f0,
|
|
17623
|
+
f1: f1,
|
|
17624
|
+
bins: selBins
|
|
17405
17625
|
});
|
|
17406
17626
|
}
|
|
17407
17627
|
}
|
|
@@ -17409,15 +17629,29 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
|
|
|
17409
17629
|
document.addEventListener('pointermove', onMove);
|
|
17410
17630
|
document.addEventListener('pointerup', onUp);
|
|
17411
17631
|
};
|
|
17632
|
+
const onRowPointerDown = (e, r)=>{
|
|
17633
|
+
if (!store) return;
|
|
17634
|
+
if (mode === 'panzoom') {
|
|
17635
|
+
startPan(e);
|
|
17636
|
+
return;
|
|
17637
|
+
}
|
|
17638
|
+
startSelect(e, r);
|
|
17639
|
+
};
|
|
17412
17640
|
const rowByTaxon = new Map(rows.map((r)=>[
|
|
17413
17641
|
r.nodeId,
|
|
17414
17642
|
r
|
|
17415
17643
|
]));
|
|
17416
|
-
// Outline rectangles (drawn above the canvas; pointer-events:none).
|
|
17417
|
-
//
|
|
17418
|
-
|
|
17644
|
+
// Outline rectangles (drawn above the canvas; pointer-events:none). Stored in
|
|
17645
|
+
// fraction space and projected to screen px under the transform, clipped to
|
|
17646
|
+
// the visible strip. The y comes from the current row so they track scroll.
|
|
17647
|
+
const outline = (key, taxonId, f0, f1, color, dashed)=>{
|
|
17419
17648
|
const r = rowByTaxon.get(taxonId);
|
|
17420
17649
|
if (!r) return null;
|
|
17650
|
+
let x0 = fracToScreen(f0);
|
|
17651
|
+
let x1 = fracToScreen(f1);
|
|
17652
|
+
if (x1 <= 0 || x0 >= w) return null; // fully outside the visible window
|
|
17653
|
+
x0 = Math.max(0, x0);
|
|
17654
|
+
x1 = Math.min(w, x1);
|
|
17421
17655
|
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
|
|
17422
17656
|
style: {
|
|
17423
17657
|
position: 'absolute',
|
|
@@ -17431,6 +17665,7 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
|
|
|
17431
17665
|
}
|
|
17432
17666
|
}, key);
|
|
17433
17667
|
};
|
|
17668
|
+
const rowCursor = mode === 'panzoom' ? 'grab' : 'crosshair';
|
|
17434
17669
|
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
|
|
17435
17670
|
ref: containerRef,
|
|
17436
17671
|
style: {
|
|
@@ -17461,7 +17696,7 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
|
|
|
17461
17696
|
right: 0,
|
|
17462
17697
|
height: r.height,
|
|
17463
17698
|
background: $bfd29d54a0f0e853$var$rowHighlight(selectedNodeId === r.nodeId, hoveredNodeId === r.nodeId, !!(hoveredSubtreeIds && hoveredSubtreeIds.has(r.nodeId))),
|
|
17464
|
-
cursor:
|
|
17699
|
+
cursor: rowCursor,
|
|
17465
17700
|
opacity: r.opacity ?? 1
|
|
17466
17701
|
}
|
|
17467
17702
|
}, r.nodeId)),
|
|
@@ -17476,9 +17711,9 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
|
|
|
17476
17711
|
pointerEvents: 'none'
|
|
17477
17712
|
}
|
|
17478
17713
|
}),
|
|
17479
|
-
snap.hovered && !snap.inProgress && outline('hover', snap.hovered.taxonId, snap.hovered.
|
|
17480
|
-
snap.selections.map((s, i)=>outline(`sel-${i}`, s.taxonId, s.
|
|
17481
|
-
snap.inProgress && outline('drag', snap.inProgress.taxonId, snap.inProgress.
|
|
17714
|
+
snap.hovered && !snap.inProgress && outline('hover', snap.hovered.taxonId, snap.hovered.f0, snap.hovered.f1, 'var(--tbrowse-accent, #2878dc)', false),
|
|
17715
|
+
snap.selections.map((s, i)=>outline(`sel-${i}`, s.taxonId, s.f0, s.f1, '#d62728', false)),
|
|
17716
|
+
snap.inProgress && outline('drag', snap.inProgress.taxonId, snap.inProgress.f0, snap.inProgress.f1, '#d62728', true)
|
|
17482
17717
|
]
|
|
17483
17718
|
});
|
|
17484
17719
|
};
|
|
@@ -17489,7 +17724,9 @@ const $bfd29d54a0f0e853$export$d85d069f2280460c = {
|
|
|
17489
17724
|
Body: $bfd29d54a0f0e853$var$BinsBody,
|
|
17490
17725
|
defaultWidth: 60,
|
|
17491
17726
|
minWidth: 200,
|
|
17492
|
-
defaultZoneState: {
|
|
17727
|
+
defaultZoneState: {
|
|
17728
|
+
mode: 'select'
|
|
17729
|
+
},
|
|
17493
17730
|
isAvailable: (data)=>Boolean(data.hostData && data.hostData.bins && data.hostData.bins.genomesByTaxon && Object.keys(data.hostData.bins.genomesByTaxon).length > 0),
|
|
17494
17731
|
defaultVisible: true
|
|
17495
17732
|
};
|
|
@@ -17724,7 +17961,7 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
|
|
|
17724
17961
|
const binsUiRef = (0, $gXNCa$react.useRef)(null);
|
|
17725
17962
|
if (!binsUiRef.current) binsUiRef.current = (0, $bfd29d54a0f0e853$export$f06340802363875a)();
|
|
17726
17963
|
(0, $gXNCa$react.useEffect)(()=>{
|
|
17727
|
-
binsUiRef.current.
|
|
17964
|
+
binsUiRef.current.reset();
|
|
17728
17965
|
}, [
|
|
17729
17966
|
grameneTaxDist
|
|
17730
17967
|
]);
|