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/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