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.
- genome_spy/__init__.py +199 -0
- genome_spy/_chart_authoring.py +231 -0
- genome_spy/_conditions.py +72 -0
- genome_spy/_embed.py +87 -0
- genome_spy/_expressions.py +271 -0
- genome_spy/_parameters.py +267 -0
- genome_spy/_render.py +207 -0
- genome_spy/_utils.py +75 -0
- genome_spy/_widget.py +262 -0
- genome_spy/api.py +198 -0
- genome_spy/arrow.py +155 -0
- genome_spy/channels.py +193 -0
- genome_spy/chart.py +1240 -0
- genome_spy/data.py +56 -0
- genome_spy/data_transformers.py +267 -0
- genome_spy/datasets/__init__.py +189 -0
- genome_spy/datasets/_airway.py +219 -0
- genome_spy/datasets/_annotations.py +37 -0
- genome_spy/datasets/_gistic.py +43 -0
- genome_spy/datasets/_grammar.py +66 -0
- genome_spy/datasets/_hapmap.py +180 -0
- genome_spy/datasets/_mutation.py +289 -0
- genome_spy/datasets/_oncoprint.py +523 -0
- genome_spy/datasets/data/airway_metadata.csv +9 -0
- genome_spy/datasets/data/airway_scaledcounts.csv +38695 -0
- genome_spy/datasets/data/brca.maf.gz +0 -0
- genome_spy/datasets/data/hapmap_gwas.csv +14413 -0
- genome_spy/datasets/data/mutation_impact_reference.json +27 -0
- genome_spy/datasets/data/oncoprint_dataset3.json +266 -0
- genome_spy/datasets/data/p53_sequence_comparison.json.gz +0 -0
- genome_spy/datasets/data/pik3ca_mutations.json +1 -0
- genome_spy/datasets/data/pik3ca_tcga_brca_lollipop.json +38 -0
- genome_spy/datasets/data/refseq_gene_bodies.csv.gz +0 -0
- genome_spy/datasets/data/tal1_alphagenome_reference.json.gz +0 -0
- genome_spy/datasets/data/tcga.tsv +146 -0
- genome_spy/datasets/data/tcga_laml.maf.gz +0 -0
- genome_spy/datasets/data/tcga_laml_annot.tsv +201 -0
- genome_spy/datasets/data/tcga_laml_combined_oncoplot.json.gz +0 -0
- genome_spy/datasets/data/tcga_ov_gistic_lesions.tsv.gz +0 -0
- genome_spy/datasets/data/tcga_ov_gistic_scores.tsv.gz +0 -0
- genome_spy/helpers.py +185 -0
- genome_spy/jupyter.py +5 -0
- genome_spy/py.typed +0 -0
- genome_spy/schema/__init__.py +784 -0
- genome_spy/schema/_kwds.py +1394 -0
- genome_spy/schema/_typing.py +186 -0
- genome_spy/schema/capabilities.json +593 -0
- genome_spy/schema/channels.py +8943 -0
- genome_spy/schema/composition.py +1064 -0
- genome_spy/schema/core.py +51821 -0
- genome_spy/schema/ergonomics.py +2056 -0
- genome_spy/schema/expressions.py +476 -0
- genome_spy/schema/genome-spy-schema.json +33657 -0
- genome_spy/schema/lazy.py +326 -0
- genome_spy/schema/mixins.py +11684 -0
- genome_spy/schemapi.py +264 -0
- genome_spy/static/widget.js +345 -0
- genome_spy_python-0.1.0.dist-info/METADATA +185 -0
- genome_spy_python-0.1.0.dist-info/RECORD +64 -0
- genome_spy_python-0.1.0.dist-info/WHEEL +4 -0
- genome_spy_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- genome_spy_python-0.1.0.dist-info/licenses/LICENSES/ALTAIR-BSD-3-Clause.txt +27 -0
- genome_spy_python-0.1.0.dist-info/licenses/LICENSES/GALLERY-DATA-MIT.txt +22 -0
- genome_spy_python-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +42 -0
genome_spy/__init__.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Public package interface for genome_spy."""
|
|
2
|
+
|
|
3
|
+
from genome_spy.arrow import to_arrow_ipc
|
|
4
|
+
from genome_spy.data_transformers import data_transformers
|
|
5
|
+
from genome_spy.api import (
|
|
6
|
+
Angle,
|
|
7
|
+
AxisGenomeData,
|
|
8
|
+
BrushConfig,
|
|
9
|
+
Color,
|
|
10
|
+
condition,
|
|
11
|
+
Direction,
|
|
12
|
+
ConcatChart,
|
|
13
|
+
Data,
|
|
14
|
+
DataFormat,
|
|
15
|
+
DynamicOpacity,
|
|
16
|
+
Dx,
|
|
17
|
+
Dy,
|
|
18
|
+
ExprRef,
|
|
19
|
+
Expression,
|
|
20
|
+
FacetIndex,
|
|
21
|
+
Fill,
|
|
22
|
+
FillOpacity,
|
|
23
|
+
GenomeAxis,
|
|
24
|
+
HandledTooltip,
|
|
25
|
+
HConcatChart,
|
|
26
|
+
ImportedView,
|
|
27
|
+
JupyterChart,
|
|
28
|
+
Key,
|
|
29
|
+
LayerChart,
|
|
30
|
+
Legend,
|
|
31
|
+
Locus,
|
|
32
|
+
MultiscaleChart,
|
|
33
|
+
Opacity,
|
|
34
|
+
Parameter,
|
|
35
|
+
Paddings,
|
|
36
|
+
Parse,
|
|
37
|
+
RulerMarkConfig,
|
|
38
|
+
Sample,
|
|
39
|
+
Scale,
|
|
40
|
+
Search,
|
|
41
|
+
SelectionDomainRef,
|
|
42
|
+
SemanticScore,
|
|
43
|
+
Shape,
|
|
44
|
+
Size,
|
|
45
|
+
SizeDef,
|
|
46
|
+
Step,
|
|
47
|
+
Stroke,
|
|
48
|
+
StrokeOpacity,
|
|
49
|
+
StrokeWidth,
|
|
50
|
+
Text,
|
|
51
|
+
Tooltip,
|
|
52
|
+
TopLevelSpec,
|
|
53
|
+
Title,
|
|
54
|
+
UniqueId,
|
|
55
|
+
VConcatChart,
|
|
56
|
+
X,
|
|
57
|
+
X2,
|
|
58
|
+
XOffset,
|
|
59
|
+
Y,
|
|
60
|
+
Y2,
|
|
61
|
+
YOffset,
|
|
62
|
+
Chart,
|
|
63
|
+
axes,
|
|
64
|
+
config,
|
|
65
|
+
compare,
|
|
66
|
+
concat,
|
|
67
|
+
data_format,
|
|
68
|
+
datum,
|
|
69
|
+
dynamic_opacity,
|
|
70
|
+
expr,
|
|
71
|
+
hconcat,
|
|
72
|
+
import_view,
|
|
73
|
+
layer,
|
|
74
|
+
lazy,
|
|
75
|
+
locus,
|
|
76
|
+
multiscale,
|
|
77
|
+
parse,
|
|
78
|
+
scales,
|
|
79
|
+
step,
|
|
80
|
+
title,
|
|
81
|
+
value,
|
|
82
|
+
view,
|
|
83
|
+
view_config,
|
|
84
|
+
vconcat,
|
|
85
|
+
when,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# BEGIN GENERATED INTERACTION IMPORTS
|
|
89
|
+
from genome_spy.helpers import (
|
|
90
|
+
binding,
|
|
91
|
+
binding_checkbox,
|
|
92
|
+
binding_radio,
|
|
93
|
+
binding_range,
|
|
94
|
+
binding_select,
|
|
95
|
+
param,
|
|
96
|
+
ruler,
|
|
97
|
+
selection_interval,
|
|
98
|
+
selection_point,
|
|
99
|
+
)
|
|
100
|
+
# END GENERATED INTERACTION IMPORTS
|
|
101
|
+
|
|
102
|
+
__all__ = [
|
|
103
|
+
"data_transformers",
|
|
104
|
+
"__version__",
|
|
105
|
+
"Angle",
|
|
106
|
+
"AxisGenomeData",
|
|
107
|
+
"axes",
|
|
108
|
+
"BrushConfig",
|
|
109
|
+
"Chart",
|
|
110
|
+
"Color",
|
|
111
|
+
"condition",
|
|
112
|
+
"Direction",
|
|
113
|
+
"compare",
|
|
114
|
+
"ConcatChart",
|
|
115
|
+
"Data",
|
|
116
|
+
"DataFormat",
|
|
117
|
+
"DynamicOpacity",
|
|
118
|
+
"Dx",
|
|
119
|
+
"Dy",
|
|
120
|
+
"ExprRef",
|
|
121
|
+
"Expression",
|
|
122
|
+
"FacetIndex",
|
|
123
|
+
"Fill",
|
|
124
|
+
"FillOpacity",
|
|
125
|
+
"GenomeAxis",
|
|
126
|
+
"HandledTooltip",
|
|
127
|
+
"HConcatChart",
|
|
128
|
+
"ImportedView",
|
|
129
|
+
"JupyterChart",
|
|
130
|
+
"Key",
|
|
131
|
+
"LayerChart",
|
|
132
|
+
"Legend",
|
|
133
|
+
"Locus",
|
|
134
|
+
"MultiscaleChart",
|
|
135
|
+
"Opacity",
|
|
136
|
+
"Parameter",
|
|
137
|
+
"Paddings",
|
|
138
|
+
"Parse",
|
|
139
|
+
"Sample",
|
|
140
|
+
"Scale",
|
|
141
|
+
"Search",
|
|
142
|
+
"SelectionDomainRef",
|
|
143
|
+
"SemanticScore",
|
|
144
|
+
"Shape",
|
|
145
|
+
"Size",
|
|
146
|
+
"SizeDef",
|
|
147
|
+
"Step",
|
|
148
|
+
"Stroke",
|
|
149
|
+
"StrokeOpacity",
|
|
150
|
+
"StrokeWidth",
|
|
151
|
+
"Text",
|
|
152
|
+
"Tooltip",
|
|
153
|
+
"TopLevelSpec",
|
|
154
|
+
"to_arrow_ipc",
|
|
155
|
+
"Title",
|
|
156
|
+
"UniqueId",
|
|
157
|
+
"VConcatChart",
|
|
158
|
+
"X",
|
|
159
|
+
"X2",
|
|
160
|
+
"XOffset",
|
|
161
|
+
"Y",
|
|
162
|
+
"Y2",
|
|
163
|
+
"YOffset",
|
|
164
|
+
"concat",
|
|
165
|
+
"config",
|
|
166
|
+
"data_format",
|
|
167
|
+
"datum",
|
|
168
|
+
"dynamic_opacity",
|
|
169
|
+
"expr",
|
|
170
|
+
"hconcat",
|
|
171
|
+
"import_view",
|
|
172
|
+
"layer",
|
|
173
|
+
"lazy",
|
|
174
|
+
"locus",
|
|
175
|
+
"multiscale",
|
|
176
|
+
"parse",
|
|
177
|
+
"RulerMarkConfig",
|
|
178
|
+
"scales",
|
|
179
|
+
"step",
|
|
180
|
+
"title",
|
|
181
|
+
"value",
|
|
182
|
+
"view",
|
|
183
|
+
"view_config",
|
|
184
|
+
"vconcat",
|
|
185
|
+
"when",
|
|
186
|
+
# BEGIN GENERATED INTERACTION EXPORTS
|
|
187
|
+
"binding",
|
|
188
|
+
"binding_checkbox",
|
|
189
|
+
"binding_radio",
|
|
190
|
+
"binding_range",
|
|
191
|
+
"binding_select",
|
|
192
|
+
"param",
|
|
193
|
+
"ruler",
|
|
194
|
+
"selection_interval",
|
|
195
|
+
"selection_point",
|
|
196
|
+
# END GENERATED INTERACTION EXPORTS
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Authoring-edge normalization helpers for the handwritten chart API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from datetime import date, datetime
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
from genome_spy._utils import is_mapping
|
|
11
|
+
from genome_spy.arrow import _is_pandas_frame, _is_polars_frame, _is_pyarrow_table
|
|
12
|
+
from genome_spy.channels import Channel, channel
|
|
13
|
+
from genome_spy.schemapi import (
|
|
14
|
+
SchemaBase,
|
|
15
|
+
Undefined,
|
|
16
|
+
normalize_mapping_value,
|
|
17
|
+
normalize_schema_value,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def normalize_data(data: Any) -> Any:
|
|
22
|
+
"""Normalize Python-side data inputs into schema-compatible values."""
|
|
23
|
+
if data is None:
|
|
24
|
+
return None
|
|
25
|
+
if isinstance(data, SchemaBase):
|
|
26
|
+
return json_safe(data.to_dict(validate=False))
|
|
27
|
+
if is_mapping(data):
|
|
28
|
+
return cast(
|
|
29
|
+
dict[str, Any], json_safe(normalize_schema_value(data, validate=False))
|
|
30
|
+
)
|
|
31
|
+
records = records_from_data(data)
|
|
32
|
+
if records is not None:
|
|
33
|
+
return records_data(records)
|
|
34
|
+
raise TypeError(f"Unsupported data value: {type(data)!r}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def json_safe(value: Any) -> Any:
|
|
38
|
+
"""Convert authoring-edge values into JSON-safe primitives."""
|
|
39
|
+
if value is None:
|
|
40
|
+
return None
|
|
41
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
42
|
+
return None
|
|
43
|
+
if isinstance(value, datetime | date):
|
|
44
|
+
return value.isoformat()
|
|
45
|
+
if not isinstance(value, str | bytes) and hasattr(value, "item"):
|
|
46
|
+
try:
|
|
47
|
+
item = value.item()
|
|
48
|
+
except (AttributeError, TypeError, ValueError):
|
|
49
|
+
item = value
|
|
50
|
+
if item is not value:
|
|
51
|
+
return json_safe(item)
|
|
52
|
+
if isinstance(value, list):
|
|
53
|
+
return [json_safe(item) for item in value]
|
|
54
|
+
if is_mapping(value):
|
|
55
|
+
return {key: json_safe(item) for key, item in value.items()}
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def records_from_data(data: Any) -> list[dict[str, Any]] | None:
|
|
60
|
+
"""Extract record-like rows from common Python table inputs."""
|
|
61
|
+
if isinstance(data, list):
|
|
62
|
+
return data
|
|
63
|
+
if _is_pyarrow_table(data):
|
|
64
|
+
records = data.to_pylist()
|
|
65
|
+
return records if isinstance(records, list) else None
|
|
66
|
+
if hasattr(data, "to_dicts"):
|
|
67
|
+
records = data.to_dicts()
|
|
68
|
+
if isinstance(records, list):
|
|
69
|
+
return records
|
|
70
|
+
return None
|
|
71
|
+
if is_mapping(data):
|
|
72
|
+
values = data.get("values")
|
|
73
|
+
if isinstance(values, list):
|
|
74
|
+
return values
|
|
75
|
+
return None
|
|
76
|
+
if hasattr(data, "to_dict"):
|
|
77
|
+
try:
|
|
78
|
+
records = data.to_dict(orient="records")
|
|
79
|
+
except TypeError:
|
|
80
|
+
return None
|
|
81
|
+
if isinstance(records, list):
|
|
82
|
+
return records
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def records_data(records: list[dict[str, Any]]) -> dict[str, Any]:
|
|
87
|
+
"""Wrap record rows as inline schema data."""
|
|
88
|
+
return {"values": json_safe(records)}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def infer_field_type(field: str, data: Any) -> str | None:
|
|
92
|
+
"""Infer a GenomeSpy encoding type from up to the first 100 records."""
|
|
93
|
+
table_type = infer_table_field_type(field, data)
|
|
94
|
+
if table_type is not None:
|
|
95
|
+
return table_type
|
|
96
|
+
if _is_polars_frame(data) or _is_pandas_frame(data) or _is_pyarrow_table(data):
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
records = records_from_data(data)
|
|
100
|
+
if not records:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
for row in records[:100]:
|
|
104
|
+
if not is_mapping(row) or field not in row:
|
|
105
|
+
continue
|
|
106
|
+
inferred_type = infer_value_type(row[field])
|
|
107
|
+
if inferred_type is not None:
|
|
108
|
+
return inferred_type
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def infer_table_field_type(field: str, data: Any) -> str | None:
|
|
113
|
+
"""Infer a field type from supported table dtype metadata without rows."""
|
|
114
|
+
if _is_polars_frame(data):
|
|
115
|
+
dtype = getattr(data, "schema", {}).get(field)
|
|
116
|
+
if dtype is None:
|
|
117
|
+
return None
|
|
118
|
+
is_numeric = getattr(dtype, "is_numeric", None)
|
|
119
|
+
return "quantitative" if callable(is_numeric) and is_numeric() else "nominal"
|
|
120
|
+
|
|
121
|
+
if _is_pandas_frame(data):
|
|
122
|
+
dtypes = getattr(data, "dtypes", None)
|
|
123
|
+
if dtypes is None:
|
|
124
|
+
return None
|
|
125
|
+
try:
|
|
126
|
+
dtype = dtypes[field]
|
|
127
|
+
except (KeyError, TypeError):
|
|
128
|
+
return None
|
|
129
|
+
return "quantitative" if getattr(dtype, "kind", "") in "iufc" else "nominal"
|
|
130
|
+
|
|
131
|
+
if _is_pyarrow_table(data):
|
|
132
|
+
schema = getattr(data, "schema", None)
|
|
133
|
+
if schema is None:
|
|
134
|
+
return None
|
|
135
|
+
try:
|
|
136
|
+
arrow_type = schema.field(field).type
|
|
137
|
+
except (KeyError, TypeError, AttributeError):
|
|
138
|
+
return None
|
|
139
|
+
return (
|
|
140
|
+
"quantitative"
|
|
141
|
+
if str(arrow_type).startswith(("int", "uint", "float", "double", "decimal"))
|
|
142
|
+
else "nominal"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def infer_value_type(value: Any) -> str | None:
|
|
149
|
+
"""Infer a GenomeSpy encoding type for one Python value."""
|
|
150
|
+
if value is None:
|
|
151
|
+
return None
|
|
152
|
+
if isinstance(value, bool):
|
|
153
|
+
return "nominal"
|
|
154
|
+
if isinstance(value, int | float):
|
|
155
|
+
return "quantitative"
|
|
156
|
+
return "nominal"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def normalize_channel(
|
|
160
|
+
name: str,
|
|
161
|
+
value: Channel
|
|
162
|
+
| SchemaBase
|
|
163
|
+
| str
|
|
164
|
+
| dict[str, Any]
|
|
165
|
+
| Sequence[Channel | SchemaBase | str | dict[str, Any]]
|
|
166
|
+
| None,
|
|
167
|
+
*,
|
|
168
|
+
data: Any = None,
|
|
169
|
+
) -> dict[str, Any] | list[dict[str, Any]] | None:
|
|
170
|
+
"""Normalize one chart encoding channel definition."""
|
|
171
|
+
if value is None:
|
|
172
|
+
return None
|
|
173
|
+
if isinstance(value, Sequence) and not isinstance(value, str | bytes):
|
|
174
|
+
return [channel(item, encoding_name=name).to_dict() for item in value]
|
|
175
|
+
definition = channel(value).to_dict()
|
|
176
|
+
return normalized_channel_definition(name, definition, data=data)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def normalized_channel_definition(
|
|
180
|
+
name: str,
|
|
181
|
+
definition: dict[str, Any],
|
|
182
|
+
*,
|
|
183
|
+
data: Any = None,
|
|
184
|
+
) -> dict[str, Any]:
|
|
185
|
+
"""Normalize one mapping-form channel definition."""
|
|
186
|
+
normalized = dict(definition)
|
|
187
|
+
if name in {"x2", "y2", "key"}:
|
|
188
|
+
normalized.pop("type", None)
|
|
189
|
+
elif "type" not in normalized and isinstance(normalized.get("field"), str):
|
|
190
|
+
inferred_type = infer_field_type(normalized["field"], data)
|
|
191
|
+
if inferred_type is not None:
|
|
192
|
+
normalized["type"] = inferred_type
|
|
193
|
+
return normalized
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def merge_encoding_definitions(
|
|
197
|
+
current_encoding: Any,
|
|
198
|
+
updates: dict[
|
|
199
|
+
str,
|
|
200
|
+
Channel
|
|
201
|
+
| SchemaBase
|
|
202
|
+
| str
|
|
203
|
+
| dict[str, Any]
|
|
204
|
+
| Sequence[Channel | SchemaBase | str | dict[str, Any]]
|
|
205
|
+
| None,
|
|
206
|
+
],
|
|
207
|
+
*,
|
|
208
|
+
data: Any,
|
|
209
|
+
) -> dict[str, Any]:
|
|
210
|
+
"""Merge and normalize chart encoding updates."""
|
|
211
|
+
merged = {} if current_encoding is Undefined else dict(current_encoding)
|
|
212
|
+
for name, value in updates.items():
|
|
213
|
+
merged[name] = normalize_channel(name, value, data=data)
|
|
214
|
+
return merged
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def normalize_transform(transform: SchemaBase | dict[str, Any]) -> dict[str, Any]:
|
|
218
|
+
"""Normalize one transform definition."""
|
|
219
|
+
try:
|
|
220
|
+
return normalize_mapping_value(transform, key="transform", validate=False)
|
|
221
|
+
except TypeError as error:
|
|
222
|
+
raise TypeError(f"Unsupported transform value: {type(transform)!r}") from error
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def normalize_transform_kwarg(
|
|
226
|
+
value: SchemaBase | dict[str, Any],
|
|
227
|
+
*,
|
|
228
|
+
key: str,
|
|
229
|
+
) -> dict[str, Any]:
|
|
230
|
+
"""Normalize one nested transform keyword value."""
|
|
231
|
+
return normalize_mapping_value(value, key=key, validate=False)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Altair-style conditional encoding authoring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TypeAlias
|
|
6
|
+
|
|
7
|
+
from genome_spy._parameters import Parameter
|
|
8
|
+
from genome_spy.channels import Channel, channel
|
|
9
|
+
from genome_spy.schemapi import SchemaBase
|
|
10
|
+
|
|
11
|
+
ConditionValue: TypeAlias = Channel | SchemaBase | str | dict[str, object]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _branch_definition(value: ConditionValue) -> dict[str, object]:
|
|
15
|
+
return channel(value).to_dict()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _Then(Channel):
|
|
19
|
+
"""A conditional channel awaiting an optional fallback branch."""
|
|
20
|
+
|
|
21
|
+
def otherwise(self, value: ConditionValue) -> Channel:
|
|
22
|
+
"""Return the conditional channel with its fallback branch."""
|
|
23
|
+
return Channel({**_branch_definition(value), **self.to_dict()})
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _When:
|
|
27
|
+
"""A validated parameter predicate awaiting its true branch."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, predicate: Parameter) -> None:
|
|
30
|
+
self._predicate = predicate
|
|
31
|
+
|
|
32
|
+
def then(self, value: ConditionValue) -> _Then:
|
|
33
|
+
"""Return a conditional channel using ``value`` when selected."""
|
|
34
|
+
condition = {
|
|
35
|
+
"param": self._predicate.name,
|
|
36
|
+
"empty": self._predicate.empty,
|
|
37
|
+
**_branch_definition(value),
|
|
38
|
+
}
|
|
39
|
+
return _Then({"condition": condition})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def when(predicate: Parameter) -> _When:
|
|
43
|
+
"""Start an Altair-style selection condition.
|
|
44
|
+
|
|
45
|
+
GenomeSpy 0.86 supports selection parameters as conditional predicates.
|
|
46
|
+
Expression predicates will become available only if the upstream schema
|
|
47
|
+
adds that grammar.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
predicate: A point or interval selection parameter.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A builder whose ``then()`` method defines the selected branch.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
TypeError: If ``predicate`` is not a selection parameter.
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
>>> import genome_spy as gs
|
|
60
|
+
>>> brush = gs.selection_interval(encodings=["x"])
|
|
61
|
+
>>> condition = gs.when(brush).then(gs.value("red")).otherwise(
|
|
62
|
+
... gs.value("gray")
|
|
63
|
+
... )
|
|
64
|
+
>>> condition.to_dict()["condition"]["param"] == brush.name
|
|
65
|
+
True
|
|
66
|
+
"""
|
|
67
|
+
if not isinstance(predicate, Parameter) or not predicate.is_selection:
|
|
68
|
+
raise TypeError("when() currently requires a selection parameter.")
|
|
69
|
+
return _When(predicate)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = ["when"]
|
genome_spy/_embed.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Shared rendering configuration for GenomeSpy browser embeds."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Literal, TypeAlias
|
|
7
|
+
|
|
8
|
+
from genome_spy.schema import SCHEMA_VERSION
|
|
9
|
+
from genome_spy.schemapi import Undefined, UndefinedType
|
|
10
|
+
|
|
11
|
+
ControlName: TypeAlias = Literal["svg", "png", "inspector", "full-window"]
|
|
12
|
+
Controls: TypeAlias = bool | ControlName | Sequence[ControlName]
|
|
13
|
+
|
|
14
|
+
_CONTROL_DEFINITIONS: dict[ControlName, tuple[str, str]] = {
|
|
15
|
+
"svg": ("core", "svgButton"),
|
|
16
|
+
"png": ("core", "pngButton"),
|
|
17
|
+
"inspector": ("inspector", "inspectorButton"),
|
|
18
|
+
"full-window": ("core", "fullWindowButton"),
|
|
19
|
+
}
|
|
20
|
+
DEFAULT_CONTROLS: tuple[ControlName, ...] = ("svg", "png", "inspector")
|
|
21
|
+
SUPPORTED_CONTROLS: tuple[ControlName, ...] = tuple(_CONTROL_DEFINITIONS)
|
|
22
|
+
|
|
23
|
+
_CORE_PACKAGE_URL = (
|
|
24
|
+
f"https://cdn.jsdelivr.net/npm/@genome-spy/core@{SCHEMA_VERSION}/dist"
|
|
25
|
+
)
|
|
26
|
+
DEFAULT_EMBED_URL = f"{_CORE_PACKAGE_URL}/bundle/index.es.js"
|
|
27
|
+
DEFAULT_CONTROLS_MODULE_URL = f"{_CORE_PACKAGE_URL}/src/controls.js"
|
|
28
|
+
DEFAULT_INSPECTOR_MODULE_URL = (
|
|
29
|
+
"https://cdn.jsdelivr.net/npm/"
|
|
30
|
+
f"@genome-spy/inspector@{SCHEMA_VERSION}/dist/index.es.js"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def normalize_controls(
|
|
35
|
+
controls: Controls | UndefinedType = Undefined,
|
|
36
|
+
) -> tuple[ControlName, ...]:
|
|
37
|
+
"""Return validated control names in display order."""
|
|
38
|
+
if controls is Undefined or controls is True:
|
|
39
|
+
return DEFAULT_CONTROLS
|
|
40
|
+
if controls is False:
|
|
41
|
+
return ()
|
|
42
|
+
|
|
43
|
+
values: Sequence[str]
|
|
44
|
+
if isinstance(controls, str):
|
|
45
|
+
values = (controls,)
|
|
46
|
+
elif isinstance(controls, Sequence):
|
|
47
|
+
values = controls
|
|
48
|
+
else:
|
|
49
|
+
raise TypeError(
|
|
50
|
+
"controls must be a boolean, a control name, or a sequence of "
|
|
51
|
+
"control names."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
normalized: list[ControlName] = []
|
|
55
|
+
seen: set[str] = set()
|
|
56
|
+
for value in values:
|
|
57
|
+
if not isinstance(value, str):
|
|
58
|
+
raise TypeError("Every control name must be a string.")
|
|
59
|
+
if value not in SUPPORTED_CONTROLS:
|
|
60
|
+
expected = ", ".join(repr(name) for name in SUPPORTED_CONTROLS)
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Unknown GenomeSpy control {value!r}. Expected one of: {expected}."
|
|
63
|
+
)
|
|
64
|
+
if value in seen:
|
|
65
|
+
raise ValueError(f"GenomeSpy control {value!r} was specified twice.")
|
|
66
|
+
seen.add(value)
|
|
67
|
+
normalized.append(value)
|
|
68
|
+
return tuple(normalized)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def control_definitions() -> dict[str, dict[str, str]]:
|
|
72
|
+
"""Return browser module and export metadata for supported controls."""
|
|
73
|
+
return {
|
|
74
|
+
name: {"module": module, "export": export}
|
|
75
|
+
for name, (module, export) in _CONTROL_DEFINITIONS.items()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
"ControlName",
|
|
81
|
+
"Controls",
|
|
82
|
+
"DEFAULT_CONTROLS",
|
|
83
|
+
"DEFAULT_CONTROLS_MODULE_URL",
|
|
84
|
+
"DEFAULT_EMBED_URL",
|
|
85
|
+
"DEFAULT_INSPECTOR_MODULE_URL",
|
|
86
|
+
"SUPPORTED_CONTROLS",
|
|
87
|
+
]
|