From 30b505b84fe9a5922f7eb20f60f6567bbd6c24da Mon Sep 17 00:00:00 2001 From: Peter Kerpedjiev Date: Sun, 22 Feb 2026 08:28:25 -0800 Subject: [PATCH 1/8] feat: Add an API for adding local data --- src/higlass/api.py | 49 ++++++++++++++++++++++++++++++++++++++++++++-- test/test_api.py | 26 ++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/higlass/api.py b/src/higlass/api.py index 3fe3473..de00001 100644 --- a/src/higlass/api.py +++ b/src/higlass/api.py @@ -120,16 +120,61 @@ def opts( return track +class _LocalDataMixin: + def local_data( + self: TrackT, # type: ignore + tsinfo, + data, + inplace: bool = False, + ) -> TrackT: # type: ignore + """Configures local data for a Track. + + Parameters + ---------- + tsinfo : dict + Tileset info to be placed under ["tilesetInfo"]["x"]. + data : list + Tile data to be placed under ["tiles"]. + inplace : bool, optional + Whether to modify the existing track in place or return + a new track with the data applied (default: `False`). + + Returns + ------- + track : A track with local data configured. + """ + min_pos = tsinfo.get("min_pos", []) + max_pos = 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: + tile_key = "x.0.0.0" + elif len(min_pos) == 1: + tile_key = "x.0.0" + else: + raise ValueError("min_pos must be a one or two element array") + + track = self if inplace else utils.copy_unique(self) + track.data = { + "type": "local-tiles", + "tilesetInfo": {"x": tsinfo}, + "tiles": {tile_key: data}, + } + return track + + ## Extend higlass-schema classes -class EnumTrack(hgs.EnumTrack, _OptionsMixin, _PropertiesMixin): +class EnumTrack(hgs.EnumTrack, _LocalDataMixin, _OptionsMixin, _PropertiesMixin): """Represents a generic track.""" ... -class HeatmapTrack(hgs.HeatmapTrack, _OptionsMixin, _PropertiesMixin): +class HeatmapTrack(hgs.HeatmapTrack, _LocalDataMixin, _OptionsMixin, _PropertiesMixin): """Represets a specialized heatmap track.""" ... diff --git a/test/test_api.py b/test/test_api.py index ea84c90..abb6211 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -209,6 +209,32 @@ def test_options_mixin(): assert track.options and track.options["foo"] == "bar" +def test_local_data_mixin(): + track = hg.track("heatmap") + tsinfo = {"min_pos": [0, 0], "max_pos": [100, 100]} + data = [{"x": 1, "y": 2}] + + other = track.local_data(tsinfo, data) + assert track.uid != other.uid + assert track.data is None + assert other.data.type == "local-tiles" + assert other.data.tilesetInfo["x"] == tsinfo + assert other.data.tiles["x.0.0.0"] == data + + track2 = hg.track("heatmap") + tsinfo_1d = {"min_pos": [0], "max_pos": [100]} + other2 = track2.local_data(tsinfo_1d, data, inplace=True) + assert track2 is other2 + assert track2.data.tiles["x.0.0"] == data + + with pytest.raises(ValueError, match="min_pos and max_pos must have equal lengths"): + hg.track("heatmap").local_data({"min_pos": [0], "max_pos": [0, 0]}, data) + + with pytest.raises(ValueError, match="min_pos must be a one or two element array"): + hg.track("heatmap").local_data({"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" From 044a371979fb23d0bf06fe92bd9f322f97ee428a Mon Sep 17 00:00:00 2001 From: Peter Kerpedjiev Date: Sun, 22 Feb 2026 08:35:43 -0800 Subject: [PATCH 2/8] Updated the docs --- docs/getting_started.rst | 57 ++++++++++++++++++++++++++++++++++++++++ src/higlass/api.py | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) 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/api.py b/src/higlass/api.py index de00001..c992c04 100644 --- a/src/higlass/api.py +++ b/src/higlass/api.py @@ -196,7 +196,7 @@ class CombinedTrack(hgs.CombinedTrack, _OptionsMixin, _PropertiesMixin): ... -class PluginTrack(hgs.BaseTrack, _OptionsMixin, _PropertiesMixin): +class PluginTrack(hgs.BaseTrack, _OptionsMixin, _PropertiesMixin, _LocalDataMixin): """Represents an unknown plugin track.""" plugin_url: ClassVar[str] From 8ba9e92853576c8806e933addd1d434ac7e4e1db Mon Sep 17 00:00:00 2001 From: Peter Kerpedjiev Date: Sun, 22 Feb 2026 16:16:56 -0800 Subject: [PATCH 3/8] Formatting fix --- test/test_api.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/test_api.py b/test/test_api.py index abb6211..6ace2a2 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -213,26 +213,27 @@ def test_local_data_mixin(): track = hg.track("heatmap") tsinfo = {"min_pos": [0, 0], "max_pos": [100, 100]} data = [{"x": 1, "y": 2}] - + other = track.local_data(tsinfo, data) assert track.uid != other.uid assert track.data is None assert other.data.type == "local-tiles" assert other.data.tilesetInfo["x"] == tsinfo assert other.data.tiles["x.0.0.0"] == data - + track2 = hg.track("heatmap") tsinfo_1d = {"min_pos": [0], "max_pos": [100]} other2 = track2.local_data(tsinfo_1d, data, inplace=True) assert track2 is other2 assert track2.data.tiles["x.0.0"] == data - + with pytest.raises(ValueError, match="min_pos and max_pos must have equal lengths"): hg.track("heatmap").local_data({"min_pos": [0], "max_pos": [0, 0]}, data) - - with pytest.raises(ValueError, match="min_pos must be a one or two element array"): - hg.track("heatmap").local_data({"min_pos": [0, 0, 0], "max_pos": [0, 0, 0]}, data) + with pytest.raises(ValueError, match="min_pos must be a one or two element array"): + hg.track("heatmap").local_data( + {"min_pos": [0, 0, 0], "max_pos": [0, 0, 0]}, data + ) def test_plugin_track(): From 0ad25c7b7ba7b5cd9c311cfdbd2d57c937e8533b Mon Sep 17 00:00:00 2001 From: Peter Kerpedjiev Date: Sun, 22 Feb 2026 20:29:31 -0800 Subject: [PATCH 4/8] Bumped higlass version --- src/higlass/widget.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/higlass/widget.js b/src/higlass/widget.js index 6d108ec..dec0fdd 100644 --- a/src/higlass/widget.js +++ b/src/higlass/widget.js @@ -1,4 +1,8 @@ +<<<<<<< HEAD import * as hglib from "https://esm.sh/higlass@2.2.3?deps=react@17,react-dom@17,pixi.js@6"; +======= +import * as hglib from "https://esm.sh/higlass@2.2.1?deps=react@17,react-dom@17,pixi.js@6"; +>>>>>>> 59fdefa (Bumped higlass version) import { v4 } from "https://esm.sh/@lukeed/uuid@2.0.1"; /** @import { AnyModel } from "@anywidget/types" */ From 653bfdc4320e3a763e63c810075b460c9189d70a Mon Sep 17 00:00:00 2001 From: Peter Kerpedjiev Date: Sun, 22 Feb 2026 22:32:23 -0800 Subject: [PATCH 5/8] Fixed linting errors --- src/higlass/widget.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/higlass/widget.js b/src/higlass/widget.js index dec0fdd..9bbfd40 100644 --- a/src/higlass/widget.js +++ b/src/higlass/widget.js @@ -330,6 +330,7 @@ export default { }); if (viewconf.views.length === 1) { +<<<<<<< HEAD api.on( "location", (/** @type {GenomicLocation} */ loc) => { @@ -352,6 +353,20 @@ export default { view.uid, undefined, ); +======= + api.on("location", (/** @type {GenomicLocation} */ loc) => { + model.set("location", locationToCoordinates(loc)); + model.save_changes(); + }, viewconf.views[0].uid, "location-listener"); + } else { + viewconf.views.forEach((view, idx) => { + api.on("location", (/** @type{GenomicLocation} */ loc) => { + let location = model.get("location").slice(); + location[idx] = locationToCoordinates(loc); + model.set("location", location); + model.save_changes(); + }, view.uid, `location-listener-${idx}`); +>>>>>>> f7c33b2 (Fixed linting errors) }); } From fe8491cb4d4f984665f9451b023eac5ec9524837 Mon Sep 17 00:00:00 2001 From: Peter Kerpedjiev Date: Sun, 22 Feb 2026 22:39:24 -0800 Subject: [PATCH 6/8] Fixed formatting --- src/higlass/widget.js | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/higlass/widget.js b/src/higlass/widget.js index 9bbfd40..fa04dcc 100644 --- a/src/higlass/widget.js +++ b/src/higlass/widget.js @@ -1,8 +1,4 @@ -<<<<<<< HEAD import * as hglib from "https://esm.sh/higlass@2.2.3?deps=react@17,react-dom@17,pixi.js@6"; -======= -import * as hglib from "https://esm.sh/higlass@2.2.1?deps=react@17,react-dom@17,pixi.js@6"; ->>>>>>> 59fdefa (Bumped higlass version) import { v4 } from "https://esm.sh/@lukeed/uuid@2.0.1"; /** @import { AnyModel } from "@anywidget/types" */ @@ -331,6 +327,9 @@ export default { if (viewconf.views.length === 1) { <<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> b79a7e6 (Fixed formatting) api.on( "location", (/** @type {GenomicLocation} */ loc) => { @@ -338,6 +337,7 @@ export default { model.save_changes(); }, viewconf.views[0].uid, +<<<<<<< HEAD undefined, ); } else { @@ -367,6 +367,23 @@ export default { model.save_changes(); }, view.uid, `location-listener-${idx}`); >>>>>>> f7c33b2 (Fixed linting errors) +======= + "location-listener", + ); + } else { + viewconf.views.forEach((view, idx) => { + api.on( + "location", + (/** @type{GenomicLocation} */ loc) => { + let location = model.get("location").slice(); + location[idx] = locationToCoordinates(loc); + model.set("location", location); + model.save_changes(); + }, + view.uid, + `location-listener-${idx}`, + ); +>>>>>>> b79a7e6 (Fixed formatting) }); } From 339d5067752950ef816bb36f446bac5b5475d85a Mon Sep 17 00:00:00 2001 From: Peter Kerpedjiev Date: Mon, 23 Feb 2026 20:48:36 -0800 Subject: [PATCH 7/8] Changed to use a tileset rather than track constructor for local tile data --- src/higlass/__init__.py | 1 + src/higlass/api.py | 51 +++----------------------------------- src/higlass/tilesets.py | 54 +++++++++++++++++++++++++++++++++++++++++ test/test_api.py | 21 ++++++---------- 4 files changed, 66 insertions(+), 61 deletions(-) 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/api.py b/src/higlass/api.py index c992c04..3fe3473 100644 --- a/src/higlass/api.py +++ b/src/higlass/api.py @@ -120,61 +120,16 @@ def opts( return track -class _LocalDataMixin: - def local_data( - self: TrackT, # type: ignore - tsinfo, - data, - inplace: bool = False, - ) -> TrackT: # type: ignore - """Configures local data for a Track. - - Parameters - ---------- - tsinfo : dict - Tileset info to be placed under ["tilesetInfo"]["x"]. - data : list - Tile data to be placed under ["tiles"]. - inplace : bool, optional - Whether to modify the existing track in place or return - a new track with the data applied (default: `False`). - - Returns - ------- - track : A track with local data configured. - """ - min_pos = tsinfo.get("min_pos", []) - max_pos = 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: - tile_key = "x.0.0.0" - elif len(min_pos) == 1: - tile_key = "x.0.0" - else: - raise ValueError("min_pos must be a one or two element array") - - track = self if inplace else utils.copy_unique(self) - track.data = { - "type": "local-tiles", - "tilesetInfo": {"x": tsinfo}, - "tiles": {tile_key: data}, - } - return track - - ## Extend higlass-schema classes -class EnumTrack(hgs.EnumTrack, _LocalDataMixin, _OptionsMixin, _PropertiesMixin): +class EnumTrack(hgs.EnumTrack, _OptionsMixin, _PropertiesMixin): """Represents a generic track.""" ... -class HeatmapTrack(hgs.HeatmapTrack, _LocalDataMixin, _OptionsMixin, _PropertiesMixin): +class HeatmapTrack(hgs.HeatmapTrack, _OptionsMixin, _PropertiesMixin): """Represets a specialized heatmap track.""" ... @@ -196,7 +151,7 @@ class CombinedTrack(hgs.CombinedTrack, _OptionsMixin, _PropertiesMixin): ... -class PluginTrack(hgs.BaseTrack, _OptionsMixin, _PropertiesMixin, _LocalDataMixin): +class PluginTrack(hgs.BaseTrack, _OptionsMixin, _PropertiesMixin): """Represents an unknown plugin track.""" plugin_url: ClassVar[str] diff --git a/src/higlass/tilesets.py b/src/higlass/tilesets.py index 5551619..4c37d39 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,59 @@ 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. + """ + trk = higlass.api.track(type_=type_, **kwargs) + trk.data = { + "type": "local-tiles", + "tilesetInfo": {"x": self.tsinfo}, + "tiles": {self._tile_key: self.data}, + } + return trk + + 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 6ace2a2..ec1e233 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -209,31 +209,26 @@ def test_options_mixin(): assert track.options and track.options["foo"] == "bar" -def test_local_data_mixin(): - track = hg.track("heatmap") +def test_local_data_tileset(): tsinfo = {"min_pos": [0, 0], "max_pos": [100, 100]} data = [{"x": 1, "y": 2}] - other = track.local_data(tsinfo, data) - assert track.uid != other.uid - assert track.data is None + tileset = hg.LocalDataTileset(tsinfo, data) + other = tileset.track("heatmap") assert other.data.type == "local-tiles" assert other.data.tilesetInfo["x"] == tsinfo assert other.data.tiles["x.0.0.0"] == data - track2 = hg.track("heatmap") tsinfo_1d = {"min_pos": [0], "max_pos": [100]} - other2 = track2.local_data(tsinfo_1d, data, inplace=True) - assert track2 is other2 - assert track2.data.tiles["x.0.0"] == data + tileset_1d = hg.LocalDataTileset(tsinfo_1d, data) + other_1d = tileset_1d.track("heatmap") + assert other_1d.data.tiles["x.0.0"] == data with pytest.raises(ValueError, match="min_pos and max_pos must have equal lengths"): - hg.track("heatmap").local_data({"min_pos": [0], "max_pos": [0, 0]}, data) + 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.track("heatmap").local_data( - {"min_pos": [0, 0, 0], "max_pos": [0, 0, 0]}, data - ) + hg.LocalDataTileset({"min_pos": [0, 0, 0], "max_pos": [0, 0, 0]}, data) def test_plugin_track(): From 96f0f62a5b30d37cdd1ac7aed0eb6d4e0ef36f3e Mon Sep 17 00:00:00 2001 From: Nezar Abdennur Date: Sun, 1 Mar 2026 06:08:23 -0500 Subject: [PATCH 8/8] Restore widget.js from main after botched rebase --- src/higlass/widget.js | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/src/higlass/widget.js b/src/higlass/widget.js index fa04dcc..6d108ec 100644 --- a/src/higlass/widget.js +++ b/src/higlass/widget.js @@ -326,10 +326,6 @@ export default { }); if (viewconf.views.length === 1) { -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> b79a7e6 (Fixed formatting) api.on( "location", (/** @type {GenomicLocation} */ loc) => { @@ -337,7 +333,6 @@ export default { model.save_changes(); }, viewconf.views[0].uid, -<<<<<<< HEAD undefined, ); } else { @@ -353,37 +348,6 @@ export default { view.uid, undefined, ); -======= - api.on("location", (/** @type {GenomicLocation} */ loc) => { - model.set("location", locationToCoordinates(loc)); - model.save_changes(); - }, viewconf.views[0].uid, "location-listener"); - } else { - viewconf.views.forEach((view, idx) => { - api.on("location", (/** @type{GenomicLocation} */ loc) => { - let location = model.get("location").slice(); - location[idx] = locationToCoordinates(loc); - model.set("location", location); - model.save_changes(); - }, view.uid, `location-listener-${idx}`); ->>>>>>> f7c33b2 (Fixed linting errors) -======= - "location-listener", - ); - } else { - viewconf.views.forEach((view, idx) => { - api.on( - "location", - (/** @type{GenomicLocation} */ loc) => { - let location = model.get("location").slice(); - location[idx] = locationToCoordinates(loc); - model.set("location", location); - model.save_changes(); - }, - view.uid, - `location-listener-${idx}`, - ); ->>>>>>> b79a7e6 (Fixed formatting) }); }