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/schemapi.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Runtime primitives for generated GenomeSpy schema wrappers.
|
|
2
|
+
|
|
3
|
+
Unlike Altair, this small GenomeSpy-specific runtime is maintained directly in
|
|
4
|
+
the installable package. Generation tooling imports it from generated classes
|
|
5
|
+
but does not maintain a duplicate source copy.
|
|
6
|
+
|
|
7
|
+
Portions are adapted from Vega-Altair's schema runtime:
|
|
8
|
+
https://github.com/vega/altair/blob/main/altair/utils/schemapi.py
|
|
9
|
+
Copyright (c) 2015-2025, Vega-Altair Developers. BSD-3-Clause license; see
|
|
10
|
+
``LICENSES/ALTAIR-BSD-3-Clause.txt``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from copy import deepcopy
|
|
16
|
+
import json
|
|
17
|
+
from typing import Any, ClassVar, Self
|
|
18
|
+
|
|
19
|
+
from jsonschema import ValidationError
|
|
20
|
+
from jsonschema.validators import validator_for
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SchemaValidationError(ValueError):
|
|
24
|
+
"""Report a generated-schema validation failure.
|
|
25
|
+
|
|
26
|
+
The error retains the original ``jsonschema`` failure and adds the wrapper
|
|
27
|
+
class and failing JSON path to the message.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
schema_class: Wrapper class whose schema rejected the instance.
|
|
31
|
+
error: Original JSON Schema validation error.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A contextual validation exception.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
No exceptions are raised during initialization.
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
``SchemaValidationError(UnitSpec, error)``
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, schema_class: type[SchemaBase], error: ValidationError) -> None:
|
|
44
|
+
self.schema_class = schema_class
|
|
45
|
+
self.original = error
|
|
46
|
+
path = ".".join(str(part) for part in error.absolute_path) or "<root>"
|
|
47
|
+
super().__init__(f"Invalid {schema_class.__name__} at {path}: {error.message}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class UndefinedType:
|
|
51
|
+
"""Sentinel for properties omitted from a serialized spec."""
|
|
52
|
+
|
|
53
|
+
def __repr__(self) -> str:
|
|
54
|
+
return "Undefined"
|
|
55
|
+
|
|
56
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> UndefinedType:
|
|
57
|
+
"""Preserve sentinel identity when schema state is deeply copied."""
|
|
58
|
+
del memo
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
Undefined = UndefinedType()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SchemaBase:
|
|
66
|
+
"""Small base class for future generated GenomeSpy schema wrappers."""
|
|
67
|
+
|
|
68
|
+
_schema: ClassVar[dict[str, Any]] = {}
|
|
69
|
+
_rootschema: ClassVar[dict[str, Any]] = {}
|
|
70
|
+
|
|
71
|
+
def __init__(self, **kwds: Any) -> None:
|
|
72
|
+
self._kwds = kwds
|
|
73
|
+
|
|
74
|
+
def __getattr__(self, name: str) -> Any:
|
|
75
|
+
"""Expose stored schema properties as attributes."""
|
|
76
|
+
if name == "_kwds":
|
|
77
|
+
raise AttributeError(name)
|
|
78
|
+
try:
|
|
79
|
+
return self._kwds[name]
|
|
80
|
+
except KeyError:
|
|
81
|
+
raise AttributeError(name) from None
|
|
82
|
+
|
|
83
|
+
def copy(self, *, deep: bool = True, **kwds: Any) -> Self:
|
|
84
|
+
"""Return a copy with optional keyword updates."""
|
|
85
|
+
values = deepcopy(self._kwds) if deep else dict(self._kwds)
|
|
86
|
+
merged = {**values, **kwds}
|
|
87
|
+
return self.__class__(**merged)
|
|
88
|
+
|
|
89
|
+
def _with_property(
|
|
90
|
+
self, name: str, value: Any = Undefined, /, **kwargs: Any
|
|
91
|
+
) -> Self:
|
|
92
|
+
"""Return a shallow copy with one schema property updated."""
|
|
93
|
+
if kwargs:
|
|
94
|
+
if value is Undefined:
|
|
95
|
+
merged_value: Any = dict(kwargs)
|
|
96
|
+
elif value is None:
|
|
97
|
+
raise TypeError(f"Cannot merge keyword properties into null {name!r}.")
|
|
98
|
+
elif isinstance(value, SchemaBase):
|
|
99
|
+
merged_value = value.to_dict(validate=False)
|
|
100
|
+
merged_value.update(kwargs)
|
|
101
|
+
elif isinstance(value, dict):
|
|
102
|
+
merged_value = dict(value)
|
|
103
|
+
merged_value.update(kwargs)
|
|
104
|
+
else:
|
|
105
|
+
raise TypeError(f"Unsupported nested {name!r} value: {type(value)!r}")
|
|
106
|
+
return self.copy(deep=False, **{name: merged_value})
|
|
107
|
+
return self.copy(deep=False, **{name: value})
|
|
108
|
+
|
|
109
|
+
def to_dict(self, *, validate: bool = True) -> dict[str, Any]:
|
|
110
|
+
"""Serialize this schema wrapper to a JSON-compatible dictionary."""
|
|
111
|
+
result = {
|
|
112
|
+
key: _todict(value)
|
|
113
|
+
for key, value in self._kwds.items()
|
|
114
|
+
if value is not Undefined
|
|
115
|
+
}
|
|
116
|
+
if validate:
|
|
117
|
+
try:
|
|
118
|
+
self.validate(result)
|
|
119
|
+
except ValidationError as error:
|
|
120
|
+
raise SchemaValidationError(type(self), error) from None
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
def to_json(self, *, validate: bool = True) -> str:
|
|
124
|
+
"""Serialize this schema wrapper to formatted JSON."""
|
|
125
|
+
return json.dumps(self.to_dict(validate=validate), indent=2)
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def validate(cls, instance: dict[str, Any]) -> None:
|
|
129
|
+
"""Validate an instance against this wrapper's schema."""
|
|
130
|
+
rootschema = cls._rootschema or cls._schema
|
|
131
|
+
validator_class = validator_for(rootschema)
|
|
132
|
+
validator_class.check_schema(rootschema)
|
|
133
|
+
validator = validator_class(rootschema).evolve(schema=cls._schema)
|
|
134
|
+
validator.validate(instance)
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def resolve_references(cls) -> dict[str, Any]:
|
|
138
|
+
"""Return this class schema with referenced properties merged in."""
|
|
139
|
+
rootschema = cls._rootschema or cls._schema
|
|
140
|
+
return _resolve_schema_references(cls._schema, rootschema)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _todict(value: Any) -> Any:
|
|
144
|
+
return normalize_schema_value(value, validate=True)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def normalize_schema_value(value: Any, *, validate: bool = False) -> Any:
|
|
148
|
+
"""Recursively convert schema wrappers into plain Python values."""
|
|
149
|
+
if isinstance(value, str):
|
|
150
|
+
return str(value)
|
|
151
|
+
if isinstance(value, SchemaBase):
|
|
152
|
+
return value.to_dict(validate=validate)
|
|
153
|
+
if isinstance(value, list | tuple):
|
|
154
|
+
return [normalize_schema_value(item, validate=validate) for item in value]
|
|
155
|
+
if isinstance(value, dict):
|
|
156
|
+
return {
|
|
157
|
+
key: normalize_schema_value(item, validate=validate)
|
|
158
|
+
for key, item in value.items()
|
|
159
|
+
if item is not Undefined
|
|
160
|
+
}
|
|
161
|
+
return value
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def normalize_mapping_value(
|
|
165
|
+
value: SchemaBase | dict[str, Any],
|
|
166
|
+
*,
|
|
167
|
+
key: str,
|
|
168
|
+
validate: bool = False,
|
|
169
|
+
) -> dict[str, Any]:
|
|
170
|
+
"""Convert a schema wrapper or mapping into a plain mapping."""
|
|
171
|
+
normalized = normalize_schema_value(value, validate=validate)
|
|
172
|
+
if not isinstance(normalized, dict):
|
|
173
|
+
raise TypeError(f"Unsupported nested {key!r} value: {type(value)!r}")
|
|
174
|
+
return normalized
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def merge_mapping_value(
|
|
178
|
+
current: Any,
|
|
179
|
+
key: str,
|
|
180
|
+
value: Any = Undefined,
|
|
181
|
+
/,
|
|
182
|
+
**kwargs: Any,
|
|
183
|
+
) -> Any:
|
|
184
|
+
"""Merge a nested schema object using builder-style semantics."""
|
|
185
|
+
if value is Undefined:
|
|
186
|
+
if current is Undefined or current is None:
|
|
187
|
+
return dict(kwargs)
|
|
188
|
+
if isinstance(current, SchemaBase | dict):
|
|
189
|
+
merged = normalize_mapping_value(current, key=key, validate=False)
|
|
190
|
+
merged.update(kwargs)
|
|
191
|
+
return merged
|
|
192
|
+
raise TypeError(f"Cannot merge {key!r} into non-mapping value.")
|
|
193
|
+
|
|
194
|
+
if value is None:
|
|
195
|
+
if kwargs:
|
|
196
|
+
raise TypeError(f"Cannot merge keyword properties into null {key!r}.")
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
merged = normalize_mapping_value(value, key=key, validate=False)
|
|
200
|
+
if kwargs:
|
|
201
|
+
merged.update(kwargs)
|
|
202
|
+
return merged
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _ref_name(schema: dict[str, Any]) -> str | None:
|
|
206
|
+
ref = schema.get("$ref")
|
|
207
|
+
if not isinstance(ref, str):
|
|
208
|
+
return None
|
|
209
|
+
return ref.split("/")[-1]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _resolve_schema_references(
|
|
213
|
+
schema: dict[str, Any],
|
|
214
|
+
rootschema: dict[str, Any],
|
|
215
|
+
*,
|
|
216
|
+
seen: frozenset[str] = frozenset(),
|
|
217
|
+
) -> dict[str, Any]:
|
|
218
|
+
ref_name = _ref_name(schema)
|
|
219
|
+
if ref_name is not None:
|
|
220
|
+
definitions = rootschema.get("definitions", {})
|
|
221
|
+
if not isinstance(definitions, dict) or ref_name in seen:
|
|
222
|
+
return {}
|
|
223
|
+
target = definitions.get(ref_name)
|
|
224
|
+
if not isinstance(target, dict):
|
|
225
|
+
return {}
|
|
226
|
+
return _resolve_schema_references(
|
|
227
|
+
target,
|
|
228
|
+
rootschema,
|
|
229
|
+
seen=seen | {ref_name},
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
resolved = dict(schema)
|
|
233
|
+
properties: dict[str, Any] = {}
|
|
234
|
+
own_properties = schema.get("properties", {})
|
|
235
|
+
if isinstance(own_properties, dict):
|
|
236
|
+
properties.update(own_properties)
|
|
237
|
+
for key in ("anyOf", "oneOf", "allOf"):
|
|
238
|
+
variants = schema.get(key, [])
|
|
239
|
+
if not isinstance(variants, list):
|
|
240
|
+
continue
|
|
241
|
+
for variant in variants:
|
|
242
|
+
if not isinstance(variant, dict):
|
|
243
|
+
continue
|
|
244
|
+
variant_properties = _resolve_schema_references(
|
|
245
|
+
variant,
|
|
246
|
+
rootschema,
|
|
247
|
+
seen=seen,
|
|
248
|
+
).get("properties", {})
|
|
249
|
+
if isinstance(variant_properties, dict):
|
|
250
|
+
properties.update(variant_properties)
|
|
251
|
+
if properties:
|
|
252
|
+
resolved["properties"] = properties
|
|
253
|
+
return resolved
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
__all__ = [
|
|
257
|
+
"merge_mapping_value",
|
|
258
|
+
"normalize_mapping_value",
|
|
259
|
+
"normalize_schema_value",
|
|
260
|
+
"SchemaBase",
|
|
261
|
+
"SchemaValidationError",
|
|
262
|
+
"Undefined",
|
|
263
|
+
"UndefinedType",
|
|
264
|
+
]
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
export function datasetApi(api, descriptor) {
|
|
2
|
+
if (!descriptor.scoped) {
|
|
3
|
+
return api.datasets;
|
|
4
|
+
}
|
|
5
|
+
if (!descriptor.owner) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
`Dataset "${descriptor.name}" belongs to an unnamed nested view.`
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
const owner = api.views?.get({ scope: [], view: descriptor.owner });
|
|
11
|
+
if (!owner) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`Could not find the view that declares dataset "${descriptor.name}".`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return owner.datasets;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function setLoading(el, loading) {
|
|
20
|
+
if (el.style) {
|
|
21
|
+
el.style.visibility = loading ? "hidden" : "";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function mountControls({
|
|
26
|
+
container,
|
|
27
|
+
api,
|
|
28
|
+
names,
|
|
29
|
+
definitions,
|
|
30
|
+
moduleUrls,
|
|
31
|
+
}) {
|
|
32
|
+
if (!names.length) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const controlsModule = await import(moduleUrls.core);
|
|
37
|
+
const modules = { core: controlsModule };
|
|
38
|
+
const controls = [];
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
const definition = definitions[name];
|
|
41
|
+
if (!definition) {
|
|
42
|
+
throw new Error(`Control ${name} has no definition.`);
|
|
43
|
+
}
|
|
44
|
+
if (!modules[definition.module]) {
|
|
45
|
+
const moduleUrl = moduleUrls[definition.module];
|
|
46
|
+
if (!moduleUrl) {
|
|
47
|
+
throw new Error(`Control module ${definition.module} has no URL.`);
|
|
48
|
+
}
|
|
49
|
+
modules[definition.module] = await import(moduleUrl);
|
|
50
|
+
}
|
|
51
|
+
const factory = modules[definition.module][definition.export];
|
|
52
|
+
if (typeof factory !== "function") {
|
|
53
|
+
throw new Error(`Control ${name} is unavailable.`);
|
|
54
|
+
}
|
|
55
|
+
controls.push(factory());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (typeof controlsModule.attachControls !== "function") {
|
|
59
|
+
throw new Error("GenomeSpy attachControls export was not found.");
|
|
60
|
+
}
|
|
61
|
+
return controlsModule.attachControls(container, api, { controls });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function renderChart({ model, el, signal }) {
|
|
65
|
+
if (!model.get("spec")) {
|
|
66
|
+
el.textContent = "No GenomeSpy specification provided.";
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let api = null;
|
|
71
|
+
let mountedControls = null;
|
|
72
|
+
let parameterSubscriptions = [];
|
|
73
|
+
let renderRevision = 0;
|
|
74
|
+
let syncingParameterValues = false;
|
|
75
|
+
const datasetListeners = [];
|
|
76
|
+
const activeErrors = new Map();
|
|
77
|
+
|
|
78
|
+
const publishErrors = () => {
|
|
79
|
+
model.set("error", [...activeErrors.values()].join("\n"));
|
|
80
|
+
model.save_changes();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const setError = (error, source = "runtime") => {
|
|
84
|
+
activeErrors.delete(source);
|
|
85
|
+
activeErrors.set(source, String(error));
|
|
86
|
+
publishErrors();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const clearError = (source) => {
|
|
90
|
+
if (activeErrors.delete(source)) {
|
|
91
|
+
publishErrors();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const disposeCurrent = ({ reportErrors = true } = {}) => {
|
|
96
|
+
const controls = mountedControls;
|
|
97
|
+
const currentApi = api;
|
|
98
|
+
mountedControls = null;
|
|
99
|
+
api = null;
|
|
100
|
+
const errors = [];
|
|
101
|
+
try {
|
|
102
|
+
controls?.dispose?.();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
errors.push(error);
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
currentApi?.finalize?.();
|
|
108
|
+
} catch (error) {
|
|
109
|
+
errors.push(error);
|
|
110
|
+
}
|
|
111
|
+
if (errors.length && reportErrors) {
|
|
112
|
+
setError(errors.map(String).join("\n"), "cleanup");
|
|
113
|
+
} else if (!errors.length) {
|
|
114
|
+
clearError("cleanup");
|
|
115
|
+
} else {
|
|
116
|
+
console.error("GenomeSpy cleanup failed", ...errors);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const publishParameterValue = (name, value) => {
|
|
121
|
+
const values = { ...(model.get("parameter_values") || {}) };
|
|
122
|
+
values[name] = value;
|
|
123
|
+
syncingParameterValues = true;
|
|
124
|
+
model.set("parameter_values", values);
|
|
125
|
+
model.save_changes();
|
|
126
|
+
syncingParameterValues = false;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const clearInteractions = () => {
|
|
130
|
+
for (const unsubscribe of parameterSubscriptions) {
|
|
131
|
+
unsubscribe();
|
|
132
|
+
}
|
|
133
|
+
parameterSubscriptions = [];
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const attachInteractions = () => {
|
|
137
|
+
if (!api) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
clearInteractions();
|
|
141
|
+
|
|
142
|
+
for (const name of model.get("parameter_names") || []) {
|
|
143
|
+
try {
|
|
144
|
+
const parameter = api.getParam(name);
|
|
145
|
+
const values = model.get("parameter_values") || {};
|
|
146
|
+
if (Object.prototype.hasOwnProperty.call(values, name)) {
|
|
147
|
+
parameter.setValue(values[name]);
|
|
148
|
+
}
|
|
149
|
+
parameterSubscriptions.push(
|
|
150
|
+
parameter.subscribe((value) => publishParameterValue(name, value))
|
|
151
|
+
);
|
|
152
|
+
publishParameterValue(name, parameter.getValue());
|
|
153
|
+
} catch (error) {
|
|
154
|
+
setError(error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (model.get("enable_click_events")) {
|
|
159
|
+
const interactionApi = api;
|
|
160
|
+
const onClick = (event) => {
|
|
161
|
+
const datum = event?.datum;
|
|
162
|
+
model.set("clicked_datum", datum && typeof datum === "object" ? datum : {});
|
|
163
|
+
model.set("click_revision", (model.get("click_revision") || 0) + 1);
|
|
164
|
+
model.save_changes();
|
|
165
|
+
};
|
|
166
|
+
interactionApi.addEventListener("click", onClick);
|
|
167
|
+
parameterSubscriptions.push(() =>
|
|
168
|
+
interactionApi.removeEventListener("click", onClick)
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const onParameterValuesChange = () => {
|
|
174
|
+
if (syncingParameterValues || !api) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const values = model.get("parameter_values") || {};
|
|
178
|
+
for (const name of model.get("parameter_names") || []) {
|
|
179
|
+
if (!Object.prototype.hasOwnProperty.call(values, name)) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
api.getParam(name).setValue(values[name]);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
setError(error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const applyDataset = async (descriptor) => {
|
|
191
|
+
const errorSource = `dataset:${descriptor.revision_trait}`;
|
|
192
|
+
const revision = model.get(descriptor.revision_trait) || 0;
|
|
193
|
+
const currentApi = api;
|
|
194
|
+
const currentRender = renderRevision;
|
|
195
|
+
if (!currentApi || revision === 0) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const payload = model.get(descriptor.payload_trait);
|
|
200
|
+
const format = model.get(descriptor.format_trait);
|
|
201
|
+
try {
|
|
202
|
+
const datasets = datasetApi(currentApi, descriptor);
|
|
203
|
+
if (format === "arrow") {
|
|
204
|
+
await datasets.load(descriptor.name, payload, { type: "arrow" });
|
|
205
|
+
} else if (format === "records") {
|
|
206
|
+
datasets.set(descriptor.name, payload);
|
|
207
|
+
} else {
|
|
208
|
+
throw new Error(`Unsupported dataset format: ${String(format)}`);
|
|
209
|
+
}
|
|
210
|
+
if (
|
|
211
|
+
signal.aborted ||
|
|
212
|
+
api !== currentApi ||
|
|
213
|
+
renderRevision !== currentRender ||
|
|
214
|
+
model.get(descriptor.revision_trait) !== revision
|
|
215
|
+
) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
clearError(errorSource);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (
|
|
221
|
+
!signal.aborted &&
|
|
222
|
+
api === currentApi &&
|
|
223
|
+
renderRevision === currentRender &&
|
|
224
|
+
model.get(descriptor.revision_trait) === revision
|
|
225
|
+
) {
|
|
226
|
+
setError(error, errorSource);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const renderSpec = async () => {
|
|
232
|
+
const revision = ++renderRevision;
|
|
233
|
+
const moduleUrl = model.get("bundle_url");
|
|
234
|
+
const options = model.get("embed_options") || {};
|
|
235
|
+
const controlNames = model.get("controls") || [];
|
|
236
|
+
const datasets = model.get("dataset_manifest") || [];
|
|
237
|
+
const hasInitialData = datasets.some(
|
|
238
|
+
(descriptor) => (model.get(descriptor.revision_trait) || 0) > 0
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
disposeCurrent();
|
|
242
|
+
setLoading(el, hasInitialData);
|
|
243
|
+
el.replaceChildren();
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const module = await import(moduleUrl);
|
|
247
|
+
const embed = module.embed ?? module.default?.embed ?? module.default;
|
|
248
|
+
if (typeof embed !== "function") {
|
|
249
|
+
throw new Error("GenomeSpy embed export was not found.");
|
|
250
|
+
}
|
|
251
|
+
const nextApi = await embed(el, model.get("spec"), options);
|
|
252
|
+
if (revision !== renderRevision || signal.aborted) {
|
|
253
|
+
nextApi?.finalize?.();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
api = nextApi;
|
|
257
|
+
clearError("render");
|
|
258
|
+
try {
|
|
259
|
+
const nextControls = await mountControls({
|
|
260
|
+
container: el,
|
|
261
|
+
api: nextApi,
|
|
262
|
+
names: controlNames,
|
|
263
|
+
definitions: model.get("_control_definitions") || {},
|
|
264
|
+
moduleUrls: {
|
|
265
|
+
core: model.get("controls_module_url"),
|
|
266
|
+
inspector: model.get("inspector_module_url"),
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
if (revision !== renderRevision || signal.aborted || api !== nextApi) {
|
|
270
|
+
try {
|
|
271
|
+
nextControls?.dispose?.();
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.error("GenomeSpy controls cleanup failed", error);
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
mountedControls = nextControls;
|
|
278
|
+
clearError("controls");
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (revision === renderRevision && !signal.aborted && api === nextApi) {
|
|
281
|
+
setError(
|
|
282
|
+
`GenomeSpy controls failed to load: ${String(error)}`,
|
|
283
|
+
"controls"
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
attachInteractions();
|
|
288
|
+
await Promise.all(datasets.map((descriptor) => applyDataset(descriptor)));
|
|
289
|
+
if (revision === renderRevision && !signal.aborted) {
|
|
290
|
+
setLoading(el, false);
|
|
291
|
+
}
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (revision !== renderRevision || signal.aborted) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
setLoading(el, false);
|
|
297
|
+
setError(error, "render");
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const onSpecChange = () => void renderSpec();
|
|
303
|
+
|
|
304
|
+
model.on("change:spec", onSpecChange);
|
|
305
|
+
model.on("change:bundle_url", onSpecChange);
|
|
306
|
+
model.on("change:embed_options", onSpecChange);
|
|
307
|
+
model.on("change:controls", onSpecChange);
|
|
308
|
+
model.on("change:controls_module_url", onSpecChange);
|
|
309
|
+
model.on("change:inspector_module_url", onSpecChange);
|
|
310
|
+
model.on("change:parameter_values", onParameterValuesChange);
|
|
311
|
+
model.on("change:parameter_names", attachInteractions);
|
|
312
|
+
model.on("change:enable_click_events", attachInteractions);
|
|
313
|
+
for (const descriptor of model.get("dataset_manifest") || []) {
|
|
314
|
+
const listener = () => void applyDataset(descriptor);
|
|
315
|
+
const event = `change:${descriptor.payload_trait}`;
|
|
316
|
+
model.on(event, listener);
|
|
317
|
+
datasetListeners.push([event, listener]);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
signal.addEventListener("abort", () => {
|
|
321
|
+
renderRevision += 1;
|
|
322
|
+
model.off("change:spec", onSpecChange);
|
|
323
|
+
model.off("change:bundle_url", onSpecChange);
|
|
324
|
+
model.off("change:embed_options", onSpecChange);
|
|
325
|
+
model.off("change:controls", onSpecChange);
|
|
326
|
+
model.off("change:controls_module_url", onSpecChange);
|
|
327
|
+
model.off("change:inspector_module_url", onSpecChange);
|
|
328
|
+
model.off("change:parameter_values", onParameterValuesChange);
|
|
329
|
+
model.off("change:parameter_names", attachInteractions);
|
|
330
|
+
model.off("change:enable_click_events", attachInteractions);
|
|
331
|
+
for (const [event, listener] of datasetListeners) {
|
|
332
|
+
model.off(event, listener);
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
clearInteractions();
|
|
336
|
+
} finally {
|
|
337
|
+
disposeCurrent({ reportErrors: false });
|
|
338
|
+
setLoading(el, false);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await renderSpec();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export default { render: renderChart };
|