@weng-lab/genomebrowser-ui 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,8 +12,11 @@ import {
12
12
  useRowHeight,
13
13
  useXTransform,
14
14
  Vibrant,
15
+ useTrackStore,
15
16
  } from "@weng-lab/genomebrowser";
16
- import { useMemo } from "react";
17
+ import { DNALogo } from "logo-test";
18
+ import { useMemo, useState } from "react";
19
+ import motifData from "./TF-ChIP-Canonical-Motifs-w-Trimmed.json";
17
20
 
18
21
  // --- Types ---
19
22
 
@@ -25,6 +28,7 @@ type OverlayData = {
25
28
  type OverlayInteractionRect = Rect & {
26
29
  source: "base" | "overlay";
27
30
  matchedName?: string;
31
+ pwm?: number[][];
28
32
  };
29
33
 
30
34
  // The config type — extra fields go directly on the config
@@ -33,6 +37,7 @@ type OverlayBigBedConfig = CustomTrackConfig<OverlayInteractionRect> & {
33
37
  overlayUrl: string;
34
38
  baseColor?: string;
35
39
  overlayColor?: string;
40
+ filter?: string[];
36
41
  };
37
42
 
38
43
  // --- Helpers ---
@@ -70,6 +75,113 @@ function darkenHexColor(color: string, score?: number) {
70
75
  return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
71
76
  }
72
77
 
78
+ // --- Motif data lookup ---
79
+
80
+ type MotifEntry = { trimmed_ppm: number[][]; ppm: number[][] };
81
+ const motifLookup = motifData as Record<string, MotifEntry>;
82
+
83
+ function lookupPwm(name?: string): number[][] | undefined {
84
+ const key = nameKey(name)?.toUpperCase();
85
+ if (!key) return undefined;
86
+ const entry = motifLookup[key];
87
+ return entry?.trimmed_ppm ?? entry?.ppm;
88
+ }
89
+
90
+ // --- Tooltip ---
91
+
92
+ function tfDisplayName(name?: string): string {
93
+ return nameKey(name)?.toUpperCase() || name || "Unknown";
94
+ }
95
+
96
+ function TfPeaksTooltip(rect: OverlayInteractionRect) {
97
+ const pwm = rect.pwm;
98
+ const label = tfDisplayName(rect.name);
99
+ if (!pwm || pwm.length === 0) {
100
+ return (
101
+ <g>
102
+ <rect
103
+ width={120}
104
+ height={24}
105
+ fill="white"
106
+ rx={2}
107
+ style={{ filter: "drop-shadow(0px 0px 4px rgba(0,0,0,0.25))" }}
108
+ />
109
+ <text x={6} y={16} fontSize={12} fill="#333">
110
+ {label}
111
+ </text>
112
+ </g>
113
+ );
114
+ }
115
+ const logoWidth = pwm.length * 15;
116
+ const logoHeight = 130;
117
+ const totalHeight = logoHeight - 5;
118
+ return (
119
+ <g transform={`translate(0, ${-totalHeight})`}>
120
+ <rect
121
+ width={logoWidth + 10}
122
+ height={totalHeight}
123
+ fill="white"
124
+ rx={3}
125
+ style={{ filter: "drop-shadow(0px 0px 5px rgba(0,0,0,0.3))" }}
126
+ />
127
+ <text x={5} y={16} fontSize={12} fontWeight="bold" fill="#333">
128
+ {label}
129
+ </text>
130
+ <g transform="translate(5, 5)">
131
+ <DNALogo
132
+ ppm={pwm}
133
+ mode="INFORMATION_CONTENT"
134
+ width={logoWidth}
135
+ height={logoHeight}
136
+ />
137
+ </g>
138
+ </g>
139
+ );
140
+ }
141
+
142
+ // --- Settings Panel ---
143
+
144
+ function TfPeaksSettings({ id }: { id: string }) {
145
+ const track = useTrackStore((state) => state.getTrack(id)) as
146
+ | OverlayBigBedConfig
147
+ | undefined;
148
+ const editTrack = useTrackStore((state) => state.editTrack);
149
+ const [input, setInput] = useState((track?.filter || []).join(", "));
150
+
151
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
152
+ const value = e.target.value;
153
+ setInput(value);
154
+ const names = value
155
+ .split(",")
156
+ .map((s) => s.trim().toUpperCase())
157
+ .filter(Boolean);
158
+ editTrack<OverlayBigBedConfig>(id, {
159
+ filter: names.length > 0 ? names : undefined,
160
+ });
161
+ };
162
+
163
+ return (
164
+ <div
165
+ style={{
166
+ display: "flex",
167
+ flexDirection: "column",
168
+ alignItems: "flex-start",
169
+ paddingBlock: "5px",
170
+ paddingInline: "10px",
171
+ gap: "3px",
172
+ }}
173
+ >
174
+ <div style={{ fontWeight: "bold" }}>Filter TFs</div>
175
+ <input
176
+ value={input}
177
+ onChange={handleChange}
178
+ placeholder="e.g. CTCF, TP53, GATA1"
179
+ style={{ width: "100%" }}
180
+ />
181
+ </div>
182
+ );
183
+ }
184
+
73
185
  // --- Renderer ---
