diff --git a/.gitignore b/.gitignore index b210285..a6855cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_Store __pycache__/ *.egg-info/ diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 6fed4b5..99c8cf4 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -502,6 +502,63 @@ light-weight HiGlass server in a *background thread*. This temporary server is only started if a local tileset is used and will only persist for the duration of the Python session. +Local Data with Plugin Tracks +""""""""""""""""""""""""""""" + +For regular or plugin tracks that support local data, you can use the ``local_data()`` method +to provide tileset info and tile data directly without needing a server. This is +particularly useful for small datasets or custom visualizations. + +.. code-block:: python + + from typing import Literal, ClassVar + import higlass as hg + + class LabelledPointsTrack(hg.PluginTrack): + type: Literal["labelled-points-track"] = "labelled-points-track" + plugin_url: ClassVar[str] = ( + "https://unpkg.com/higlass-labelled-points-track@0.5.1/" + "dist/higlass-labelled-points-track.min.js" + ) + + # Create track with local data + track = LabelledPointsTrack().local_data( + tsinfo={ + "zoom_step": 1, + "tile_size": 256, + "max_zoom": 0, + "min_pos": [-180, -180], + "max_pos": [180, 180], + "max_data_length": 134217728, + "max_width": 360 + }, + data=[ + { + "x": -122.29340351667594, + "y": -40.90076029033937, + "size": 10, + "data": "Fraxinus o. 'Raywood'", + "uid": "Eq71PfMlR0aqHd2zSlDclA" + }, + { + "x": -122.18808936055962, + "y": -40.805069480744535, + "size": 20, + "data": "Acer rubrum", + "uid": "DYKKOHNuRBmEPdTip0pyDw" + }, + { + "x": -122.23773953309676, + "y": -40.885281196424444, + "size": 1, + "data": "Other", + "uid": "b04PPSR6Ti28MJPpzXpAuA" + }, + ] + ) + + hg.view(track) + Cooler Files """""""""""" diff --git a/src/higlass/__init__.py b/src/higlass/__init__.py index c899211..3b534bd 100644 --- a/src/higlass/__init__.py +++ b/src/higlass/__init__.py @@ -31,6 +31,7 @@ ) from higlass.server import HiGlassServer from higlass.tilesets import ( + LocalDataTileset, Tileset, bed2ddb, beddb, diff --git a/src/higlass/tilesets.py b/src/higlass/tilesets.py index 5551619..77856ee 100644 --- a/src/higlass/tilesets.py +++ b/src/higlass/tilesets.py @@ -11,6 +11,7 @@ from higlass._utils import TrackType, datatype_default_track __all__ = [ + "LocalDataTileset", "Tileset", "bed2ddb", "bigwig", @@ -71,6 +72,61 @@ def remote( return RemoteTileset(uid, server, name) +@dataclass +class LocalDataTileset: + """A tileset that serves data locally without a server. + + Parameters + ---------- + tsinfo : dict + Tileset info dict (must include ``min_pos`` and ``max_pos``). + data : list + Tile data for the tileset. + """ + + tsinfo: dict + data: list + + def __post_init__(self): + min_pos = self.tsinfo.get("min_pos", []) + max_pos = self.tsinfo.get("max_pos", []) + + if len(min_pos) != len(max_pos): + raise ValueError("min_pos and max_pos must have equal lengths") + + if len(min_pos) == 2: + self._tile_key = "x.0.0.0" + elif len(min_pos) == 1: + self._tile_key = "x.0.0" + else: + raise ValueError("min_pos must be a one or two element array") + + def track(self, type_: TrackType, **kwargs) -> higlass.api.Track: + """Create a HiGlass track with local data embedded. + + Parameters + ---------- + type_ : TrackType + The track type to create. + **kwargs : dict + Additional top-level track properties. + + Returns + ------- + higlass.api.Track + A track with the ``data`` section populated for local-tiles. + """ + return higlass.api.track( + type_=type_, + data=dict( + type="local-tiles", + tilesetInfo={"x": self.tsinfo}, + tiles={self._tile_key: self.data}, + ), + **kwargs, + ) + + class Tileset(abc.ABC): """Base class for defining custom tilesets in `higlass`. diff --git a/test/test_api.py b/test/test_api.py index ea84c90..34b0e7d 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -209,6 +209,28 @@ def test_options_mixin(): assert track.options and track.options["foo"] == "bar" +def test_local_data_tileset(): + tsinfo = {"min_pos": [0, 0], "max_pos": [100, 100]} + data = [{"x": 1, "y": 2}] + + tileset = hg.LocalDataTileset(tsinfo, data) + track = tileset.track("heatmap") + assert track.data.type == "local-tiles" # ty: ignore[unresolved-attribute] + assert track.data.tilesetInfo["x"] == tsinfo # ty: ignore[unresolved-attribute,not-subscriptable] + assert track.data.tiles["x.0.0.0"] == data # ty: ignore[unresolved-attribute,not-subscriptable] + + tsinfo_1d = {"min_pos": [0], "max_pos": [100]} + tileset_1d = hg.LocalDataTileset(tsinfo_1d, data) + track_1d = tileset_1d.track("heatmap") + assert track_1d.data.tiles["x.0.0"] == data # ty: ignore[unresolved-attribute,not-subscriptable] + + with pytest.raises(ValueError, match="min_pos and max_pos must have equal lengths"): + hg.LocalDataTileset({"min_pos": [0], "max_pos": [0, 0]}, data) + + with pytest.raises(ValueError, match="min_pos must be a one or two element array"): + hg.LocalDataTileset({"min_pos": [0, 0, 0], "max_pos": [0, 0, 0]}, data) + + def test_plugin_track(): """Test that plugin track attributes are maintained after a copy.""" some_url = "https://some_url" @@ -227,7 +249,7 @@ class PileupTrack(hg.PluginTrack): } # Create and use the custom track - pileup_track = PileupTrack(data=pileup_data) # ty:ignore[unknown-argument] + pileup_track = PileupTrack(data=pileup_data) # ty: ignore[unknown-argument] view = hg.view((pileup_track, "top")) uid1 = view.uid