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/chart.py
ADDED
|
@@ -0,0 +1,1240 @@
|
|
|
1
|
+
"""A compact but growing Chart API for GenomeSpy core specifications."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Protocol, Self, cast
|
|
10
|
+
from uuid import uuid4
|
|
11
|
+
|
|
12
|
+
from genome_spy._embed import (
|
|
13
|
+
DEFAULT_CONTROLS_MODULE_URL,
|
|
14
|
+
DEFAULT_EMBED_URL,
|
|
15
|
+
DEFAULT_INSPECTOR_MODULE_URL,
|
|
16
|
+
Controls,
|
|
17
|
+
control_definitions,
|
|
18
|
+
normalize_controls,
|
|
19
|
+
)
|
|
20
|
+
from genome_spy._utils import JsonSpec, compact_json, pretty_json
|
|
21
|
+
from genome_spy._chart_authoring import (
|
|
22
|
+
merge_encoding_definitions,
|
|
23
|
+
normalize_data,
|
|
24
|
+
normalize_transform,
|
|
25
|
+
)
|
|
26
|
+
from genome_spy.channels import Channel
|
|
27
|
+
from genome_spy.data_transformers import _DatasetConsolidation, data_transformers
|
|
28
|
+
from genome_spy.schema import (
|
|
29
|
+
ConcatSpec,
|
|
30
|
+
GenomeSpyConfig,
|
|
31
|
+
HConcatSpec,
|
|
32
|
+
ImportSpec,
|
|
33
|
+
LayerSpec,
|
|
34
|
+
MARK_TYPES,
|
|
35
|
+
MultiscaleSpec,
|
|
36
|
+
Root,
|
|
37
|
+
SCHEMA_VERSION,
|
|
38
|
+
UnitSpec,
|
|
39
|
+
VConcatSpec,
|
|
40
|
+
)
|
|
41
|
+
from genome_spy.schema.mixins import (
|
|
42
|
+
ConcatPropertiesMixin,
|
|
43
|
+
ConfigMethodMixin,
|
|
44
|
+
EncodingMethodMixin,
|
|
45
|
+
HConcatPropertiesMixin,
|
|
46
|
+
ImportedViewConstructorMixin,
|
|
47
|
+
LayerPropertiesMixin,
|
|
48
|
+
MarkMethodMixin,
|
|
49
|
+
MultiscalePropertiesMixin,
|
|
50
|
+
ResolutionMethodMixin,
|
|
51
|
+
TopLevelMergeMixin,
|
|
52
|
+
TransformMethodMixin,
|
|
53
|
+
UnitPropertiesMixin,
|
|
54
|
+
VConcatPropertiesMixin,
|
|
55
|
+
)
|
|
56
|
+
from genome_spy.schema.composition import (
|
|
57
|
+
concat as _concat,
|
|
58
|
+
hconcat as _hconcat,
|
|
59
|
+
layer as _layer,
|
|
60
|
+
multiscale as _multiscale,
|
|
61
|
+
import_view as _import_view,
|
|
62
|
+
vconcat as _vconcat,
|
|
63
|
+
)
|
|
64
|
+
from genome_spy.schemapi import (
|
|
65
|
+
SchemaBase,
|
|
66
|
+
Undefined,
|
|
67
|
+
UndefinedType,
|
|
68
|
+
merge_mapping_value,
|
|
69
|
+
normalize_mapping_value,
|
|
70
|
+
normalize_schema_value,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if TYPE_CHECKING:
|
|
74
|
+
from genome_spy._parameters import Parameter
|
|
75
|
+
from genome_spy._render import _PreparedSpec
|
|
76
|
+
|
|
77
|
+
_CORE_DIST_URL = f"https://cdn.jsdelivr.net/npm/@genome-spy/core@{SCHEMA_VERSION}/dist"
|
|
78
|
+
DEFAULT_SCHEMA_URL = f"{_CORE_DIST_URL}/schema.json"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _CopyableSpec(Protocol):
|
|
82
|
+
def _copy(self, *, deep: bool = True, **kwargs: Any) -> Any: ...
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _SerializableView(Protocol):
|
|
86
|
+
def to_dict(
|
|
87
|
+
self, *, include_schema: bool = True, validate: bool = True
|
|
88
|
+
) -> dict[str, Any]: ...
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
HTML_TEMPLATE = """
|
|
92
|
+
<div id="{container_id}"></div>
|
|
93
|
+
<script type="text/javascript">
|
|
94
|
+
(function(spec, moduleUrl, embedOptions, controlOptions) {{
|
|
95
|
+
let outputDiv = document.currentScript.previousElementSibling;
|
|
96
|
+
if (!outputDiv || outputDiv.id !== "{container_id}") {{
|
|
97
|
+
outputDiv = document.getElementById("{container_id}");
|
|
98
|
+
}}
|
|
99
|
+
|
|
100
|
+
function showError(error) {{
|
|
101
|
+
outputDiv.innerHTML = (
|
|
102
|
+
'<div style="color:red;">'
|
|
103
|
+
+ '<p>JavaScript Error: ' + error.message + '</p>'
|
|
104
|
+
+ '<p>GenomeSpy failed to render in this notebook frontend. '
|
|
105
|
+
+ 'See the browser console for details.</p>'
|
|
106
|
+
+ '</div>'
|
|
107
|
+
);
|
|
108
|
+
throw error;
|
|
109
|
+
}}
|
|
110
|
+
|
|
111
|
+
function showControlError(error) {{
|
|
112
|
+
const message = document.createElement("p");
|
|
113
|
+
message.setAttribute("role", "status");
|
|
114
|
+
message.style.color = "red";
|
|
115
|
+
message.textContent = "GenomeSpy controls failed to load: " + error.message;
|
|
116
|
+
outputDiv.appendChild(message);
|
|
117
|
+
console.error(error);
|
|
118
|
+
}}
|
|
119
|
+
|
|
120
|
+
(async function() {{
|
|
121
|
+
try {{
|
|
122
|
+
const module = await import(moduleUrl);
|
|
123
|
+
const embed = module.embed ?? module.default?.embed ?? module.default;
|
|
124
|
+
if (typeof embed !== "function") {{
|
|
125
|
+
throw new Error("GenomeSpy embed export was not found.");
|
|
126
|
+
}}
|
|
127
|
+
const api = await embed(outputDiv, spec, embedOptions);
|
|
128
|
+
if (controlOptions.names.length) {{
|
|
129
|
+
try {{
|
|
130
|
+
const controlsModule = await import(controlOptions.moduleUrls.core);
|
|
131
|
+
const modules = {{ core: controlsModule }};
|
|
132
|
+
const controls = [];
|
|
133
|
+
for (const name of controlOptions.names) {{
|
|
134
|
+
const definition = controlOptions.definitions[name];
|
|
135
|
+
if (!definition) {{
|
|
136
|
+
throw new Error(`Control ${{name}} has no definition.`);
|
|
137
|
+
}}
|
|
138
|
+
if (!modules[definition.module]) {{
|
|
139
|
+
const moduleUrl = controlOptions.moduleUrls[definition.module];
|
|
140
|
+
if (!moduleUrl) {{
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Control module ${{definition.module}} has no URL.`
|
|
143
|
+
);
|
|
144
|
+
}}
|
|
145
|
+
modules[definition.module] = await import(moduleUrl);
|
|
146
|
+
}}
|
|
147
|
+
const factory = modules[definition.module][definition.export];
|
|
148
|
+
if (typeof factory !== "function") {{
|
|
149
|
+
throw new Error(`Control ${{name}} is unavailable.`);
|
|
150
|
+
}}
|
|
151
|
+
controls.push(factory());
|
|
152
|
+
}}
|
|
153
|
+
controlsModule.attachControls(outputDiv, api, {{ controls }});
|
|
154
|
+
}} catch (error) {{
|
|
155
|
+
showControlError(error);
|
|
156
|
+
}}
|
|
157
|
+
}}
|
|
158
|
+
}} catch (error) {{
|
|
159
|
+
showError(error);
|
|
160
|
+
}}
|
|
161
|
+
}})();
|
|
162
|
+
}})({spec_json}, {module_url_json}, {embed_options_json}, {control_options_json});
|
|
163
|
+
</script>
|
|
164
|
+
""".strip()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _infer_encoding_name(value: Channel | SchemaBase | str | dict[str, Any]) -> str:
|
|
168
|
+
if isinstance(value, Channel) and value.encoding_name is not None:
|
|
169
|
+
return value.encoding_name
|
|
170
|
+
raise TypeError(
|
|
171
|
+
"Positional encodings must be channel objects such as X(...), Y(...), "
|
|
172
|
+
"Color(...), or Size(...)."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _merge_encoding_definitions(
|
|
177
|
+
current_encoding: Any,
|
|
178
|
+
updates: dict[
|
|
179
|
+
str,
|
|
180
|
+
Channel
|
|
181
|
+
| SchemaBase
|
|
182
|
+
| str
|
|
183
|
+
| dict[str, Any]
|
|
184
|
+
| Sequence[Channel | SchemaBase | str | dict[str, Any]]
|
|
185
|
+
| None,
|
|
186
|
+
],
|
|
187
|
+
*,
|
|
188
|
+
data: Any,
|
|
189
|
+
) -> dict[str, Any]:
|
|
190
|
+
return merge_encoding_definitions(current_encoding, updates, data=data)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _parameter_declaration_identity(value: Parameter | SchemaBase) -> tuple[str, bool]:
|
|
194
|
+
"""Return a parameter declaration's name and explicit-name status."""
|
|
195
|
+
from genome_spy._parameters import Parameter
|
|
196
|
+
from genome_spy.schema.ergonomics import _PARAMETER_TYPES
|
|
197
|
+
|
|
198
|
+
if isinstance(value, Parameter):
|
|
199
|
+
return value.name, value.name_is_explicit
|
|
200
|
+
if not isinstance(value, _PARAMETER_TYPES):
|
|
201
|
+
raise TypeError(
|
|
202
|
+
f"Expected a generated GenomeSpy parameter definition, got {type(value)!r}."
|
|
203
|
+
)
|
|
204
|
+
name = value.to_dict(validate=False).get("name")
|
|
205
|
+
if not isinstance(name, str):
|
|
206
|
+
raise TypeError("A parameter declaration must have a string name.")
|
|
207
|
+
return name, True
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class TopLevelSpec(TopLevelMergeMixin, EncodingMethodMixin, TransformMethodMixin):
|
|
211
|
+
"""Shared behavior for top-level GenomeSpy specifications."""
|
|
212
|
+
|
|
213
|
+
_schema_spec_cls: ClassVar[type[SchemaBase]]
|
|
214
|
+
_schema_url: str
|
|
215
|
+
|
|
216
|
+
def _initialize_spec(
|
|
217
|
+
self, *, properties: dict[str, Any], schema_url: str | None
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Initialize generated schema state for a top-level specification."""
|
|
220
|
+
self._schema_spec_cls.__init__(cast(Any, self), **properties)
|
|
221
|
+
self._schema_url = DEFAULT_SCHEMA_URL if schema_url is None else schema_url
|
|
222
|
+
|
|
223
|
+
def _merge_top_level(
|
|
224
|
+
self, name: str, value: Any, /, properties: Mapping[str, Any]
|
|
225
|
+
) -> Self:
|
|
226
|
+
"""Return a copy with one top-level mapping property merged."""
|
|
227
|
+
merged = merge_mapping_value(
|
|
228
|
+
self._kwds.get(name, Undefined), # type: ignore[attr-defined]
|
|
229
|
+
name,
|
|
230
|
+
value,
|
|
231
|
+
**properties,
|
|
232
|
+
)
|
|
233
|
+
return cast(Self, cast(_CopyableSpec, self)._copy(deep=False, **{name: merged}))
|
|
234
|
+
|
|
235
|
+
def _merged_encoding(
|
|
236
|
+
self,
|
|
237
|
+
args: tuple[Channel, ...],
|
|
238
|
+
kwargs: dict[
|
|
239
|
+
str,
|
|
240
|
+
Channel
|
|
241
|
+
| SchemaBase
|
|
242
|
+
| str
|
|
243
|
+
| dict[str, Any]
|
|
244
|
+
| Sequence[Channel | SchemaBase | str | dict[str, Any]]
|
|
245
|
+
| None,
|
|
246
|
+
],
|
|
247
|
+
) -> dict[str, Any]:
|
|
248
|
+
"""Return merged encoding definitions for fluent ``encode(...)`` calls."""
|
|
249
|
+
updates = dict(kwargs)
|
|
250
|
+
for arg in args:
|
|
251
|
+
name = _infer_encoding_name(arg)
|
|
252
|
+
if name in updates:
|
|
253
|
+
raise TypeError(f"Encoding channel {name!r} was specified twice.")
|
|
254
|
+
updates[name] = arg
|
|
255
|
+
|
|
256
|
+
current_encoding = self._kwds.get("encoding", Undefined) # type: ignore[attr-defined]
|
|
257
|
+
data = self._kwds.get("data", Undefined) # type: ignore[attr-defined]
|
|
258
|
+
return _merge_encoding_definitions(current_encoding, updates, data=data)
|
|
259
|
+
|
|
260
|
+
def _encode(
|
|
261
|
+
self,
|
|
262
|
+
args: tuple[Channel, ...],
|
|
263
|
+
properties: dict[
|
|
264
|
+
str,
|
|
265
|
+
Channel
|
|
266
|
+
| SchemaBase
|
|
267
|
+
| str
|
|
268
|
+
| dict[str, Any]
|
|
269
|
+
| Sequence[Channel | SchemaBase | str | dict[str, Any]]
|
|
270
|
+
| None,
|
|
271
|
+
],
|
|
272
|
+
) -> Self:
|
|
273
|
+
"""Return a copy with generated encoding arguments merged."""
|
|
274
|
+
merged = self._merged_encoding(args, properties)
|
|
275
|
+
return cast(
|
|
276
|
+
Self,
|
|
277
|
+
cast(_CopyableSpec, self)._copy(deep=False, encoding=merged),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def _config_object(self) -> GenomeSpyConfig:
|
|
281
|
+
"""Return the current top-level config as a schema wrapper."""
|
|
282
|
+
current = self._kwds.get("config", Undefined) # type: ignore[attr-defined]
|
|
283
|
+
if current is Undefined:
|
|
284
|
+
return GenomeSpyConfig()
|
|
285
|
+
if current is None:
|
|
286
|
+
raise TypeError("Cannot configure nested properties into null 'config'.")
|
|
287
|
+
if isinstance(current, GenomeSpyConfig):
|
|
288
|
+
return current
|
|
289
|
+
return GenomeSpyConfig(
|
|
290
|
+
**normalize_mapping_value(current, key="config", validate=False)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _configured_nested(
|
|
294
|
+
self,
|
|
295
|
+
name: str,
|
|
296
|
+
value: SchemaBase | dict[str, Any] | None | object = Undefined,
|
|
297
|
+
/,
|
|
298
|
+
**kwargs: Any,
|
|
299
|
+
) -> GenomeSpyConfig:
|
|
300
|
+
"""Return a config object with one nested property updated."""
|
|
301
|
+
return self._config_object()._with_property(name, value, **kwargs)
|
|
302
|
+
|
|
303
|
+
def _configured_property(self, name: str, value: Any) -> GenomeSpyConfig:
|
|
304
|
+
"""Return a config object with one scalar property updated."""
|
|
305
|
+
config = self._config_object()
|
|
306
|
+
return config._with_property(name, value)
|
|
307
|
+
|
|
308
|
+
def _configure_nested(
|
|
309
|
+
self,
|
|
310
|
+
name: str,
|
|
311
|
+
value: SchemaBase | dict[str, Any] | None | object = Undefined,
|
|
312
|
+
/,
|
|
313
|
+
**kwargs: Any,
|
|
314
|
+
) -> Self:
|
|
315
|
+
"""Return a copy with one nested config family updated."""
|
|
316
|
+
return cast(
|
|
317
|
+
Self,
|
|
318
|
+
cast(_CopyableSpec, self)._copy(
|
|
319
|
+
deep=False, config=self._configured_nested(name, value, **kwargs)
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def _configure_property(self, name: str, value: Any) -> Self:
|
|
324
|
+
"""Return a copy with one scalar config property updated."""
|
|
325
|
+
return cast(
|
|
326
|
+
Self,
|
|
327
|
+
cast(_CopyableSpec, self)._copy(
|
|
328
|
+
deep=False, config=self._configured_property(name, value)
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def _properties(self, **kwargs: Any) -> Self:
|
|
333
|
+
"""Return a new spec with merged top-level properties."""
|
|
334
|
+
return self._with_properties(kwargs)
|
|
335
|
+
|
|
336
|
+
def _normalized_properties(self, properties: dict[str, Any]) -> dict[str, Any]:
|
|
337
|
+
"""Return top-level properties normalized for schema-backed copying."""
|
|
338
|
+
normalized: dict[str, Any] = {}
|
|
339
|
+
for key, value in properties.items():
|
|
340
|
+
if key == "params" and isinstance(value, Sequence):
|
|
341
|
+
normalized[key] = list(value)
|
|
342
|
+
elif key == "templates" and isinstance(value, Mapping):
|
|
343
|
+
normalized[key] = {
|
|
344
|
+
name: (
|
|
345
|
+
template.to_dict(include_schema=False, validate=False)
|
|
346
|
+
if isinstance(template, TopLevelSpec)
|
|
347
|
+
else normalize_schema_value(template, validate=False)
|
|
348
|
+
)
|
|
349
|
+
for name, template in value.items()
|
|
350
|
+
}
|
|
351
|
+
else:
|
|
352
|
+
normalized[key] = normalize_schema_value(value, validate=False)
|
|
353
|
+
return normalized
|
|
354
|
+
|
|
355
|
+
def add_params(self, *params: Parameter | SchemaBase) -> Self:
|
|
356
|
+
"""Return a chart with parameter declarations appended.
|
|
357
|
+
|
|
358
|
+
Parameter handles are unwrapped only when the chart is serialized, so
|
|
359
|
+
the same handle can also be reused in expressions, conditions, and
|
|
360
|
+
filters.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
*params: Parameter handles or generated parameter definitions.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
A new chart with the declarations appended in argument order.
|
|
367
|
+
|
|
368
|
+
Raises:
|
|
369
|
+
TypeError: If an argument is not a parameter declaration.
|
|
370
|
+
ValueError: If an explicit parameter name is declared twice.
|
|
371
|
+
|
|
372
|
+
Example:
|
|
373
|
+
>>> import genome_spy as gs
|
|
374
|
+
>>> cutoff = gs.param("cutoff", value=0.5)
|
|
375
|
+
>>> gs.Chart().add_params(cutoff).to_dict(validate=False)["params"]
|
|
376
|
+
[{'name': 'cutoff', 'value': 0.5}]
|
|
377
|
+
"""
|
|
378
|
+
from genome_spy._parameters import Parameter
|
|
379
|
+
|
|
380
|
+
current = self._kwds.get("params", Undefined) # type: ignore[attr-defined]
|
|
381
|
+
declarations = [] if current is Undefined else list(current)
|
|
382
|
+
names: dict[str, bool] = {}
|
|
383
|
+
for declaration in declarations:
|
|
384
|
+
name, explicit = _parameter_declaration_identity(declaration)
|
|
385
|
+
names[name] = explicit
|
|
386
|
+
|
|
387
|
+
for parameter in params:
|
|
388
|
+
if not isinstance(parameter, Parameter | SchemaBase):
|
|
389
|
+
raise TypeError(
|
|
390
|
+
"add_params() arguments must be parameter handles or generated "
|
|
391
|
+
f"schema definitions, got {type(parameter)!r}."
|
|
392
|
+
)
|
|
393
|
+
name, explicit = _parameter_declaration_identity(parameter)
|
|
394
|
+
if name in names:
|
|
395
|
+
if explicit or names[name]:
|
|
396
|
+
raise ValueError(f"Parameter name {name!r} is already declared.")
|
|
397
|
+
continue
|
|
398
|
+
names[name] = explicit
|
|
399
|
+
declarations.append(parameter)
|
|
400
|
+
return cast(
|
|
401
|
+
Self, cast(_CopyableSpec, self)._copy(deep=False, params=declarations)
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
def _appended_transform(self, transform: dict[str, Any]) -> Self:
|
|
405
|
+
"""Return a copy with one normalized transform appended."""
|
|
406
|
+
current = self._kwds.get("transform", Undefined) # type: ignore[attr-defined]
|
|
407
|
+
merged = [] if current is Undefined else list(current)
|
|
408
|
+
merged.append(normalize_transform(transform))
|
|
409
|
+
return cast(Self, cast(_CopyableSpec, self)._copy(deep=False, transform=merged))
|
|
410
|
+
|
|
411
|
+
def _configured(
|
|
412
|
+
self,
|
|
413
|
+
value: SchemaBase | dict[str, Any] | None | object = Undefined,
|
|
414
|
+
/,
|
|
415
|
+
**kwargs: Any,
|
|
416
|
+
) -> Self:
|
|
417
|
+
"""Return a copy with top-level config merged."""
|
|
418
|
+
merged = merge_mapping_value(
|
|
419
|
+
self._kwds.get("config", Undefined), # type: ignore[attr-defined]
|
|
420
|
+
"config",
|
|
421
|
+
value,
|
|
422
|
+
**kwargs,
|
|
423
|
+
)
|
|
424
|
+
return cast(Self, cast(_CopyableSpec, self)._copy(deep=False, config=merged))
|
|
425
|
+
|
|
426
|
+
def _with_properties(self, properties: dict[str, Any]) -> Self:
|
|
427
|
+
"""Return a copy with normalized top-level properties applied."""
|
|
428
|
+
return cast(
|
|
429
|
+
Self,
|
|
430
|
+
cast(_CopyableSpec, self)._copy(
|
|
431
|
+
deep=False,
|
|
432
|
+
**self._normalized_properties(properties),
|
|
433
|
+
),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
def _copy_with_properties(self, *, deep: bool, properties: dict[str, Any]) -> Self:
|
|
437
|
+
"""Return a copy with generated explicit top-level updates."""
|
|
438
|
+
return cast(
|
|
439
|
+
Self,
|
|
440
|
+
cast(_CopyableSpec, self)._copy(
|
|
441
|
+
deep=deep,
|
|
442
|
+
**self._normalized_properties(properties),
|
|
443
|
+
),
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
def _merged_resolution(
|
|
447
|
+
self, key: str, updates: Mapping[str, Any]
|
|
448
|
+
) -> dict[str, Any]:
|
|
449
|
+
"""Return a merged composition ``resolve`` mapping."""
|
|
450
|
+
current = self._kwds.get("resolve", Undefined) # type: ignore[attr-defined]
|
|
451
|
+
merged: dict[str, Any] = {} if current is Undefined else dict(current)
|
|
452
|
+
current_values = merged.get(key, Undefined)
|
|
453
|
+
merged_values: dict[str, Any] = (
|
|
454
|
+
{} if current_values is Undefined else dict(current_values)
|
|
455
|
+
)
|
|
456
|
+
for name, value in updates.items():
|
|
457
|
+
merged_values[name] = normalize_schema_value(value, validate=False)
|
|
458
|
+
merged[key] = merged_values
|
|
459
|
+
return merged
|
|
460
|
+
|
|
461
|
+
def _with_resolution(self, key: str, updates: Mapping[str, Any]) -> Self:
|
|
462
|
+
"""Return a copy with one composition resolution family merged."""
|
|
463
|
+
return cast(
|
|
464
|
+
Self,
|
|
465
|
+
cast(_CopyableSpec, self)._copy(
|
|
466
|
+
deep=False,
|
|
467
|
+
resolve=self._merged_resolution(key, updates),
|
|
468
|
+
),
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
def _serialized_top_level_values(
|
|
472
|
+
self, *, normalize_chart_data: Callable[[Any], Any] = normalize_data
|
|
473
|
+
) -> dict[str, Any]:
|
|
474
|
+
"""Return copied top-level state with authoring-edge values normalized."""
|
|
475
|
+
values = dict(self._kwds) # type: ignore[attr-defined]
|
|
476
|
+
|
|
477
|
+
def nested_view(value: Any) -> Any:
|
|
478
|
+
if isinstance(value, TopLevelSpec):
|
|
479
|
+
return value._to_dict(
|
|
480
|
+
include_schema=False,
|
|
481
|
+
validate=False,
|
|
482
|
+
normalize_chart_data=normalize_chart_data,
|
|
483
|
+
)
|
|
484
|
+
if isinstance(value, (list, tuple)):
|
|
485
|
+
return [nested_view(child) for child in value]
|
|
486
|
+
if isinstance(value, dict):
|
|
487
|
+
return {key: nested_view(child) for key, child in value.items()}
|
|
488
|
+
return value
|
|
489
|
+
|
|
490
|
+
# Additional view-bearing properties use the same serialization context
|
|
491
|
+
# as composition children. Data rows are opaque; templates keep their
|
|
492
|
+
# independent import scope and the primary children are handled below.
|
|
493
|
+
excluded = {
|
|
494
|
+
"data",
|
|
495
|
+
"datasets",
|
|
496
|
+
"templates",
|
|
497
|
+
getattr(self, "_children_key", None),
|
|
498
|
+
}
|
|
499
|
+
values = {
|
|
500
|
+
key: value if key in excluded else nested_view(value)
|
|
501
|
+
for key, value in values.items()
|
|
502
|
+
}
|
|
503
|
+
data = values.get("data", Undefined)
|
|
504
|
+
if data is not Undefined:
|
|
505
|
+
normalized_data = normalize_chart_data(data)
|
|
506
|
+
if normalized_data is None:
|
|
507
|
+
values.pop("data")
|
|
508
|
+
else:
|
|
509
|
+
values["data"] = normalized_data
|
|
510
|
+
params = values.get("params", Undefined)
|
|
511
|
+
if params is not Undefined:
|
|
512
|
+
from genome_spy._parameters import _unwrap_parameter
|
|
513
|
+
|
|
514
|
+
values["params"] = [
|
|
515
|
+
normalize_schema_value(_unwrap_parameter(param), validate=False)
|
|
516
|
+
for param in params
|
|
517
|
+
]
|
|
518
|
+
return values
|
|
519
|
+
|
|
520
|
+
def _validated_root_spec(
|
|
521
|
+
self,
|
|
522
|
+
spec: dict[str, Any],
|
|
523
|
+
*,
|
|
524
|
+
include_schema: bool,
|
|
525
|
+
validate: bool,
|
|
526
|
+
) -> dict[str, Any]:
|
|
527
|
+
"""Return a root-validated spec dictionary ready for serialization."""
|
|
528
|
+
if include_schema:
|
|
529
|
+
spec["$schema"] = self._schema_url
|
|
530
|
+
return Root(**spec).to_dict(validate=validate)
|
|
531
|
+
|
|
532
|
+
def transform(self, *transforms: SchemaBase | dict[str, Any]) -> Self:
|
|
533
|
+
"""Add one or more arbitrary GenomeSpy transforms.
|
|
534
|
+
|
|
535
|
+
Description:
|
|
536
|
+
Use this generic method when GenomeSpy supports a transform that
|
|
537
|
+
does not yet have a dedicated handwritten helper in the Python API.
|
|
538
|
+
Each transform may be a raw mapping or a generated schema wrapper.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
*transforms: One or more transform definitions.
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
A new spec with the transforms appended in order.
|
|
545
|
+
|
|
546
|
+
Raises:
|
|
547
|
+
TypeError: If a transform is not a mapping or schema wrapper.
|
|
548
|
+
|
|
549
|
+
Example:
|
|
550
|
+
>>> chart.transform({"type": "collect", "sort": {"field": ["x"]}})
|
|
551
|
+
"""
|
|
552
|
+
result = self
|
|
553
|
+
for transform in transforms:
|
|
554
|
+
result = result._append_transform(normalize_transform(transform))
|
|
555
|
+
return result
|
|
556
|
+
|
|
557
|
+
@classmethod
|
|
558
|
+
def from_dict(
|
|
559
|
+
cls, spec: Mapping[str, Any], *, validate: bool = True
|
|
560
|
+
) -> TopLevelSpec:
|
|
561
|
+
"""Construct a renderable chart from a GenomeSpy specification.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
spec: Complete GenomeSpy specification.
|
|
565
|
+
validate: Validate the input against the generated root schema.
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
A chart matching the specification's structural root variant.
|
|
569
|
+
|
|
570
|
+
Raises:
|
|
571
|
+
SchemaValidationError: If validation fails.
|
|
572
|
+
TypeError: If the specification is not a mapping.
|
|
573
|
+
ValueError: If no supported root structure is present.
|
|
574
|
+
|
|
575
|
+
Example:
|
|
576
|
+
>>> chart = TopLevelSpec.from_dict({"mark": "point"})
|
|
577
|
+
"""
|
|
578
|
+
if not isinstance(spec, Mapping):
|
|
579
|
+
raise TypeError(
|
|
580
|
+
f"GenomeSpy specification must be a mapping, got {type(spec)!r}"
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
values = deepcopy(dict(spec))
|
|
584
|
+
if validate:
|
|
585
|
+
Root(**values).to_dict()
|
|
586
|
+
|
|
587
|
+
schema_url = values.pop("$schema", DEFAULT_SCHEMA_URL)
|
|
588
|
+
if not isinstance(schema_url, str):
|
|
589
|
+
raise TypeError("The $schema property must be a string.")
|
|
590
|
+
return cast(
|
|
591
|
+
TopLevelSpec,
|
|
592
|
+
_view_from_dict(values, schema_url=schema_url, allow_import=False),
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
@classmethod
|
|
596
|
+
def from_json(cls, json_string: str, *, validate: bool = True) -> TopLevelSpec:
|
|
597
|
+
"""Construct a renderable chart from a JSON specification.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
json_string: JSON-encoded GenomeSpy specification.
|
|
601
|
+
validate: Validate the input against the generated root schema.
|
|
602
|
+
|
|
603
|
+
Returns:
|
|
604
|
+
A chart matching the specification's structural root variant.
|
|
605
|
+
|
|
606
|
+
Raises:
|
|
607
|
+
json.JSONDecodeError: If ``json_string`` is invalid JSON.
|
|
608
|
+
SchemaValidationError: If schema validation fails.
|
|
609
|
+
TypeError: If the decoded value is not a mapping.
|
|
610
|
+
ValueError: If no supported root structure is present.
|
|
611
|
+
|
|
612
|
+
Example:
|
|
613
|
+
>>> chart = TopLevelSpec.from_json('{"mark": "point"}')
|
|
614
|
+
"""
|
|
615
|
+
decoded = json.loads(json_string)
|
|
616
|
+
if not isinstance(decoded, dict):
|
|
617
|
+
raise TypeError("GenomeSpy JSON specification must decode to an object.")
|
|
618
|
+
return cls.from_dict(decoded, validate=validate)
|
|
619
|
+
|
|
620
|
+
def to_dict(
|
|
621
|
+
self, *, include_schema: bool = True, validate: bool = True
|
|
622
|
+
) -> dict[str, Any]:
|
|
623
|
+
"""Serialize the spec to a JSON-compatible dictionary."""
|
|
624
|
+
raise NotImplementedError
|
|
625
|
+
|
|
626
|
+
def _to_dict(
|
|
627
|
+
self,
|
|
628
|
+
*,
|
|
629
|
+
include_schema: bool,
|
|
630
|
+
validate: bool,
|
|
631
|
+
normalize_chart_data: Callable[[Any], Any],
|
|
632
|
+
) -> dict[str, Any]:
|
|
633
|
+
"""Serialize with a caller-provided data-normalization policy."""
|
|
634
|
+
raise NotImplementedError
|
|
635
|
+
|
|
636
|
+
def _prepare_render(self) -> _PreparedSpec:
|
|
637
|
+
"""Prepare this chart for a renderer that supports binary buffers."""
|
|
638
|
+
from genome_spy._render import prepare_render
|
|
639
|
+
|
|
640
|
+
return prepare_render(self)
|
|
641
|
+
|
|
642
|
+
def _serialize(self, *, include_schema: bool, validate: bool) -> dict[str, Any]:
|
|
643
|
+
enabled = data_transformers.consolidate_datasets
|
|
644
|
+
collector = _DatasetConsolidation(self) if enabled else None
|
|
645
|
+
spec = self._to_dict(
|
|
646
|
+
include_schema=include_schema,
|
|
647
|
+
validate=False,
|
|
648
|
+
normalize_chart_data=(
|
|
649
|
+
(lambda data: collector.source(normalize_data(data)))
|
|
650
|
+
if collector is not None
|
|
651
|
+
else normalize_data
|
|
652
|
+
),
|
|
653
|
+
)
|
|
654
|
+
if collector is not None:
|
|
655
|
+
collector.finish(spec)
|
|
656
|
+
return Root(**spec).to_dict(validate=validate)
|
|
657
|
+
|
|
658
|
+
def _prepare_widget(self) -> Any:
|
|
659
|
+
"""Prepare this chart for live named-dataset widget updates."""
|
|
660
|
+
from genome_spy._render import prepare_widget
|
|
661
|
+
|
|
662
|
+
return prepare_widget(self)
|
|
663
|
+
|
|
664
|
+
@property
|
|
665
|
+
def spec(self) -> JsonSpec:
|
|
666
|
+
"""Return the rendered GenomeSpy specification with JSON display."""
|
|
667
|
+
return JsonSpec(self.to_dict())
|
|
668
|
+
|
|
669
|
+
def to_json(self, *, include_schema: bool = True, validate: bool = True) -> str:
|
|
670
|
+
"""Serialize the spec to formatted JSON."""
|
|
671
|
+
return pretty_json(
|
|
672
|
+
self.to_dict(include_schema=include_schema, validate=validate)
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
def __str__(self) -> str:
|
|
676
|
+
"""Print charts as the JSON spec for notebook/debugging workflows."""
|
|
677
|
+
return self.to_json()
|
|
678
|
+
|
|
679
|
+
def to_html(
|
|
680
|
+
self,
|
|
681
|
+
*,
|
|
682
|
+
bundle_url: str = DEFAULT_EMBED_URL,
|
|
683
|
+
embed_options: Mapping[str, Any] | None = None,
|
|
684
|
+
controls: Controls | UndefinedType = Undefined,
|
|
685
|
+
controls_module_url: str = DEFAULT_CONTROLS_MODULE_URL,
|
|
686
|
+
inspector_module_url: str = DEFAULT_INSPECTOR_MODULE_URL,
|
|
687
|
+
container_id: str | None = None,
|
|
688
|
+
) -> str:
|
|
689
|
+
"""Render the chart as an HTML snippet.
|
|
690
|
+
|
|
691
|
+
Description:
|
|
692
|
+
The snippet loads the pinned GenomeSpy browser modules. Controls
|
|
693
|
+
affect only this HTML representation and are not serialized into
|
|
694
|
+
the chart specification.
|
|
695
|
+
|
|
696
|
+
Args:
|
|
697
|
+
bundle_url: Browser module containing GenomeSpy's ``embed`` function.
|
|
698
|
+
embed_options: Options passed directly to ``embed``.
|
|
699
|
+
controls: Controls to mount, ``True`` for defaults, or ``False`` to
|
|
700
|
+
disable them.
|
|
701
|
+
controls_module_url: Browser module containing Core controls.
|
|
702
|
+
inspector_module_url: Browser module containing the Inspector control.
|
|
703
|
+
container_id: Optional HTML id for the chart container.
|
|
704
|
+
|
|
705
|
+
Returns:
|
|
706
|
+
An HTML snippet containing the chart specification and embed code.
|
|
707
|
+
|
|
708
|
+
Raises:
|
|
709
|
+
TypeError: If the controls value has an invalid type.
|
|
710
|
+
ValueError: If a control name is unknown or duplicated.
|
|
711
|
+
|
|
712
|
+
Example:
|
|
713
|
+
>>> chart.to_html(controls=["svg", "png"])
|
|
714
|
+
"""
|
|
715
|
+
container_id = container_id or f"genome-spy-{uuid4().hex}"
|
|
716
|
+
spec_json = compact_json(self.to_dict())
|
|
717
|
+
module_url_json = compact_json(bundle_url)
|
|
718
|
+
embed_options_json = compact_json(dict(embed_options or {}))
|
|
719
|
+
control_options_json = compact_json(
|
|
720
|
+
{
|
|
721
|
+
"names": normalize_controls(controls),
|
|
722
|
+
"definitions": control_definitions(),
|
|
723
|
+
"moduleUrls": {
|
|
724
|
+
"core": controls_module_url,
|
|
725
|
+
"inspector": inspector_module_url,
|
|
726
|
+
},
|
|
727
|
+
}
|
|
728
|
+
)
|
|
729
|
+
return HTML_TEMPLATE.format(
|
|
730
|
+
container_id=container_id,
|
|
731
|
+
spec_json=spec_json,
|
|
732
|
+
module_url_json=module_url_json,
|
|
733
|
+
embed_options_json=embed_options_json,
|
|
734
|
+
control_options_json=control_options_json,
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
def save(
|
|
738
|
+
self,
|
|
739
|
+
path: str | Path,
|
|
740
|
+
*,
|
|
741
|
+
format: str | None = None,
|
|
742
|
+
bundle_url: str = DEFAULT_EMBED_URL,
|
|
743
|
+
embed_options: Mapping[str, Any] | None = None,
|
|
744
|
+
controls: Controls | UndefinedType = Undefined,
|
|
745
|
+
controls_module_url: str = DEFAULT_CONTROLS_MODULE_URL,
|
|
746
|
+
inspector_module_url: str = DEFAULT_INSPECTOR_MODULE_URL,
|
|
747
|
+
) -> None:
|
|
748
|
+
"""Save the chart as JSON or HTML.
|
|
749
|
+
|
|
750
|
+
Description:
|
|
751
|
+
The filename suffix selects the format unless ``format`` is given.
|
|
752
|
+
Rendering controls and embed options apply only to HTML output.
|
|
753
|
+
|
|
754
|
+
Args:
|
|
755
|
+
path: Destination path.
|
|
756
|
+
format: Explicit ``"json"`` or ``"html"`` format.
|
|
757
|
+
bundle_url: Browser module containing GenomeSpy's ``embed`` function.
|
|
758
|
+
embed_options: Options passed directly to ``embed`` for HTML output.
|
|
759
|
+
controls: HTML controls, ``True`` for defaults, or ``False`` to
|
|
760
|
+
disable them.
|
|
761
|
+
controls_module_url: Browser module containing Core controls.
|
|
762
|
+
inspector_module_url: Browser module containing the Inspector control.
|
|
763
|
+
|
|
764
|
+
Returns:
|
|
765
|
+
None.
|
|
766
|
+
|
|
767
|
+
Raises:
|
|
768
|
+
ValueError: If the format is unsupported or HTML-only options are
|
|
769
|
+
supplied for JSON output.
|
|
770
|
+
|
|
771
|
+
Example:
|
|
772
|
+
>>> chart.save("chart.html", controls=False)
|
|
773
|
+
"""
|
|
774
|
+
output_path = Path(path)
|
|
775
|
+
file_format = format or output_path.suffix.lstrip(".")
|
|
776
|
+
if file_format == "json":
|
|
777
|
+
html_options = [
|
|
778
|
+
name
|
|
779
|
+
for name, supplied in (
|
|
780
|
+
("bundle_url", bundle_url != DEFAULT_EMBED_URL),
|
|
781
|
+
("embed_options", embed_options is not None),
|
|
782
|
+
("controls", controls is not Undefined),
|
|
783
|
+
(
|
|
784
|
+
"controls_module_url",
|
|
785
|
+
controls_module_url != DEFAULT_CONTROLS_MODULE_URL,
|
|
786
|
+
),
|
|
787
|
+
(
|
|
788
|
+
"inspector_module_url",
|
|
789
|
+
inspector_module_url != DEFAULT_INSPECTOR_MODULE_URL,
|
|
790
|
+
),
|
|
791
|
+
)
|
|
792
|
+
if supplied
|
|
793
|
+
]
|
|
794
|
+
if html_options:
|
|
795
|
+
raise ValueError(
|
|
796
|
+
f"The {', '.join(html_options)} option(s) apply only to HTML "
|
|
797
|
+
"rendering."
|
|
798
|
+
)
|
|
799
|
+
output_path.write_text(self.to_json() + "\n", encoding="utf-8")
|
|
800
|
+
return
|
|
801
|
+
if file_format == "html":
|
|
802
|
+
output_path.write_text(
|
|
803
|
+
self.to_html(
|
|
804
|
+
bundle_url=bundle_url,
|
|
805
|
+
embed_options=embed_options,
|
|
806
|
+
controls=controls,
|
|
807
|
+
controls_module_url=controls_module_url,
|
|
808
|
+
inspector_module_url=inspector_module_url,
|
|
809
|
+
)
|
|
810
|
+
+ "\n",
|
|
811
|
+
encoding="utf-8",
|
|
812
|
+
)
|
|
813
|
+
return
|
|
814
|
+
raise ValueError("Unsupported format. Use 'json' or 'html'.")
|
|
815
|
+
|
|
816
|
+
def widget(
|
|
817
|
+
self,
|
|
818
|
+
*,
|
|
819
|
+
bundle_url: str = DEFAULT_EMBED_URL,
|
|
820
|
+
embed_options: dict[str, Any] | None = None,
|
|
821
|
+
controls: Controls | UndefinedType = Undefined,
|
|
822
|
+
controls_module_url: str = DEFAULT_CONTROLS_MODULE_URL,
|
|
823
|
+
inspector_module_url: str = DEFAULT_INSPECTOR_MODULE_URL,
|
|
824
|
+
parameter_names: Sequence[str] = (),
|
|
825
|
+
parameter_values: Mapping[str, Any] | None = None,
|
|
826
|
+
enable_click_events: bool = False,
|
|
827
|
+
) -> Any:
|
|
828
|
+
"""Create a notebook widget for the spec.
|
|
829
|
+
|
|
830
|
+
Args:
|
|
831
|
+
bundle_url: GenomeSpy bundle URL used by the widget.
|
|
832
|
+
embed_options: Options passed to GenomeSpy's ``embed`` function.
|
|
833
|
+
controls: Display controls, or ``False`` to disable them.
|
|
834
|
+
controls_module_url: Browser module containing Core controls.
|
|
835
|
+
inspector_module_url: Browser module containing the Inspector control.
|
|
836
|
+
parameter_names: Named GenomeSpy parameters synchronized with the
|
|
837
|
+
widget's ``parameter_values`` trait.
|
|
838
|
+
parameter_values: Initial values for the synchronized parameters.
|
|
839
|
+
enable_click_events: Whether clicked mark data is synchronized to
|
|
840
|
+
``clicked_datum`` and ``click_revision``.
|
|
841
|
+
|
|
842
|
+
Returns:
|
|
843
|
+
An anywidget-backed :class:`JupyterChart`.
|
|
844
|
+
|
|
845
|
+
Raises:
|
|
846
|
+
TypeError: If the controls value has an invalid type.
|
|
847
|
+
ValueError: If a control name is unknown or duplicated.
|
|
848
|
+
|
|
849
|
+
Example:
|
|
850
|
+
>>> widget = chart.widget(controls=["png", "inspector"])
|
|
851
|
+
"""
|
|
852
|
+
from genome_spy.jupyter import JupyterChart
|
|
853
|
+
|
|
854
|
+
return JupyterChart(
|
|
855
|
+
self,
|
|
856
|
+
bundle_url=bundle_url,
|
|
857
|
+
embed_options=embed_options,
|
|
858
|
+
controls=controls,
|
|
859
|
+
controls_module_url=controls_module_url,
|
|
860
|
+
inspector_module_url=inspector_module_url,
|
|
861
|
+
parameter_names=parameter_names,
|
|
862
|
+
parameter_values=parameter_values,
|
|
863
|
+
enable_click_events=enable_click_events,
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
def display(
|
|
867
|
+
self,
|
|
868
|
+
*,
|
|
869
|
+
bundle_url: str = DEFAULT_EMBED_URL,
|
|
870
|
+
embed_options: dict[str, Any] | None = None,
|
|
871
|
+
controls: Controls | UndefinedType = Undefined,
|
|
872
|
+
controls_module_url: str = DEFAULT_CONTROLS_MODULE_URL,
|
|
873
|
+
inspector_module_url: str = DEFAULT_INSPECTOR_MODULE_URL,
|
|
874
|
+
) -> None:
|
|
875
|
+
"""Display this chart once with temporary rendering options.
|
|
876
|
+
|
|
877
|
+
Description:
|
|
878
|
+
This mirrors Altair's display-time configuration: the supplied
|
|
879
|
+
options affect this display call without changing chart JSON.
|
|
880
|
+
|
|
881
|
+
Args:
|
|
882
|
+
bundle_url: Browser module containing GenomeSpy's ``embed`` function.
|
|
883
|
+
embed_options: Options passed directly to ``embed``.
|
|
884
|
+
controls: Controls to mount, ``True`` for defaults, or ``False`` to
|
|
885
|
+
disable them.
|
|
886
|
+
controls_module_url: Browser module containing Core controls.
|
|
887
|
+
inspector_module_url: Browser module containing the Inspector control.
|
|
888
|
+
|
|
889
|
+
Returns:
|
|
890
|
+
None.
|
|
891
|
+
|
|
892
|
+
Raises:
|
|
893
|
+
ImportError: If IPython is unavailable.
|
|
894
|
+
TypeError: If the controls value has an invalid type.
|
|
895
|
+
ValueError: If a control name is unknown or duplicated.
|
|
896
|
+
|
|
897
|
+
Example:
|
|
898
|
+
>>> chart.display(controls=False)
|
|
899
|
+
"""
|
|
900
|
+
try:
|
|
901
|
+
from IPython.display import display
|
|
902
|
+
except ImportError as error:
|
|
903
|
+
raise ImportError(
|
|
904
|
+
"Chart.display() requires IPython. Use chart.to_html() outside "
|
|
905
|
+
"a notebook."
|
|
906
|
+
) from error
|
|
907
|
+
|
|
908
|
+
display_chart = cast(Callable[[Any], None], display)
|
|
909
|
+
display_chart(
|
|
910
|
+
self.widget(
|
|
911
|
+
bundle_url=bundle_url,
|
|
912
|
+
embed_options=embed_options,
|
|
913
|
+
controls=controls,
|
|
914
|
+
controls_module_url=controls_module_url,
|
|
915
|
+
inspector_module_url=inspector_module_url,
|
|
916
|
+
)
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
def _repr_mimebundle_(
|
|
920
|
+
self,
|
|
921
|
+
include: object | None = None,
|
|
922
|
+
exclude: object | None = None,
|
|
923
|
+
) -> object:
|
|
924
|
+
"""Display the spec through the anywidget notebook renderer."""
|
|
925
|
+
del include, exclude
|
|
926
|
+
return self.widget()._repr_mimebundle_()
|
|
927
|
+
|
|
928
|
+
def __add__(self, other: TopLevelSpec | ImportedView) -> LayerChart:
|
|
929
|
+
return layer(self, other)
|
|
930
|
+
|
|
931
|
+
def __or__(self, other: TopLevelSpec | ImportedView) -> HConcatChart:
|
|
932
|
+
return hconcat(self, other)
|
|
933
|
+
|
|
934
|
+
def __and__(self, other: TopLevelSpec | ImportedView) -> VConcatChart:
|
|
935
|
+
return vconcat(self, other)
|
|
936
|
+
|
|
937
|
+
def _append_transform(self, transform: dict[str, Any]) -> Self:
|
|
938
|
+
raise NotImplementedError
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
class Chart( # type: ignore[misc] # Generated copy narrows SchemaBase.copy updates.
|
|
942
|
+
UnitPropertiesMixin, TopLevelSpec, ConfigMethodMixin, MarkMethodMixin, UnitSpec
|
|
943
|
+
):
|
|
944
|
+
"""An immutable-style builder backed by generated ``UnitSpec`` state."""
|
|
945
|
+
|
|
946
|
+
_schema_spec_cls = UnitSpec
|
|
947
|
+
|
|
948
|
+
def _copy(self, *, deep: bool = True, **kwargs: Any) -> Self:
|
|
949
|
+
"""Return a schema-backed copy while preserving the schema URL."""
|
|
950
|
+
copied = cast(Self, SchemaBase.copy(cast(Any, self), deep=deep, **kwargs))
|
|
951
|
+
copied._schema_url = self._schema_url
|
|
952
|
+
return copied
|
|
953
|
+
|
|
954
|
+
def to_dict(
|
|
955
|
+
self, *, include_schema: bool = True, validate: bool = True
|
|
956
|
+
) -> dict[str, Any]:
|
|
957
|
+
"""Serialize and optionally validate the complete chart specification."""
|
|
958
|
+
return self._serialize(
|
|
959
|
+
include_schema=include_schema,
|
|
960
|
+
validate=validate,
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
def _to_dict(
|
|
964
|
+
self,
|
|
965
|
+
*,
|
|
966
|
+
include_schema: bool,
|
|
967
|
+
validate: bool,
|
|
968
|
+
normalize_chart_data: Callable[[Any], Any],
|
|
969
|
+
) -> dict[str, Any]:
|
|
970
|
+
"""Serialize this unit spec with a configurable data policy."""
|
|
971
|
+
values = self._serialized_top_level_values(
|
|
972
|
+
normalize_chart_data=normalize_chart_data
|
|
973
|
+
)
|
|
974
|
+
spec = UnitSpec(**values).to_dict(validate=False)
|
|
975
|
+
return self._validated_root_spec(
|
|
976
|
+
spec,
|
|
977
|
+
include_schema=include_schema,
|
|
978
|
+
validate=validate,
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
def _with_mark(self, mark_type: str, **kwargs: Any) -> Chart:
|
|
982
|
+
if mark_type not in MARK_TYPES:
|
|
983
|
+
raise ValueError(f"Unsupported mark type: {mark_type}")
|
|
984
|
+
mark: str | dict[str, Any]
|
|
985
|
+
if kwargs:
|
|
986
|
+
mark = {"type": mark_type, **kwargs}
|
|
987
|
+
else:
|
|
988
|
+
mark = mark_type
|
|
989
|
+
return self._copy(deep=False, mark=mark)
|
|
990
|
+
|
|
991
|
+
def _append_transform(self, transform: dict[str, Any]) -> Self:
|
|
992
|
+
return self._appended_transform(transform)
|
|
993
|
+
|
|
994
|
+
def _configure(
|
|
995
|
+
self,
|
|
996
|
+
value: SchemaBase | dict[str, Any] | None | object = Undefined,
|
|
997
|
+
/,
|
|
998
|
+
**kwargs: Any,
|
|
999
|
+
) -> Self:
|
|
1000
|
+
return self._configured(value, **kwargs)
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
class _CompositionSpec(TopLevelSpec, ConfigMethodMixin, ResolutionMethodMixin):
|
|
1004
|
+
_schema_spec_cls: ClassVar[type]
|
|
1005
|
+
_children_key: ClassVar[str]
|
|
1006
|
+
_kwds: dict[str, Any]
|
|
1007
|
+
_schema_url: str
|
|
1008
|
+
|
|
1009
|
+
def _copy(self, *, deep: bool = True, **kwargs: Any) -> Self:
|
|
1010
|
+
"""Return a schema-backed copy while preserving the schema URL."""
|
|
1011
|
+
copied = cast(Self, SchemaBase.copy(cast(Any, self), deep=deep, **kwargs))
|
|
1012
|
+
copied._schema_url = self._schema_url
|
|
1013
|
+
return copied
|
|
1014
|
+
|
|
1015
|
+
def to_dict(
|
|
1016
|
+
self, *, include_schema: bool = True, validate: bool = True
|
|
1017
|
+
) -> dict[str, Any]:
|
|
1018
|
+
return self._serialize(
|
|
1019
|
+
include_schema=include_schema,
|
|
1020
|
+
validate=validate,
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
def _to_dict(
|
|
1024
|
+
self,
|
|
1025
|
+
*,
|
|
1026
|
+
include_schema: bool,
|
|
1027
|
+
validate: bool,
|
|
1028
|
+
normalize_chart_data: Callable[[Any], Any],
|
|
1029
|
+
) -> dict[str, Any]:
|
|
1030
|
+
"""Serialize this composition with one shared data policy."""
|
|
1031
|
+
values = self._serialized_top_level_values(
|
|
1032
|
+
normalize_chart_data=normalize_chart_data
|
|
1033
|
+
)
|
|
1034
|
+
children = values.get(self._children_key, Undefined)
|
|
1035
|
+
if children is not Undefined:
|
|
1036
|
+
values[self._children_key] = [
|
|
1037
|
+
(
|
|
1038
|
+
child._to_dict(
|
|
1039
|
+
include_schema=False,
|
|
1040
|
+
validate=False,
|
|
1041
|
+
normalize_chart_data=normalize_chart_data,
|
|
1042
|
+
)
|
|
1043
|
+
if isinstance(child, TopLevelSpec)
|
|
1044
|
+
else child.to_dict(include_schema=False, validate=False)
|
|
1045
|
+
)
|
|
1046
|
+
for child in children
|
|
1047
|
+
]
|
|
1048
|
+
spec = self._schema_spec_cls(**values).to_dict(validate=False)
|
|
1049
|
+
return self._validated_root_spec(
|
|
1050
|
+
spec,
|
|
1051
|
+
include_schema=include_schema,
|
|
1052
|
+
validate=validate,
|
|
1053
|
+
)
|
|
1054
|
+
|
|
1055
|
+
def _append_transform(self, transform: dict[str, Any]) -> Self:
|
|
1056
|
+
return self._appended_transform(transform)
|
|
1057
|
+
|
|
1058
|
+
def _configure(
|
|
1059
|
+
self,
|
|
1060
|
+
value: SchemaBase | dict[str, Any] | None | object = Undefined,
|
|
1061
|
+
/,
|
|
1062
|
+
**kwargs: Any,
|
|
1063
|
+
) -> Self:
|
|
1064
|
+
return self._configured(value, **kwargs)
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
class LayerChart( # type: ignore[misc] # Generated copy narrows SchemaBase.copy updates.
|
|
1068
|
+
LayerPropertiesMixin, _CompositionSpec, LayerSpec
|
|
1069
|
+
):
|
|
1070
|
+
"""A layered GenomeSpy specification."""
|
|
1071
|
+
|
|
1072
|
+
_schema_spec_cls = LayerSpec
|
|
1073
|
+
_children_key = "layer"
|
|
1074
|
+
|
|
1075
|
+
def __add__(self, other: TopLevelSpec | ImportedView) -> LayerChart:
|
|
1076
|
+
current = self._kwds.get("layer", Undefined)
|
|
1077
|
+
merged = [] if current is Undefined else list(current)
|
|
1078
|
+
merged.append(other)
|
|
1079
|
+
return self._copy(deep=False, layer=merged)
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
class HConcatChart( # type: ignore[misc] # Generated copy narrows SchemaBase.copy updates.
|
|
1083
|
+
HConcatPropertiesMixin, _CompositionSpec, HConcatSpec
|
|
1084
|
+
):
|
|
1085
|
+
"""A horizontally concatenated GenomeSpy specification."""
|
|
1086
|
+
|
|
1087
|
+
_schema_spec_cls = HConcatSpec
|
|
1088
|
+
_children_key = "hconcat"
|
|
1089
|
+
|
|
1090
|
+
def __or__(self, other: _SerializableView) -> HConcatChart:
|
|
1091
|
+
current = self._kwds.get("hconcat", Undefined)
|
|
1092
|
+
merged = [] if current is Undefined else list(current)
|
|
1093
|
+
merged.append(other)
|
|
1094
|
+
return self._copy(deep=False, hconcat=merged)
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
class VConcatChart( # type: ignore[misc] # Generated copy narrows SchemaBase.copy updates.
|
|
1098
|
+
VConcatPropertiesMixin, _CompositionSpec, VConcatSpec
|
|
1099
|
+
):
|
|
1100
|
+
"""A vertically concatenated GenomeSpy specification."""
|
|
1101
|
+
|
|
1102
|
+
_schema_spec_cls = VConcatSpec
|
|
1103
|
+
_children_key = "vconcat"
|
|
1104
|
+
|
|
1105
|
+
def __and__(self, other: _SerializableView) -> VConcatChart:
|
|
1106
|
+
current = self._kwds.get("vconcat", Undefined)
|
|
1107
|
+
merged = [] if current is Undefined else list(current)
|
|
1108
|
+
merged.append(other)
|
|
1109
|
+
return self._copy(deep=False, vconcat=merged)
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
class ConcatChart( # type: ignore[misc] # Generated copy narrows SchemaBase.copy updates.
|
|
1113
|
+
ConcatPropertiesMixin, _CompositionSpec, ConcatSpec
|
|
1114
|
+
):
|
|
1115
|
+
"""A grid-concatenated GenomeSpy specification."""
|
|
1116
|
+
|
|
1117
|
+
_schema_spec_cls = ConcatSpec
|
|
1118
|
+
_children_key = "concat"
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
class MultiscaleChart( # type: ignore[misc] # Generated copy narrows SchemaBase.copy updates.
|
|
1122
|
+
MultiscalePropertiesMixin, _CompositionSpec, MultiscaleSpec
|
|
1123
|
+
):
|
|
1124
|
+
"""A semantic-zoom composition backed by generated schema state."""
|
|
1125
|
+
|
|
1126
|
+
_schema_spec_cls = MultiscaleSpec
|
|
1127
|
+
_children_key = "multiscale"
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
class ImportedView(ImportedViewConstructorMixin, ImportSpec):
|
|
1131
|
+
"""A child-view import that can participate in chart composition."""
|
|
1132
|
+
|
|
1133
|
+
def _initialize_import(self, *, properties: dict[str, Any]) -> None:
|
|
1134
|
+
"""Initialize generated schema state for an imported child view."""
|
|
1135
|
+
ImportSpec.__init__(self, **properties)
|
|
1136
|
+
|
|
1137
|
+
def to_dict(
|
|
1138
|
+
self, *, include_schema: bool = False, validate: bool = True
|
|
1139
|
+
) -> dict[str, Any]:
|
|
1140
|
+
"""Serialize the imported child view without a root schema URL.
|
|
1141
|
+
|
|
1142
|
+
Args:
|
|
1143
|
+
include_schema: Accepted for composition compatibility; imports
|
|
1144
|
+
never emit a root ``$schema`` property.
|
|
1145
|
+
validate: Validate against the generated import schema.
|
|
1146
|
+
|
|
1147
|
+
Returns:
|
|
1148
|
+
The JSON-compatible imported-view specification.
|
|
1149
|
+
|
|
1150
|
+
Raises:
|
|
1151
|
+
SchemaValidationError: If validation fails.
|
|
1152
|
+
|
|
1153
|
+
Example:
|
|
1154
|
+
>>> ImportedView(import_={"template": "track"}).to_dict()
|
|
1155
|
+
{'import': {'template': 'track'}}
|
|
1156
|
+
"""
|
|
1157
|
+
del include_schema
|
|
1158
|
+
return ImportSpec(**self._kwds).to_dict(validate=validate)
|
|
1159
|
+
|
|
1160
|
+
def __add__(self, other: _SerializableView) -> LayerChart:
|
|
1161
|
+
"""Layer this imported view with another view."""
|
|
1162
|
+
return LayerChart(layer=cast(Any, [self, other]))
|
|
1163
|
+
|
|
1164
|
+
def __and__(self, other: TopLevelSpec | ImportedView) -> VConcatChart:
|
|
1165
|
+
"""Vertically concatenate this imported view with another view."""
|
|
1166
|
+
return VConcatChart(vconcat=cast(Any, [self, other]))
|
|
1167
|
+
|
|
1168
|
+
def __or__(self, other: TopLevelSpec | ImportedView) -> HConcatChart:
|
|
1169
|
+
"""Horizontally concatenate this imported view with another view."""
|
|
1170
|
+
return HConcatChart(hconcat=cast(Any, [self, other]))
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def _view_from_dict(
|
|
1174
|
+
values: dict[str, Any],
|
|
1175
|
+
*,
|
|
1176
|
+
schema_url: str,
|
|
1177
|
+
allow_import: bool,
|
|
1178
|
+
) -> TopLevelSpec | ImportedView:
|
|
1179
|
+
if allow_import and "import" in values:
|
|
1180
|
+
import_definition = values.pop("import")
|
|
1181
|
+
return ImportedView(import_=import_definition, **values)
|
|
1182
|
+
|
|
1183
|
+
for structural_key in (
|
|
1184
|
+
"mark",
|
|
1185
|
+
"layer",
|
|
1186
|
+
"multiscale",
|
|
1187
|
+
"vconcat",
|
|
1188
|
+
"hconcat",
|
|
1189
|
+
"concat",
|
|
1190
|
+
):
|
|
1191
|
+
if structural_key not in values:
|
|
1192
|
+
continue
|
|
1193
|
+
if structural_key == "mark":
|
|
1194
|
+
return Chart(schema_url=schema_url, **values)
|
|
1195
|
+
|
|
1196
|
+
raw_children = values.pop(structural_key)
|
|
1197
|
+
if not isinstance(raw_children, list):
|
|
1198
|
+
raise TypeError(f"{structural_key} must be a list of view specifications.")
|
|
1199
|
+
children: list[TopLevelSpec | ImportedView] = []
|
|
1200
|
+
for child in raw_children:
|
|
1201
|
+
if not isinstance(child, Mapping):
|
|
1202
|
+
raise TypeError(
|
|
1203
|
+
f"{structural_key} children must be mappings, got {type(child)!r}"
|
|
1204
|
+
)
|
|
1205
|
+
children.append(
|
|
1206
|
+
_view_from_dict(
|
|
1207
|
+
deepcopy(dict(child)),
|
|
1208
|
+
schema_url=schema_url,
|
|
1209
|
+
allow_import=True,
|
|
1210
|
+
)
|
|
1211
|
+
)
|
|
1212
|
+
if structural_key == "layer":
|
|
1213
|
+
return LayerChart(
|
|
1214
|
+
layer=cast(Any, children), schema_url=schema_url, **values
|
|
1215
|
+
)
|
|
1216
|
+
if structural_key == "multiscale":
|
|
1217
|
+
return MultiscaleChart(
|
|
1218
|
+
multiscale=cast(Any, children), schema_url=schema_url, **values
|
|
1219
|
+
)
|
|
1220
|
+
if structural_key == "vconcat":
|
|
1221
|
+
return VConcatChart(
|
|
1222
|
+
vconcat=cast(Any, children), schema_url=schema_url, **values
|
|
1223
|
+
)
|
|
1224
|
+
if structural_key == "hconcat":
|
|
1225
|
+
return HConcatChart(
|
|
1226
|
+
hconcat=cast(Any, children), schema_url=schema_url, **values
|
|
1227
|
+
)
|
|
1228
|
+
return ConcatChart(concat=cast(Any, children), schema_url=schema_url, **values)
|
|
1229
|
+
|
|
1230
|
+
if "import" in values:
|
|
1231
|
+
raise ValueError("An imported view must be nested inside a composition.")
|
|
1232
|
+
raise ValueError("GenomeSpy specification has no supported structural root.")
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
concat = _concat
|
|
1236
|
+
hconcat = _hconcat
|
|
1237
|
+
layer = _layer
|
|
1238
|
+
multiscale = _multiscale
|
|
1239
|
+
import_view = _import_view
|
|
1240
|
+
vconcat = _vconcat
|