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