74
186
 
75
187
  function OverlayBigBedRenderer(
@@ -83,18 +195,32 @@ function OverlayBigBedRenderer(
83
195
  color,
84
196
  baseColor: baseColorProp,
85
197
  overlayColor: overlayColorProp,
198
+ filter,
86
199
  ...rest
87
200
  } = props;
88
201
  const domain = useBrowserStore((state) => state.domain);
89
202
  const { totalWidth, sideWidth } = dimensions;
90
203
  const { x, reverseX } = useXTransform(totalWidth);
91
204
 
205
+ const filterSet = useMemo(
206
+ () =>
207
+ filter && filter.length > 0
208
+ ? new Set(filter.map((f) => f.toUpperCase()))
209
+ : null,
210
+ [filter],
211
+ );
212
+
92
213
  const rows = useMemo(() => {
93
- const visible = (data?.primary || []).filter(
214
+ let visible = (data?.primary || []).filter(
94
215
  (r) => r.end >= domain.start && r.start <= domain.end,
95
216
  );
217
+ if (filterSet) {
218
+ visible = visible.filter((r) =>
219
+ filterSet.has(nameKey(r.name)?.toUpperCase()),
220
+ );
221
+ }
96
222
  return renderSquishBigBedData(visible, x);
97
- }, [data, domain.end, domain.start, x]);
223
+ }, [data, domain.end, domain.start, x, filterSet]);
98
224
 
99
225
  const rowHeight = useRowHeight(rows.length, id);
100
226
  const height = Math.max(trackHeight, rowHeight * Math.max(rows.length, 1));
@@ -151,6 +277,7 @@ function OverlayBigBedRenderer(
151
277
  name: baseName,
152
278
  color: fill,
153
279
  score: rect.score,
280
+ pwm: lookupPwm(baseName),
154
281
  };
155
282
 
156
283
  return (
@@ -180,6 +307,7 @@ function OverlayBigBedRenderer(
180
307
  color: a.color || overlayColor,
181
308
  score: a.score,
182
309
  matchedName: baseName,
310
+ pwm: lookupPwm(a.name),
183
311
  };
184
312
  return (
185
313
  <rect
@@ -228,6 +356,8 @@ export const tfPeaksTrack: OverlayBigBedConfig = {
228
356
  overlayUrl: DECORATOR_BIGBED_URL,
229
357
  baseColor: "#d1d5db",
230
358
  overlayColor: "#1e3a8a",
359
+ tooltip: TfPeaksTooltip,
360
+ settingsPanel: TfPeaksSettings,
231
361
  renderers: {
232
362
  [DisplayMode.Full]: OverlayBigBedRenderer,
233
363
  },
package/src/lib.ts CHANGED
@@ -15,5 +15,3 @@ export type {
15
15
  GeneRowInfo,
16
16
  OtherTrackInfo,
17
17
  } from "./TrackSelect/Folders";
18
-
19
- export { tfPeaksTrack } from "./TrackSelect/CustomTracks/TfPeaks";
package/test/main.tsx CHANGED
@@ -37,8 +37,8 @@ import { foldersByAssembly, TrackSelect } from "../src/lib";
37
37
  import type { BiosampleRowInfo } from "../src/TrackSelect/Folders/biosamples/shared/types";
38
38
  import type { GeneRowInfo } from "../src/TrackSelect/Folders/genes/shared/types";
39
39
  import type { OtherTrackInfo } from "../src/TrackSelect/Folders/other-tracks/shared/types";
40
- import { tfPeaksTrack } from "../src/TrackSelect/CustomTracks/TfPeaks";
41
40
  import { Exon } from "@weng-lab/genomebrowser/dist/components/tracks/transcript/types";
41
+ import { tfPeaksTrack } from "@weng-lab/genomebrowser/test/TfPeaks";
42
42
 
43
43
  interface Transcript {
44
44
  id: string;