genome-spy-python 0.1.0__py3-none-any.whl

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.
Files changed (64) hide show
  1. genome_spy/__init__.py +199 -0
  2. genome_spy/_chart_authoring.py +231 -0
  3. genome_spy/_conditions.py +72 -0
  4. genome_spy/_embed.py +87 -0
  5. genome_spy/_expressions.py +271 -0
  6. genome_spy/_parameters.py +267 -0
  7. genome_spy/_render.py +207 -0
  8. genome_spy/_utils.py +75 -0
  9. genome_spy/_widget.py +262 -0
  10. genome_spy/api.py +198 -0
  11. genome_spy/arrow.py +155 -0
  12. genome_spy/channels.py +193 -0
  13. genome_spy/chart.py +1240 -0
  14. genome_spy/data.py +56 -0
  15. genome_spy/data_transformers.py +267 -0
  16. genome_spy/datasets/__init__.py +189 -0
  17. genome_spy/datasets/_airway.py +219 -0
  18. genome_spy/datasets/_annotations.py +37 -0
  19. genome_spy/datasets/_gistic.py +43 -0
  20. genome_spy/datasets/_grammar.py +66 -0
  21. genome_spy/datasets/_hapmap.py +180 -0
  22. genome_spy/datasets/_mutation.py +289 -0
  23. genome_spy/datasets/_oncoprint.py +523 -0
  24. genome_spy/datasets/data/airway_metadata.csv +9 -0
  25. genome_spy/datasets/data/airway_scaledcounts.csv +38695 -0
  26. genome_spy/datasets/data/brca.maf.gz +0 -0
  27. genome_spy/datasets/data/hapmap_gwas.csv +14413 -0
  28. genome_spy/datasets/data/mutation_impact_reference.json +27 -0
  29. genome_spy/datasets/data/oncoprint_dataset3.json +266 -0
  30. genome_spy/datasets/data/p53_sequence_comparison.json.gz +0 -0
  31. genome_spy/datasets/data/pik3ca_mutations.json +1 -0
  32. genome_spy/datasets/data/pik3ca_tcga_brca_lollipop.json +38 -0
  33. genome_spy/datasets/data/refseq_gene_bodies.csv.gz +0 -0
  34. genome_spy/datasets/data/tal1_alphagenome_reference.json.gz +0 -0
  35. genome_spy/datasets/data/tcga.tsv +146 -0
  36. genome_spy/datasets/data/tcga_laml.maf.gz +0 -0
  37. genome_spy/datasets/data/tcga_laml_annot.tsv +201 -0
  38. genome_spy/datasets/data/tcga_laml_combined_oncoplot.json.gz +0 -0
  39. genome_spy/datasets/data/tcga_ov_gistic_lesions.tsv.gz +0 -0
  40. genome_spy/datasets/data/tcga_ov_gistic_scores.tsv.gz +0 -0
  41. genome_spy/helpers.py +185 -0
  42. genome_spy/jupyter.py +5 -0
  43. genome_spy/py.typed +0 -0
  44. genome_spy/schema/__init__.py +784 -0
  45. genome_spy/schema/_kwds.py +1394 -0
  46. genome_spy/schema/_typing.py +186 -0
  47. genome_spy/schema/capabilities.json +593 -0
  48. genome_spy/schema/channels.py +8943 -0
  49. genome_spy/schema/composition.py +1064 -0
  50. genome_spy/schema/core.py +51821 -0
  51. genome_spy/schema/ergonomics.py +2056 -0
  52. genome_spy/schema/expressions.py +476 -0
  53. genome_spy/schema/genome-spy-schema.json +33657 -0
  54. genome_spy/schema/lazy.py +326 -0
  55. genome_spy/schema/mixins.py +11684 -0
  56. genome_spy/schemapi.py +264 -0
  57. genome_spy/static/widget.js +345 -0
  58. genome_spy_python-0.1.0.dist-info/METADATA +185 -0
  59. genome_spy_python-0.1.0.dist-info/RECORD +64 -0
  60. genome_spy_python-0.1.0.dist-info/WHEEL +4 -0
  61. genome_spy_python-0.1.0.dist-info/licenses/LICENSE +21 -0
  62. genome_spy_python-0.1.0.dist-info/licenses/LICENSES/ALTAIR-BSD-3-Clause.txt +27 -0
  63. genome_spy_python-0.1.0.dist-info/licenses/LICENSES/GALLERY-DATA-MIT.txt +22 -0
  64. genome_spy_python-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +42 -0
