Skip to content

Commit 1767413

Browse files
pkerpedjievnvictus
andauthored
Add an API to add local data to tracks (#200)
* feat: Add an API for adding local data * Updated the docs * Formatting fix * Bumped higlass version * Fixed linting errors * Fixed formatting * Changed to use a tileset rather than track constructor for local tile data * Restore widget.js from main after botched rebase * Update gitignore * Suppress ty errors on unresolved attributes * Renamed LocalDataTileset to InlineTileset * Fixed tests * Fix typing --------- Co-authored-by: Nezar Abdennur <nabdennur@gmail.com>
1 parent 815e4aa commit 1767413

6 files changed

Lines changed: 145 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.DS_Store
12
__pycache__/
23

34
*.egg-info/

docs/getting_started.rst

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,63 @@ light-weight HiGlass server in a *background thread*. This temporary
502502
server is only started if a local tileset is used and will only persist
503503
for the duration of the Python session.
504504

505+
Local Data with Plugin Tracks
506+
"""""""""""""""""""""""""""""
507+
508+
For regular or plugin tracks that support local data, you can use the ``local_data()`` method
509+
to provide tileset info and tile data directly without needing a server. This is
510+
particularly useful for small datasets or custom visualizations.
511+
512+
.. code-block:: python
513+
514+
from typing import Literal, ClassVar
515+
import higlass as hg
516+
517+
class LabelledPointsTrack(hg.PluginTrack):
518+
type: Literal["labelled-points-track"] = "labelled-points-track"
519+
plugin_url: ClassVar[str] = (
520+
"https://unpkg.com/higlass-labelled-points-track@0.5.1/"
521+
"dist/higlass-labelled-points-track.min.js"
522+
)
523+
524+
# Create track with local data
525+
track = LabelledPointsTrack().local_data(
526+
tsinfo={
527+
"zoom_step": 1,
528+
"tile_size": 256,
529+
"max_zoom": 0,
530+
"min_pos": [-180, -180],
531+
"max_pos": [180, 180],
532+
"max_data_length": 134217728,
533+
"max_width": 360
534+
},
535+
data=[
536+
{
537+
"x": -122.29340351667594,
538+
"y": -40.90076029033937,
539+
"size": 10,
540+
"data": "Fraxinus o. 'Raywood'",
541+
"uid": "Eq71PfMlR0aqHd2zSlDclA"
542+
},
543+
{
544+
"x": -122.18808936055962,
545+
"y": -40.805069480744535,
546+
"size": 20,
547+
"data": "Acer rubrum",
548+
"uid": "DYKKOHNuRBmEPdTip0pyDw"
549+
},
550+
{
551+
"x": -122.23773953309676,
552+
"y": -40.885281196424444,
553+
"size": 1,
554+
"data": "Other",
555+
"uid": "b04PPSR6Ti28MJPpzXpAuA"
556+
},
557+
]
558+
)
559+
560+
hg.view(track)
561+
505562
Cooler Files
506563
""""""""""""
507564

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ extend-select = [
6969
]
7070

7171
[tool.ty.src]
72-
exclude = ["examples/Plugin.ipynb", "docs/conf.py", "docs/build.py"]
72+
exclude = ["**/*.ipynb", "docs/conf.py", "docs/build.py"]
7373

7474
[tool.uv]
7575
required-version = ">=0.10.0"

src/higlass/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
)
3232
from higlass.server import HiGlassServer
3333
from higlass.tilesets import (
34+
InlineTileset,
3435
Tileset,
3536
bed2ddb,
3637
beddb,

src/higlass/tilesets.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from higlass._utils import TrackType, datatype_default_track
1212

1313
__all__ = [
14+
"InlineTileset",
1415
"Tileset",
1516
"bed2ddb",
1617
"bigwig",
@@ -71,6 +72,61 @@ def remote(
7172
return RemoteTileset(uid, server, name)
7273

7374

75+
@dataclass
76+
class InlineTileset:
77+
"""A tileset that serves data locally without a server.
78+
79+
Parameters
80+
----------
81+
tsinfo : dict
82+
Tileset info dict (must include ``min_pos`` and ``max_pos``).
83+
data : list
84+
Tile data for the tileset.
85+
"""
86+
87+
tsinfo: dict
88+
data: list
89+
90+
def __post_init__(self):
91+
min_pos = self.tsinfo.get("min_pos", [])
92+
max_pos = self.tsinfo.get("max_pos", [])
93+
94+
if len(min_pos) != len(max_pos):
95+
raise ValueError("min_pos and max_pos must have equal lengths")
96+
97+
if len(min_pos) == 2:
98+
self._tile_key = "x.0.0.0"
99+
elif len(min_pos) == 1:
100+
self._tile_key = "x.0.0"
101+
else:
102+
raise ValueError("min_pos must be a one or two element array")
103+
104+
def track(self, type_: TrackType, **kwargs) -> higlass.api.Track:
105+
"""Create a HiGlass track with local data embedded.
106+
107+
Parameters
108+
----------
109+
type_ : TrackType
110+
The track type to create.
111+
**kwargs : dict
112+
Additional top-level track properties.
113+
114+
Returns
115+
-------
116+
higlass.api.Track
117+
A track with the ``data`` section populated for local-tiles.
118+
"""
119+
return higlass.api.track(
120+
type_=type_,
121+
data=dict(
122+
type="local-tiles",
123+
tilesetInfo={"x": self.tsinfo},
124+
tiles={self._tile_key: self.data},
125+
),
126+
**kwargs,
127+
)
128+
129+
74130
class Tileset(abc.ABC):
75131
"""Base class for defining custom tilesets in `higlass`.
76132

test/test_api.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,34 @@ def test_options_mixin():
209209
assert track.options and track.options["foo"] == "bar"
210210

211211

212+
def test_local_data_tileset():
213+
tsinfo = {"min_pos": [0, 0], "max_pos": [100, 100]}
214+
data = [{"x": 1, "y": 2}]
215+
216+
tileset = hg.InlineTileset(tsinfo, data)
217+
track = tileset.track("heatmap")
218+
assert track.data.type == "local-tiles" # ty: ignore[unresolved-attribute]
219+
assert (
220+
track.data.tilesetInfo["x"] == tsinfo # ty: ignore[unresolved-attribute,not-subscriptable]
221+
)
222+
assert (
223+
track.data.tiles["x.0.0.0"] == data # ty: ignore[unresolved-attribute,not-subscriptable]
224+
)
225+
226+
tsinfo_1d = {"min_pos": [0], "max_pos": [100]}
227+
tileset_1d = hg.InlineTileset(tsinfo_1d, data)
228+
track_1d = tileset_1d.track("heatmap")
229+
assert (
230+
track_1d.data.tiles["x.0.0"] == data # ty: ignore[unresolved-attribute,not-subscriptable]
231+
)
232+
233+
with pytest.raises(ValueError, match="min_pos and max_pos must have equal lengths"):
234+
hg.InlineTileset({"min_pos": [0], "max_pos": [0, 0]}, data)
235+
236+
with pytest.raises(ValueError, match="min_pos must be a one or two element array"):
237+
hg.InlineTileset({"min_pos": [0, 0, 0], "max_pos": [0, 0, 0]}, data)
238+
239+
212240
def test_plugin_track():
213241
"""Test that plugin track attributes are maintained after a copy."""
214242
some_url = "https://some_url"
@@ -227,7 +255,7 @@ class PileupTrack(hg.PluginTrack):
227255
}
228256

229257
# Create and use the custom track
230-
pileup_track = PileupTrack(data=pileup_data) # ty:ignore[unknown-argument]
258+
pileup_track = PileupTrack(data=pileup_data) # ty: ignore[unknown-argument]
231259

232260
view = hg.view((pileup_track, "top"))
233261
uid1 = view.uid

0 commit comments

Comments
 (0)