genome_spy/api.py ADDED
@@ -0,0 +1,198 @@
1
+ """Public API re-exports for the handwritten ergonomic layer."""
2
+
3
+ from genome_spy.channels import Locus, compare, datum, locus, value
4
+ from genome_spy._expressions import Expression
5
+ from genome_spy._parameters import Parameter
6
+ from genome_spy.arrow import to_arrow_ipc
7
+ from genome_spy.chart import (
8
+ Chart,
9
+ ConcatChart,
10
+ HConcatChart,
11
+ ImportedView,
12
+ LayerChart,
13
+ MultiscaleChart,
14
+ TopLevelSpec,
15
+ VConcatChart,
16
+ )
17
+ from genome_spy.data import Data, lazy
18
+ from genome_spy.helpers import (
19
+ axes,
20
+ config,
21
+ condition,
22
+ data_format,
23
+ dynamic_opacity,
24
+ expr,
25
+ parse,
26
+ scales,
27
+ step,
28
+ title,
29
+ view,
30
+ view_config,
31
+ when,
32
+ )
33
+
34
+ # BEGIN GENERATED INTERACTION IMPORTS
35
+ from genome_spy.helpers import (
36
+ binding,
37
+ binding_checkbox,
38
+ binding_radio,
39
+ binding_range,
40
+ binding_select,
41
+ param,
42
+ ruler,
43
+ selection_interval,
44
+ selection_point,
45
+ )
46
+
47
+ # END GENERATED INTERACTION IMPORTS
48
+ from genome_spy.jupyter import JupyterChart
49
+ from genome_spy.schema.channels import (
50
+ Angle,
51
+ Color,
52
+ Direction,
53
+ Dx,
54
+ Dy,
55
+ FacetIndex,
56
+ Fill,
57
+ FillOpacity,
58
+ Key,
59
+ Opacity,
60
+ Sample,
61
+ Search,
62
+ SemanticScore,
63
+ Shape,
64
+ Size,
65
+ Stroke,
66
+ StrokeOpacity,
67
+ StrokeWidth,
68
+ Text,
69
+ Tooltip,
70
+ UniqueId,
71
+ X,
72
+ X2,
73
+ XOffset,
74
+ Y,
75
+ Y2,
76
+ YOffset,
77
+ )
78
+ from genome_spy.schema.core import (
79
+ AxisGenomeData,
80
+ BrushConfig,
81
+ DataFormat,
82
+ DynamicOpacity,
83
+ ExprRef,
84
+ GenomeAxis,
85
+ HandledTooltip,
86
+ Legend,
87
+ Paddings,
88
+ Parse,
89
+ RulerMarkConfig,
90
+ Scale,
91
+ SelectionDomainRef,
92
+ SizeDef,
93
+ Step,
94
+ Title,
95
+ )
96
+ from genome_spy.schema.composition import (
97
+ concat,
98
+ hconcat,
99
+ import_view,
100
+ layer,
101
+ multiscale,
102
+ vconcat,
103
+ )
104
+
105
+ __all__ = [
106
+ "Angle",
107
+ "axes",
108
+ "AxisGenomeData",
109
+ "BrushConfig",
110
+ "Chart",
111
+ "Color",
112
+ "condition",
113
+ "Direction",
114
+ "compare",
115
+ "ConcatChart",
116
+ "Data",
117
+ "DataFormat",
118
+ "DynamicOpacity",
119
+ "Dx",
120
+ "Dy",
121
+ "ExprRef",
122
+ "Expression",
123
+ "FacetIndex",
124
+ "Fill",
125
+ "FillOpacity",
126
+ "GenomeAxis",
127
+ "HandledTooltip",
128
+ "HConcatChart",
129
+ "ImportedView",
130
+ "JupyterChart",
131
+ "Key",
132
+ "LayerChart",
133
+ "Legend",
134
+ "Locus",
135
+ "MultiscaleChart",
136
+ "Opacity",
137
+ "Parameter",
138
+ "Paddings",
139
+ "Parse",
140
+ "Sample",
141
+ "Scale",
142
+ "SelectionDomainRef",
143
+ "Search",
144
+ "SemanticScore",
145
+ "Shape",
146
+ "Size",
147
+ "SizeDef",
148
+ "Step",
149
+ "Stroke",
150
+ "StrokeOpacity",
151
+ "StrokeWidth",
152
+ "Text",
153
+ "Tooltip",
154
+ "TopLevelSpec",
155
+ "to_arrow_ipc",
156
+ "Title",
157
+ "UniqueId",
158
+ "VConcatChart",
159
+ "X",
160
+ "X2",
161
+ "XOffset",
162
+ "Y",
163
+ "Y2",
164
+ "YOffset",
165
+ "concat",
166
+ "config",
167
+ "data_format",
168
+ "datum",
169
+ "dynamic_opacity",
170
+ "expr",
171
+ "hconcat",
172
+ "import_view",
173
+ "layer",
174
+ "lazy",
175
+ "locus",
176
+ "multiscale",
177
+ "parse",
178
+ "RulerMarkConfig",
179
+ "scales",
180
+ "step",
181
+ "title",
182
+ "value",
183
+ "view",
184
+ "view_config",
185
+ "vconcat",
186
+ "when",
187
+ # BEGIN GENERATED INTERACTION EXPORTS
188
+ "binding",
189
+ "binding_checkbox",
190
+ "binding_radio",
191
+ "binding_range",
192
+ "binding_select",
193
+ "param",
194
+ "ruler",
195
+ "selection_interval",
196
+ "selection_point",
197
+ # END GENERATED INTERACTION EXPORTS
198
+ ]
genome_spy/arrow.py ADDED
@@ -0,0 +1,155 @@
1
+ """Arrow IPC serialization helpers for notebook data transport."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from io import BytesIO
6
+ from typing import Any
7
+
8
+ __all__ = ["to_arrow_ipc"]
9
+
10
+
11
+ def _module_root(value: Any) -> str:
12
+ """Return the top-level module that defines a value's type."""
13
+ return type(value).__module__.partition(".")[0]
14
+
15
+
16
+ def _is_polars_frame(value: Any) -> bool:
17
+ """Return whether a value is a Polars dataframe without importing Polars."""
18
+ return _module_root(value) == "polars" and callable(
19
+ getattr(value, "write_ipc", None)
20
+ )
21
+
22
+
23
+ def _is_pandas_frame(value: Any) -> bool:
24
+ """Return whether a value is a pandas dataframe without importing pandas."""
25
+ return _module_root(value) == "pandas" and type(value).__name__ == "DataFrame"
26
+
27
+
28
+ def _is_pyarrow_table(value: Any) -> bool:
29
+ """Return whether a value is a supported PyArrow tabular object."""
30
+ return _module_root(value) == "pyarrow" and type(value).__name__ in {
31
+ "Table",
32
+ "RecordBatch",
33
+ }
34
+
35
+
36
+ def _validated_payload(value: Any) -> bytes:
37
+ """Return a nonempty immutable IPC payload or raise a contextual error."""
38
+ if hasattr(value, "getvalue"):
39
+ value = value.getvalue()
40
+ if not isinstance(value, bytes | bytearray | memoryview):
41
+ raise TypeError(
42
+ "Arrow IPC writer must return bytes or a binary buffer, "
43
+ f"not {type(value).__name__}."
44
+ )
45
+ payload = bytes(value)
46
+ if not payload:
47
+ raise ValueError("Arrow IPC writer returned an empty payload.")
48
+ return payload
49
+
50
+
51
+ def _write_ipc_method(data: Any) -> bytes:
52
+ """Serialize an object that exposes Polars-compatible ``write_ipc``."""
53
+ try:
54
+ result = data.write_ipc(file=None, compression="uncompressed")
55
+ except (TypeError, ValueError) as error:
56
+ raise TypeError(
57
+ f"Could not write uncompressed Arrow IPC data from {type(data).__name__}."
58
+ ) from error
59
+ return _validated_payload(result)
60
+
61
+
62
+ def _load_pyarrow() -> Any | None:
63
+ """Import PyArrow only when a recognized table requires it."""
64
+ try:
65
+ import pyarrow as pa
66
+ except ImportError:
67
+ return None
68
+ return pa
69
+
70
+
71
+ def _write_pyarrow_ipc(data: Any, pa: Any) -> bytes:
72
+ """Serialize a PyArrow table or record batch as an uncompressed IPC file."""
73
+ table = (
74
+ pa.Table.from_batches([data]) if type(data).__name__ == "RecordBatch" else data
75
+ )
76
+ output = BytesIO()
77
+ options = pa.ipc.IpcWriteOptions(compression=None)
78
+ with pa.ipc.new_file(output, table.schema, options=options) as writer:
79
+ writer.write_table(table)
80
+ return _validated_payload(output.getvalue())
81
+
82
+
83
+ def _write_pandas_ipc(data: Any, pa: Any) -> bytes:
84
+ """Serialize a pandas dataframe through optional PyArrow."""
85
+ table = pa.Table.from_pandas(data, preserve_index=False)
86
+ return _write_pyarrow_ipc(table, pa)
87
+
88
+
89
+ def _try_to_arrow_ipc(data: Any) -> bytes | None:
90
+ """Return IPC bytes for an automatically supported table, if available.
91
+
92
+ ``None`` means the input is not an automatically supported Arrow producer
93
+ or pandas support is unavailable because PyArrow is not installed. Errors
94
+ from recognized writers intentionally propagate rather than falling back to
95
+ JSON records.
96
+ """
97
+ if _is_polars_frame(data):
98
+ return _write_ipc_method(data)
99
+ if _is_pyarrow_table(data):
100
+ pa = _load_pyarrow()
101
+ if pa is None:
102
+ raise RuntimeError("PyArrow table support requires the 'arrow' extra.")
103
+ return _write_pyarrow_ipc(data, pa)
104
+ if _is_pandas_frame(data):
105
+ pa = _load_pyarrow()
106
+ return None if pa is None else _write_pandas_ipc(data, pa)
107
+ return None
108
+
109
+
110
+ def to_arrow_ipc(data: Any) -> bytes:
111
+ """Serialize a DataFrame-like object as uncompressed Arrow IPC bytes.
112
+
113
+ Description:
114
+ Supports Polars dataframes, PyArrow tables and record batches, pandas
115
+ dataframes when the optional ``arrow`` extra is installed, and custom
116
+ objects with a Polars-compatible ``write_ipc`` method. The helper
117
+ intentionally writes uncompressed IPC because GenomeSpy's Arrow decoder
118
+ does not support compressed IPC buffers yet.
119
+
120
+ Args:
121
+ data: A supported table or compatible object with
122
+ ``write_ipc(file=None, compression="uncompressed")``.
123
+
124
+ Returns:
125
+ The complete Arrow IPC file payload as immutable bytes.
126
+
127
+ Raises:
128
+ TypeError: If the object does not provide a compatible writer or the
129
+ writer does not return a bytes-like payload.
130
+ ValueError: If the writer returns an empty payload.
131
+
132
+ Example:
133
+ >>> import polars as pl
134
+ >>> payload = to_arrow_ipc(pl.DataFrame({"x": [1, 2]}))
135
+ >>> payload[:6]
136
+ b'ARROW1'
137
+ """
138
+
139
+ payload = _try_to_arrow_ipc(data)
140
+ if payload is not None:
141
+ return payload
142
+
143
+ if _is_pandas_frame(data):
144
+ raise TypeError(
145
+ "Serializing pandas DataFrames as Arrow IPC requires PyArrow. "
146
+ "Install genome-spy-python[arrow]."
147
+ )
148
+
149
+ writer = getattr(data, "write_ipc", None)
150
+ if not callable(writer):
151
+ raise TypeError(
152
+ "Arrow IPC serialization requires an object with "
153
+ 'write_ipc(file=None, compression="uncompressed").'
154
+ )
155
+ return _write_ipc_method(data)
genome_spy/channels.py ADDED
@@ -0,0 +1,193 @@
1
+ """Channel helpers for the first handwritten GenomeSpy API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Self, cast
7
+
8
+ from genome_spy._utils import is_mapping, parse_shorthand
9
+ from genome_spy.schema._kwds import CompareParamsKwds
10
+ from genome_spy.schema.core import CompareParams
11
+ from genome_spy.schema.ergonomics import (
12
+ DatumChannelMethodMixin,
13
+ Locus,
14
+ LocusChannelMethodMixin,
15
+ ValueChannelMethodMixin,
16
+ compare,
17
+ datum,
18
+ locus,
19
+ value,
20
+ )
21
+ from genome_spy.schemapi import (
22
+ SchemaBase,
23
+ Undefined,
24
+ merge_mapping_value,
25
+ normalize_schema_value,
26
+ )
27
+
28
+ _MISSING = Undefined
29
+
30
+ __all__ = [
31
+ "Channel",
32
+ "LocusChannel",
33
+ "Locus",
34
+ "channel",
35
+ "compare",
36
+ "datum",
37
+ "locus",
38
+ "value",
39
+ ]
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class Channel:
44
+ """A serializable GenomeSpy encoding channel definition.
45
+
46
+ This class is mostly a small runtime backend for generated channel wrappers
47
+ and the handwritten locus helper, not the main authoring surface users
48
+ should build on directly.
49
+ """
50
+
51
+ definition: dict[str, Any] = field(default_factory=dict)
52
+ encoding_name: str | None = None
53
+
54
+ def to_dict(self) -> dict[str, Any]:
55
+ """Return the JSON-serializable channel definition."""
56
+ return dict(self.definition)
57
+
58
+ def _with_sort(
59
+ self,
60
+ value: CompareParams | CompareParamsKwds | str | list[str] | None | object,
61
+ /,
62
+ properties: dict[str, Any],
63
+ ) -> Self:
64
+ """Return a channel with a normalized ``sort`` configuration."""
65
+ definition = self.to_dict()
66
+ definition["sort"] = _merge_sort_value(
67
+ definition.get("sort", Undefined),
68
+ value,
69
+ **properties,
70
+ )
71
+ return self._replace_definition(definition)
72
+
73
+ def _with_nested(
74
+ self,
75
+ key: str,
76
+ value: SchemaBase | dict[str, Any] | None | object = _MISSING,
77
+ /,
78
+ **kwargs: Any,
79
+ ) -> Self:
80
+ definition = self.to_dict()
81
+ definition[key] = merge_mapping_value(
82
+ definition.get(key, Undefined),
83
+ key,
84
+ value,
85
+ **kwargs,
86
+ )
87
+ return self._replace_definition(definition)
88
+
89
+ def _with_property(self, key: str, value: Any) -> Self:
90
+ definition = self.to_dict()
91
+ definition[key] = normalize_schema_value(value, validate=False)
92
+ return self._replace_definition(definition)
93
+
94
+ def _replace_definition(self, definition: dict[str, Any]) -> Self:
95
+ if type(self) is Channel:
96
+ return cast(Self, Channel(definition, self.encoding_name))
97
+ return self.__class__(definition)
98
+
99
+
100
+ @dataclass(frozen=True, slots=True)
101
+ class LocusChannel(LocusChannelMethodMixin, Channel):
102
+ """A handwritten locus channel with generated-style fluent setters.
103
+
104
+ GenomeSpy's genomic locus channels are intentionally kept as a small
105
+ handwritten ergonomic layer rather than forced into the ordinary generated
106
+ channel shape.
107
+ """
108
+
109
+ def title(self, value: str | None) -> Self:
110
+ """Return a locus channel with ``title`` updated."""
111
+ return self._with_property("title", value)
112
+
113
+ def chrom(self, value: str) -> Self:
114
+ """Return a locus channel with ``chrom`` updated."""
115
+ return self._with_property("chrom", value)
116
+
117
+ def pos(self, value: str | None) -> Self:
118
+ """Return a locus channel with ``pos`` updated."""
119
+ return self._with_property("pos", value)
120
+
121
+ def offset(self, value: float) -> Self:
122
+ """Return a locus channel with ``offset`` updated."""
123
+ return self._with_property("offset", value)
124
+
125
+ def band(self, value: float) -> Self:
126
+ """Return a locus channel with ``band`` updated."""
127
+ return self._with_property("band", value)
128
+
129
+
130
+ @dataclass(frozen=True, slots=True)
131
+ class DatumChannel(DatumChannelMethodMixin, Channel):
132
+ """A constant-datum channel with schema-derived fluent methods."""
133
+
134
+
135
+ @dataclass(frozen=True, slots=True)
136
+ class ValueChannel(ValueChannelMethodMixin, Channel):
137
+ """A constant-value channel with schema-derived fluent methods."""
138
+
139
+
140
+ def channel(
141
+ value: Channel | SchemaBase | str | dict[str, Any],
142
+ /,
143
+ *,
144
+ encoding_name: str | None = None,
145
+ **kwargs: Any,
146
+ ) -> Channel:
147
+ """Create a generic channel definition from shorthand or a raw mapping."""
148
+ if isinstance(value, Channel):
149
+ definition = value.to_dict()
150
+ encoding_name = encoding_name or value.encoding_name
151
+ elif isinstance(value, SchemaBase):
152
+ definition = cast(dict[str, Any], normalize_schema_value(value, validate=False))
153
+ elif isinstance(value, str):
154
+ definition = parse_shorthand(value)
155
+ elif is_mapping(value):
156
+ definition = cast(dict[str, Any], normalize_schema_value(value, validate=False))
157
+ else:
158
+ raise TypeError(f"Unsupported channel value: {type(value)!r}")
159
+
160
+ definition.update(kwargs)
161
+ return Channel(definition, encoding_name=encoding_name)
162
+
163
+
164
+ def _merge_sort_value(
165
+ current: Any,
166
+ value: CompareParams
167
+ | CompareParamsKwds
168
+ | str
169
+ | list[str]
170
+ | None
171
+ | object = _MISSING,
172
+ /,
173
+ **kwargs: Any,
174
+ ) -> Any:
175
+ if value is _MISSING or value is None or isinstance(value, CompareParams | dict):
176
+ try:
177
+ return merge_mapping_value(current, "sort", value, **kwargs)
178
+ except TypeError as error:
179
+ message = str(error)
180
+ if "null 'sort'" in message or "non-mapping value" in message:
181
+ raise
182
+ raise TypeError(
183
+ f"Unsupported nested 'sort' value: {type(value)!r}"
184
+ ) from error
185
+
186
+ if isinstance(value, str | list):
187
+ if kwargs:
188
+ raise TypeError(
189
+ "Cannot merge keyword properties into simple 'sort' values."
190
+ )
191
+ return normalize_schema_value(value, validate=False)
192
+
193
+ raise TypeError(f"Unsupported nested 'sort' value: {type(value)!r}")