dynamic-systems-testing-framework 1.0.1__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.
- dstf/__init__.py +41 -0
- dstf/__main__.py +10 -0
- dstf/authoring/__init__.py +13 -0
- dstf/authoring/builtin/__init__.py +32 -0
- dstf/authoring/builtin/accept.py +205 -0
- dstf/authoring/builtin/edit_criteria.py +479 -0
- dstf/authoring/builtin/edit_family.py +228 -0
- dstf/authoring/builtin/edit_on_plot.py +1354 -0
- dstf/authoring/builtin/edit_tree.py +1399 -0
- dstf/authoring/builtin/enqueue.py +144 -0
- dstf/authoring/capabilities.py +463 -0
- dstf/authoring/errors.py +49 -0
- dstf/authoring/family_rows.py +526 -0
- dstf/authoring/forms.py +631 -0
- dstf/authoring/layout.py +104 -0
- dstf/authoring/live_rows.py +337 -0
- dstf/authoring/live_runs.py +385 -0
- dstf/authoring/needle.py +127 -0
- dstf/authoring/picker_rows.py +92 -0
- dstf/authoring/queue_rows.py +354 -0
- dstf/authoring/session.py +3016 -0
- dstf/authoring/shell/__init__.py +9 -0
- dstf/authoring/shell/actions.py +702 -0
- dstf/authoring/shell/app.py +53 -0
- dstf/authoring/shell/drawer_controls.py +190 -0
- dstf/authoring/shell/family_fold.py +468 -0
- dstf/authoring/shell/figure_hook.py +249 -0
- dstf/authoring/shell/grips.js +1000 -0
- dstf/authoring/shell/header.py +783 -0
- dstf/authoring/shell/live_view.py +93 -0
- dstf/authoring/shell/picker.py +267 -0
- dstf/authoring/shell/placing.py +122 -0
- dstf/authoring/shell/plot_card.py +1171 -0
- dstf/authoring/shell/queue_panel.py +433 -0
- dstf/authoring/shell/rail.py +593 -0
- dstf/authoring/shell/shell.py +528 -0
- dstf/authoring/shell/stats_card.py +501 -0
- dstf/authoring/shell/strip.py +204 -0
- dstf/authoring/shell/style.py +721 -0
- dstf/authoring/shell/suite.py +277 -0
- dstf/authoring/shell/suite_grid.js +414 -0
- dstf/authoring/shell/suite_grid.py +373 -0
- dstf/authoring/shell/tab_theme.py +110 -0
- dstf/authoring/shell/tabstate.py +146 -0
- dstf/authoring/shell/test_list.py +179 -0
- dstf/authoring/shell/test_page.py +1404 -0
- dstf/authoring/shell/tree_card.py +1171 -0
- dstf/authoring/shell/variables_drawer.py +589 -0
- dstf/authoring/shell/widgets.py +188 -0
- dstf/authoring/situation.py +287 -0
- dstf/authoring/spec_document.py +215 -0
- dstf/authoring/spec_preview.py +83 -0
- dstf/authoring/suite_rows.py +99 -0
- dstf/authoring/tree_move.py +516 -0
- dstf/authoring/tree_pointer.py +33 -0
- dstf/authoring/tree_text.py +61 -0
- dstf/authoring/typed_number.js +64 -0
- dstf/authoring/typed_number.py +76 -0
- dstf/authoring/unscorable.py +141 -0
- dstf/authoring/variable_rows.py +216 -0
- dstf/backends/__init__.py +283 -0
- dstf/backends/base.py +1117 -0
- dstf/backends/common/__init__.py +1 -0
- dstf/backends/common/json_driver.py +484 -0
- dstf/backends/common/live_mat.py +369 -0
- dstf/backends/common/mat_reader.py +632 -0
- dstf/backends/common/proc_kill.py +238 -0
- dstf/backends/common/proc_output.py +20 -0
- dstf/backends/common/result_files.py +109 -0
- dstf/backends/common/result_variables.py +329 -0
- dstf/backends/common/statistics.py +39 -0
- dstf/backends/cross_backend.py +193 -0
- dstf/backends/dymola/__init__.py +5 -0
- dstf/backends/dymola/backend.py +776 -0
- dstf/backends/dymola/interface_loader.py +241 -0
- dstf/backends/dymola/log_parser.py +396 -0
- dstf/backends/dymola/persistent_backend.py +949 -0
- dstf/backends/fmpy/__init__.py +11 -0
- dstf/backends/fmpy/backend.py +443 -0
- dstf/backends/julia/__init__.py +27 -0
- dstf/backends/julia/backend.py +156 -0
- dstf/backends/julia/persistent_backend.py +541 -0
- dstf/backends/julia/run_persistent.jl +167 -0
- dstf/backends/julia/run_test.jl +146 -0
- dstf/backends/openmodelica/__init__.py +21 -0
- dstf/backends/openmodelica/backend.py +368 -0
- dstf/backends/openmodelica/init_xml.py +76 -0
- dstf/backends/openmodelica/log_parser.py +119 -0
- dstf/backends/openmodelica/mos_generator.py +228 -0
- dstf/backends/openmodelica/persistent_backend.py +1045 -0
- dstf/backends/openmodelica/session_loader.py +63 -0
- dstf/backends/persistent.py +1811 -0
- dstf/backends/pool.py +602 -0
- dstf/backends/progress.py +395 -0
- dstf/backends/python/__init__.py +11 -0
- dstf/backends/python/backend.py +141 -0
- dstf/backends/python/run_test.py +141 -0
- dstf/backends/run_metadata.py +103 -0
- dstf/backends/types.py +310 -0
- dstf/cli/__init__.py +12 -0
- dstf/cli/author_cmd.py +61 -0
- dstf/cli/baseline_cmds.py +341 -0
- dstf/cli/common.py +231 -0
- dstf/cli/diag_cmds.py +350 -0
- dstf/cli/main.py +561 -0
- dstf/cli/review.py +398 -0
- dstf/cli/run_cmds.py +589 -0
- dstf/cli/spec_cmds.py +123 -0
- dstf/comparison/__init__.py +13 -0
- dstf/comparison/algorithms.py +2127 -0
- dstf/comparison/builtins.py +291 -0
- dstf/comparison/comparator.py +1189 -0
- dstf/comparison/geometry.py +164 -0
- dstf/comparison/handles.py +1194 -0
- dstf/comparison/metric_tree.py +332 -0
- dstf/comparison/mode_schema.py +176 -0
- dstf/comparison/modes.py +788 -0
- dstf/comparison/registry.py +230 -0
- dstf/comparison/seeds.py +281 -0
- dstf/comparison/sides.py +158 -0
- dstf/comparison/snap.py +62 -0
- dstf/comparison/strict_keys.py +246 -0
- dstf/comparison/summaries.py +354 -0
- dstf/comparison/tree_eval.py +868 -0
- dstf/comparison/tree_spec.py +678 -0
- dstf/comparison/types.py +194 -0
- dstf/comparison/validator.py +232 -0
- dstf/config.py +815 -0
- dstf/discovery/__init__.py +0 -0
- dstf/discovery/json_recognizer.py +702 -0
- dstf/discovery/library_state.py +248 -0
- dstf/discovery/mo_parser.py +794 -0
- dstf/discovery/patch_apply.py +308 -0
- dstf/discovery/patch_envelope.py +63 -0
- dstf/discovery/plots.py +318 -0
- dstf/discovery/recognizer.py +103 -0
- dstf/discovery/scan_cache.py +154 -0
- dstf/discovery/spec_parser.py +538 -0
- dstf/discovery/spec_shape.py +122 -0
- dstf/discovery/spec_update.py +493 -0
- dstf/discovery/test_registry.py +488 -0
- dstf/discovery/unit_tests.py +95 -0
- dstf/figures/__init__.py +14 -0
- dstf/figures/assemble.py +48 -0
- dstf/figures/decimate.py +150 -0
- dstf/figures/families.py +289 -0
- dstf/figures/palette.py +170 -0
- dstf/figures/plotly.py +1413 -0
- dstf/figures/view.py +233 -0
- dstf/pipeline.py +435 -0
- dstf/plugins.py +104 -0
- dstf/reporting/__init__.py +0 -0
- dstf/reporting/console_report.py +187 -0
- dstf/reporting/context_builder.py +979 -0
- dstf/reporting/dashboard_render.py +534 -0
- dstf/reporting/junit_report.py +192 -0
- dstf/reporting/overlay_loader.py +708 -0
- dstf/reporting/report_suite.py +1059 -0
- dstf/reporting/rows.py +458 -0
- dstf/reporting/schema_export.py +413 -0
- dstf/reporting/stats_rows.py +321 -0
- dstf/reporting/status.py +388 -0
- dstf/reporting/templates/axes.js +61 -0
- dstf/reporting/templates/dashboard.html +284 -0
- dstf/reporting/templates/dashboard.js +604 -0
- dstf/reporting/templates/filters.js +100 -0
- dstf/reporting/templates/interactive.html +420 -0
- dstf/reporting/templates/report.js +116 -0
- dstf/reporting/templates/vendor/plotly.min.js +8 -0
- dstf/reporting/theme.py +324 -0
- dstf/reporting/tree_rows.py +73 -0
- dstf/reporting/verdicts.py +124 -0
- dstf/storage/__init__.py +0 -0
- dstf/storage/accept.py +216 -0
- dstf/storage/baseline.py +43 -0
- dstf/storage/batch_manifest.py +204 -0
- dstf/storage/ref_index.py +295 -0
- dstf/storage/reference_store.py +661 -0
- dstf/storage/roles.py +576 -0
- dstf/storage/run_stats.py +85 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/METADATA +266 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/RECORD +185 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/WHEEL +4 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/entry_points.txt +2 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
// The Test page's tolerance grips (D147): the arrows just outside each side
|
|
2
|
+
// of the open criterion's tolerance are hidden at rest and shown while the
|
|
3
|
+
// pointer is near their checkpoint, event or peak, and through a drag. And
|
|
4
|
+
// the plot's pointer: every shape no editor moves is made inert.
|
|
5
|
+
//
|
|
6
|
+
// WHICH arrows show is decided by CLUSTERS. Each group — one checkpoint,
|
|
7
|
+
// event or peak: its ring and its arrows — has a hover area, a rectangle
|
|
8
|
+
// around its ring and its arrows' anchors, those of them that are ON the
|
|
9
|
+
// plot (`placed`): a group zoomed off it has none. Groups whose areas
|
|
10
|
+
// overlap show TOGETHER: they are one cluster, and with the pointer
|
|
11
|
+
// anywhere in a cluster's areas every arrow of every member is shown;
|
|
12
|
+
// outside every area, none. No two clusters' areas overlap, so the pointer
|
|
13
|
+
// is in at most one and there is nothing to choose between: no nearest
|
|
14
|
+
// group, nothing kept from where the pointer was. (ONE group was shown at
|
|
15
|
+
// a time before,
|
|
16
|
+
// chosen by the nearest anchor, then by the arrows' drawn tips too, then
|
|
17
|
+
// with an advantage for the shown one: at some spacings a facing arrow
|
|
18
|
+
// still could not be reached, the display flickered between two groups,
|
|
19
|
+
// and a press where a hidden arrow belonged moved the neighbour's ring.)
|
|
20
|
+
//
|
|
21
|
+
// WHERE a shown arrow is drawn is its figure's to say, but for two arrows of
|
|
22
|
+
// one cluster that would be drawn over each other: those are STAGGERED, each
|
|
23
|
+
// moved a step across its own axis (`stagger`), so that every arrow shown
|
|
24
|
+
// can be seen and pressed.
|
|
25
|
+
//
|
|
26
|
+
// The figure never changes with the hover: a drag names a shape by INDEX, and
|
|
27
|
+
// a figure changed mid-drag loses it. Hiding is one static rule of the shell's
|
|
28
|
+
// stylesheet (style.py) over the paths this module marks —
|
|
29
|
+
// .js-plotly-plot .shapelayer path[data-grip]:not([data-grip-on])
|
|
30
|
+
// — `display: none`, because Plotly writes inline `opacity: 1; pointer-events:
|
|
31
|
+
// all` on every editable shape: hidden by `visibility` or `opacity` a grip is
|
|
32
|
+
// still grabbed. Where there is no hover (a touch screen) the same stylesheet
|
|
33
|
+
// shows every marked grip, and nothing here is needed. And an arrow whose
|
|
34
|
+
// edge is OFF the plot (`data-grip-off`) is not displayed at all, by the
|
|
35
|
+
// same stylesheet: one rule for an edge that is not on the plot — no arrow.
|
|
36
|
+
//
|
|
37
|
+
// WHICH shape is a grip, of which group, and which shapes may be dragged at
|
|
38
|
+
// all is the SERVER's to say: it rides in the figure itself
|
|
39
|
+
// (`layout.meta.dstf`, figures/plotly.py `POINTER`), so it is read off the
|
|
40
|
+
// very figure whose paths are being marked — the two cannot be out of step —
|
|
41
|
+
// and nothing here knows a metric's id pattern. A shape the server does not
|
|
42
|
+
// name draggable gets `data-inert` (`pointer-events: none`, style.py): a
|
|
43
|
+
// press on a box, a window or a result line is the plot's own.
|
|
44
|
+
//
|
|
45
|
+
// One instance per plot card (`DstfGrips.install(id)` / `.destroy(id)`, the
|
|
46
|
+
// card's `ui.plotly` element id — plot_card.py): its own clusters, its own
|
|
47
|
+
// pointer, its own latch.
|
|
48
|
+
//
|
|
49
|
+
// And what the pointer SAYS to the server beside Plotly's own events (D149),
|
|
50
|
+
// since this module already holds the press, the travel and the ranges:
|
|
51
|
+
//
|
|
52
|
+
// * a CLICK on the plot (`dstf_plot_click`) — where, in the axes of the
|
|
53
|
+
// panel under the pointer, what those axes showed, and the modifier keys.
|
|
54
|
+
// Plotly's own click event carries neither a position nor a key through
|
|
55
|
+
// NiceGUI. The server decides what a click is: Ctrl+click adds a row, a
|
|
56
|
+
// plain one sets an armed field;
|
|
57
|
+
// * a handle HELD (`dstf_grip_move`) — where its release would put it, a
|
|
58
|
+
// few times a second, so the editor's fields follow the pointer. Plotly
|
|
59
|
+
// says nothing during a shape drag;
|
|
60
|
+
// * Esc while a field is armed (`dstf_disarm`).
|
|
61
|
+
(() => {
|
|
62
|
+
if (window.DstfGrips) return;
|
|
63
|
+
|
|
64
|
+
// How far a group's hover area reaches outside its ring and its arrows'
|
|
65
|
+
// anchors, px: an arrow is drawn 10 to 17 px outside its anchor
|
|
66
|
+
// (figures/plotly.py `_GRIP_PATHS`), whole inside the area.
|
|
67
|
+
const MARGIN = 25;
|
|
68
|
+
// How near a shown arrow, or the ring its group stands around, the plot's
|
|
69
|
+
// own hover label gives way, px.
|
|
70
|
+
const HUSH = 20;
|
|
71
|
+
// How far outside the plot area an anchor still counts as ON the plot,
|
|
72
|
+
// px. A zoom lands an edge within a fraction of a pixel of the border it
|
|
73
|
+
// was dragged to, on either side of it: that edge is the plot's own, and
|
|
74
|
+
// its arrow — all in the margin — is the one the un-clip is for.
|
|
75
|
+
const EDGE = 0.5;
|
|
76
|
+
// Two arrows COLLIDE when what is painted of them shares more than this
|
|
77
|
+
// on BOTH axes, px. An arrow is 12 px across and its 1 px outline stands
|
|
78
|
+
// half a pixel outside it: two value arrows 12 px apart touch corner to
|
|
79
|
+
// corner and share that one pixel of outline. At 0 that is a collision:
|
|
80
|
+
// of thirty checkpoints 12 px apart every value arrow was moved, left
|
|
81
|
+
// and right by turns, each ONTO its neighbour — 28 pairs colliding where
|
|
82
|
+
// none had.
|
|
83
|
+
const OVERLAP = 1;
|
|
84
|
+
// How far a colliding arrow is moved ACROSS its own axis, px a step, and
|
|
85
|
+
// the steps it may take, in the order it tries them. One step each way
|
|
86
|
+
// parts a pair by 18 px, 5 px clear of each other; a third on the same
|
|
87
|
+
// spot clears both at three. The stylesheet (style.py) turns the steps
|
|
88
|
+
// into pixels: the same 9.
|
|
89
|
+
const STEP = 9;
|
|
90
|
+
const LEVELS = [-1, 1, -3, 3];
|
|
91
|
+
// How far across its axis an arrow must already stand from those it
|
|
92
|
+
// collides with to be moved THAT way first, px: a pair on a slope parts
|
|
93
|
+
// the way it leans.
|
|
94
|
+
const LEAN = 0.5;
|
|
95
|
+
// How far the pointer must travel along an axis, press to release, for
|
|
96
|
+
// the release to move a shape on it, px — Plotly's own threshold (its
|
|
97
|
+
// `MINDRAG`): under it Plotly moves nothing on that axis.
|
|
98
|
+
const MOVED = 8;
|
|
99
|
+
// How far the pointer may travel, press to click, for the click to be a
|
|
100
|
+
// click, px: a Ctrl+drag (Plotly's zoom box under a held key) ends in a
|
|
101
|
+
// real click where it is let go.
|
|
102
|
+
const CLICKED = 4;
|
|
103
|
+
// The least time between two moves of a held handle sent to the server,
|
|
104
|
+
// ms: at most 15 a second, each a preview the server computes.
|
|
105
|
+
const PACE = 66;
|
|
106
|
+
// Every move and every release is numbered, rising, page-wide: the
|
|
107
|
+
// server drops a move numbered under a release it has heard (a preview
|
|
108
|
+
// must not land after what the release staged). From the clock, so a
|
|
109
|
+
// reloaded page's numbers are above the last page's.
|
|
110
|
+
let seq = Date.now();
|
|
111
|
+
|
|
112
|
+
const own = (map, name) =>
|
|
113
|
+
map && Object.prototype.hasOwnProperty.call(map, name) ? map[name] : null;
|
|
114
|
+
const dict = (v) => (v !== null && typeof v === "object" && !Array.isArray(v) ? v : null);
|
|
115
|
+
|
|
116
|
+
// What the server says of the figure `gd` holds — its pointer map — or
|
|
117
|
+
// null for a figure that says nothing: no `meta`, another's, a `dstf`
|
|
118
|
+
// that is no dict. Then nothing is marked and every shape is left as
|
|
119
|
+
// Plotly draws it. A map is read key by key, each with a default: a
|
|
120
|
+
// figure's `meta` crosses Plotly, where an empty dict need not survive,
|
|
121
|
+
// and a `draggable` iterated as it came threw in the redraw's observer
|
|
122
|
+
// when it was no list — after which nothing was marked at all.
|
|
123
|
+
const said = (gd) => {
|
|
124
|
+
const meta = dict(gd.layout && gd.layout.meta);
|
|
125
|
+
const info = meta && dict(own(meta, "dstf"));
|
|
126
|
+
if (!info) return null;
|
|
127
|
+
return {
|
|
128
|
+
grips: dict(info.grips) || {},
|
|
129
|
+
targets: dict(info.targets) || {},
|
|
130
|
+
draggable: new Set(Array.isArray(info.draggable) ? info.draggable : []),
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
// What a map says of the grip shape named `name` — a dict naming its
|
|
134
|
+
// group — or null; and of the ring named `name`, its group or null.
|
|
135
|
+
const gripOf = (info, name) => {
|
|
136
|
+
const grip = info ? dict(own(info.grips, name)) : null;
|
|
137
|
+
return grip && typeof grip.group === "string" ? grip : null;
|
|
138
|
+
};
|
|
139
|
+
const ringOf = (info, name) => {
|
|
140
|
+
const group = info ? own(info.targets, name) : null;
|
|
141
|
+
return typeof group === "string" ? group : null;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const instances = new Map();
|
|
145
|
+
|
|
146
|
+
// Where the pointer is on the PAGE — viewport px, or null once it has
|
|
147
|
+
// left the window — kept once, for every plot: a plot hears the moves
|
|
148
|
+
// over itself alone, and one wheeled under a pointer that had never
|
|
149
|
+
// moved over it had no pointer to test (`scrolled`).
|
|
150
|
+
let pointer = null;
|
|
151
|
+
const tracked = (e) => {
|
|
152
|
+
pointer = [e.clientX, e.clientY];
|
|
153
|
+
};
|
|
154
|
+
document.addEventListener("mousemove", tracked, true);
|
|
155
|
+
document.documentElement.addEventListener("mouseleave", () => {
|
|
156
|
+
pointer = null;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Esc while a field is armed disarms it. The armed field is the
|
|
160
|
+
// server's to know — its ⌖ says so in `aria-pressed`, which the server
|
|
161
|
+
// renders — so this only asks; Esc with nothing armed is everyone
|
|
162
|
+
// else's.
|
|
163
|
+
document.addEventListener("keydown", (e) => {
|
|
164
|
+
if (e.key === "Escape" && document.querySelector('.dstf-arm[aria-pressed="true"]')) {
|
|
165
|
+
emitEvent("dstf_disarm");
|
|
166
|
+
}
|
|
167
|
+
}, true);
|
|
168
|
+
|
|
169
|
+
function create(gd, id) {
|
|
170
|
+
const st = {
|
|
171
|
+
gd, clusters: [], on: null, latched: false, xy: null, ranges: {}, press: null,
|
|
172
|
+
travel: null, failed: false, frame: 0, held: null, ended: null,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// An arrow says what it sets — a tooltip, the SVG `title` child — in
|
|
176
|
+
// the server's words, and carries the axis it moves on for the
|
|
177
|
+
// stylesheet's cursor.
|
|
178
|
+
const SVG = "http://www.w3.org/2000/svg";
|
|
179
|
+
const tell = (el, grip) => {
|
|
180
|
+
el.setAttribute("data-grip-axis", grip.axis);
|
|
181
|
+
let title = el.querySelector("title");
|
|
182
|
+
if (!title) title = el.appendChild(document.createElementNS(SVG, "title"));
|
|
183
|
+
if (title.textContent !== grip.label) title.textContent = grip.label || "";
|
|
184
|
+
};
|
|
185
|
+
const untell = (el) => {
|
|
186
|
+
el.removeAttribute("data-grip-axis");
|
|
187
|
+
for (const title of el.querySelectorAll("title")) title.remove();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Whether the grip path `el` is of the shown cluster.
|
|
191
|
+
const shown = (el) => !!st.on && st.on.groups.has(el.getAttribute("data-grip"));
|
|
192
|
+
|
|
193
|
+
// Where the shape `s` — a ring or an arrow, sized in pixels about its
|
|
194
|
+
// anchor — is anchored, in pixels of the graph div, as the figure is
|
|
195
|
+
// drawn NOW (`_fullLayout`: the axes' ranges of this very redraw, a
|
|
196
|
+
// pan's every step too), or null where it is on no axis of the figure.
|
|
197
|
+
// An arrow anchored to the PAPER in y (an event's or a peak's window
|
|
198
|
+
// spans its panel) is drawn at a height of the paper, `y`, and stands
|
|
199
|
+
// for no value there: it is `paper`, and it reaches from the panel's
|
|
200
|
+
// `top` to its `bottom`.
|
|
201
|
+
//
|
|
202
|
+
// And whether that anchor is OFF the plot: outside the plot area by
|
|
203
|
+
// more than `allowed` px on either axis. For an arrow that is EDGE px
|
|
204
|
+
// (the default), on the axis it moves on (its edge is past the end of
|
|
205
|
+
// the axis) or the other (its checkpoint is); a ring is allowed its
|
|
206
|
+
// radius (`areas`).
|
|
207
|
+
const placed = (s, fl, allowed = EDGE) => {
|
|
208
|
+
const xa = fl["xaxis" + (s.xref || "x").slice(1)];
|
|
209
|
+
if (!xa) return null;
|
|
210
|
+
const paper = s.yref === "paper";
|
|
211
|
+
const ya = fl["yaxis" + (paper ? xa.anchor : s.yref || "y").slice(1)];
|
|
212
|
+
if (!ya) return null;
|
|
213
|
+
const within = (p, axis) => p >= -allowed && p <= axis._length + allowed;
|
|
214
|
+
const px = xa.d2p(s.xanchor);
|
|
215
|
+
const py = paper ? null : ya.d2p(s.yanchor);
|
|
216
|
+
const size = fl._size;
|
|
217
|
+
return {
|
|
218
|
+
x: xa._offset + px,
|
|
219
|
+
y: paper ? size.t + size.h * (1 - s.yanchor) : ya._offset + py,
|
|
220
|
+
paper,
|
|
221
|
+
top: ya._offset,
|
|
222
|
+
bottom: ya._offset + ya._length,
|
|
223
|
+
off: !(within(px, xa) && (paper || within(py, ya))),
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// The points of the path the figure sizes shape `s` by, in pixels
|
|
228
|
+
// about its anchor (positive y is UP) — an arrow's corners, a ring's
|
|
229
|
+
// arcs with their control points.
|
|
230
|
+
const points = (s) => {
|
|
231
|
+
const n = (typeof s.path === "string" && s.path.match(/-?\d*\.?\d+/g)) || [];
|
|
232
|
+
const out = [];
|
|
233
|
+
for (let k = 0; k + 1 < n.length; k += 2) out.push([+n[k], +n[k + 1]]);
|
|
234
|
+
return out;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// What is PAINTED of the arrow `s` anchored at `at` (`placed`), as a
|
|
238
|
+
// box in pixels of the graph div: its path about that anchor, and the
|
|
239
|
+
// half of its outline's width that stands outside it. Or null for a
|
|
240
|
+
// path that says no point.
|
|
241
|
+
const painted = (s, at) => {
|
|
242
|
+
const half = ((dict(s.line) || {}).width || 0) / 2;
|
|
243
|
+
const box = { l: Infinity, r: -Infinity, t: Infinity, b: -Infinity };
|
|
244
|
+
for (const [dx, dy] of points(s)) {
|
|
245
|
+
box.l = Math.min(box.l, at.x + dx - half);
|
|
246
|
+
box.r = Math.max(box.r, at.x + dx + half);
|
|
247
|
+
box.t = Math.min(box.t, at.y - dy - half);
|
|
248
|
+
box.b = Math.max(box.b, at.y - dy + half);
|
|
249
|
+
}
|
|
250
|
+
return box.l <= box.r ? box : null;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// How far the ring `s` reaches from its anchor, px: its radius.
|
|
254
|
+
const radius = (s) => Math.max(0, ...points(s).flat().map(Math.abs));
|
|
255
|
+
|
|
256
|
+
// Whether the LINE `s` — a bound an editor moves — is off the plot
|
|
257
|
+
// whole: both its ends past the same end of an axis, by more than EDGE
|
|
258
|
+
// px. Plotly clips what it draws of a line and not the wide helper
|
|
259
|
+
// that takes its drag: a scalar's bound zoomed 3 or 12 px below the
|
|
260
|
+
// plot was still what the pointer met in the bottom margin, with the
|
|
261
|
+
// `move` cursor, and a drag from there staged a tolerance.
|
|
262
|
+
const gone = (s, fl) => {
|
|
263
|
+
if (s.type !== "line" || !fl) return false;
|
|
264
|
+
const past = (key, v0, v1) => {
|
|
265
|
+
const ref = s[key + "ref"] || key;
|
|
266
|
+
const axis = /^[xy]\d*$/.test(ref) ? fl[key + "axis" + ref.slice(1)] : null;
|
|
267
|
+
if (!axis) return false;
|
|
268
|
+
const p = [axis.d2p(v0), axis.d2p(v1)];
|
|
269
|
+
return Math.max(...p) < -EDGE || Math.min(...p) > axis._length + EDGE;
|
|
270
|
+
};
|
|
271
|
+
return past("x", s.x0, s.x1) || past("y", s.y0, s.y1);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Plotly replaces every shape path on a react or a newPlot: the marks go
|
|
275
|
+
// with them, and are put back from what the figure says of its shapes.
|
|
276
|
+
// A shape is three elements of one `data-index` — its group, its path
|
|
277
|
+
// and, for a line, the wide helper that takes its drag: all three inert
|
|
278
|
+
// — a shape no editor moves, and a line one does that is off the plot
|
|
279
|
+
// (`gone`).
|
|
280
|
+
//
|
|
281
|
+
// The mark on a grip's path is also what takes Plotly's clip off it:
|
|
282
|
+
// an arrow stands 10 to 17 px OUTSIDE its edge, in the margin when that
|
|
283
|
+
// edge is the plot's, and the stylesheet's rules over `data-grip`
|
|
284
|
+
// (style.py) un-clip the path and its group. Nothing here touches a
|
|
285
|
+
// `clip-path`.
|
|
286
|
+
//
|
|
287
|
+
// An arrow whose anchor is OFF the plot (`placed`) is marked
|
|
288
|
+
// `data-grip-off`, and the stylesheet does not display it, shown group
|
|
289
|
+
// or not. Un-clipped like the rest, the arrows of a checkpoint zoomed
|
|
290
|
+
// just off the plot were painted in the margin, about a ring that was
|
|
291
|
+
// not drawn, and took a press there; left Plotly's clip and no more,
|
|
292
|
+
// an arrow drawn INWARD from an edge 2 px off the panel (the
|
|
293
|
+
// spectrum's lower one, next to 0 Hz) was painted whole inside it,
|
|
294
|
+
// where the pointer could not come, and one beside a border its
|
|
295
|
+
// checkpoint was just past left a sliver on the border. The mark is
|
|
296
|
+
// this pass's, like the others: of the figure as this redraw draws it.
|
|
297
|
+
//
|
|
298
|
+
// Returns the arrows that are ON the plot, each with what is painted
|
|
299
|
+
// of it (`painted`), for `stagger`.
|
|
300
|
+
const mark = () => {
|
|
301
|
+
const shapes = (gd.layout && gd.layout.shapes) || [];
|
|
302
|
+
const info = said(gd);
|
|
303
|
+
const fl = gd._fullLayout;
|
|
304
|
+
const arrows = [];
|
|
305
|
+
for (const el of gd.querySelectorAll(".shapelayer [data-index]")) {
|
|
306
|
+
const shape = shapes[+el.getAttribute("data-index")] || {};
|
|
307
|
+
el.toggleAttribute(
|
|
308
|
+
"data-inert",
|
|
309
|
+
!!info && (!info.draggable.has(shape.name) || gone(shape, fl)),
|
|
310
|
+
);
|
|
311
|
+
if (el.tagName !== "path") continue;
|
|
312
|
+
const grip = gripOf(info, shape.name);
|
|
313
|
+
if (grip) {
|
|
314
|
+
const at = fl ? placed(shape, fl) : null;
|
|
315
|
+
el.setAttribute("data-grip", grip.group);
|
|
316
|
+
el.toggleAttribute("data-grip-on", shown(el));
|
|
317
|
+
el.toggleAttribute("data-grip-off", !!at && at.off);
|
|
318
|
+
el.removeAttribute("data-grip-shift");
|
|
319
|
+
tell(el, grip);
|
|
320
|
+
const box = at && !at.off ? painted(shape, at) : null;
|
|
321
|
+
if (box) {
|
|
322
|
+
arrows.push({
|
|
323
|
+
el, group: grip.group, axis: grip.axis,
|
|
324
|
+
index: +el.getAttribute("data-index"), x: at.x, y: at.y, ...box,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
} else if (el.hasAttribute("data-grip")) {
|
|
328
|
+
el.removeAttribute("data-grip");
|
|
329
|
+
el.removeAttribute("data-grip-on");
|
|
330
|
+
el.removeAttribute("data-grip-off");
|
|
331
|
+
el.removeAttribute("data-grip-shift");
|
|
332
|
+
untell(el);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return arrows;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// The marks, taken off: every shape as Plotly draws it, clipped to
|
|
339
|
+
// the plot area.
|
|
340
|
+
const unmark = () => {
|
|
341
|
+
for (const el of gd.querySelectorAll("[data-grip]")) {
|
|
342
|
+
el.removeAttribute("data-grip");
|
|
343
|
+
el.removeAttribute("data-grip-on");
|
|
344
|
+
el.removeAttribute("data-grip-off");
|
|
345
|
+
el.removeAttribute("data-grip-shift");
|
|
346
|
+
untell(el);
|
|
347
|
+
}
|
|
348
|
+
for (const el of gd.querySelectorAll("[data-inert]")) {
|
|
349
|
+
el.removeAttribute("data-inert");
|
|
350
|
+
}
|
|
351
|
+
gd.removeAttribute("data-grip-near");
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// A pass over the plot that throws must not throw where it runs — in
|
|
355
|
+
// the observer of every redraw, in the page's own mouse events — and
|
|
356
|
+
// again on every redraw and every move after: it is said ONCE on the
|
|
357
|
+
// console, and every shape is left as Plotly draws it, none inert and
|
|
358
|
+
// none hidden, until a pass goes through.
|
|
359
|
+
const guarded = (pass) => (...args) => {
|
|
360
|
+
try {
|
|
361
|
+
return pass(...args);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
st.on = null;
|
|
364
|
+
st.clusters = [];
|
|
365
|
+
try {
|
|
366
|
+
unmark();
|
|
367
|
+
} catch (_) {
|
|
368
|
+
// What could not be read cannot be unmarked either.
|
|
369
|
+
}
|
|
370
|
+
if (!st.failed) {
|
|
371
|
+
st.failed = true;
|
|
372
|
+
console.error(
|
|
373
|
+
`DstfGrips: plot ${gd.id}: its shapes could not be marked and are ` +
|
|
374
|
+
`left as Plotly draws them — ${err}`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const show = (cluster) => {
|
|
382
|
+
if (cluster === st.on) return;
|
|
383
|
+
st.on = cluster;
|
|
384
|
+
for (const el of gd.querySelectorAll(".shapelayer path[data-grip]")) {
|
|
385
|
+
el.toggleAttribute("data-grip-on", shown(el));
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// Each group's hover area, in pixels of the graph div: the bounding box
|
|
390
|
+
// of the ring it stands around and of its grips' anchors, grown by
|
|
391
|
+
// MARGIN on every side — with the rings, for the hover label (`hush`).
|
|
392
|
+
// A grip anchored to the PAPER in y (an event's or a peak's window
|
|
393
|
+
// spans its panel) has no y of its own: it reaches over the panel's
|
|
394
|
+
// whole height. A ring whose group has no grip has no area.
|
|
395
|
+
//
|
|
396
|
+
// Of the members that are ON the plot (`placed`) alone: an arrow whose
|
|
397
|
+
// anchor is off it is not displayed (`mark`). A RING is on the plot
|
|
398
|
+
// while any of it is drawn — its centre within its own radius of the
|
|
399
|
+
// plot area: Plotly clips it to the area, and what is left of it has
|
|
400
|
+
// the `move` cursor and drags; by its centre alone, a ring 3 px past
|
|
401
|
+
// the border showed 4 px, dragged, and showed no arrow. A group
|
|
402
|
+
// partly on the plot has the area of what is on it — and shows the
|
|
403
|
+
// arrows whose own edges are; one that is off it whole — its ring and
|
|
404
|
+
// every arrow — has none, and nothing shows for it wherever the
|
|
405
|
+
// pointer is.
|
|
406
|
+
const areas = () => {
|
|
407
|
+
const fl = gd._fullLayout;
|
|
408
|
+
const out = new Map();
|
|
409
|
+
if (!fl) return out;
|
|
410
|
+
const info = said(gd);
|
|
411
|
+
const gripped = new Set();
|
|
412
|
+
const rings = [];
|
|
413
|
+
// Group `g`'s area, grown to hold x from t to b.
|
|
414
|
+
const reach = (g, x, t, b) => {
|
|
415
|
+
const a = out.get(g) || { l: x, r: x, t, b, rings: [] };
|
|
416
|
+
a.l = Math.min(a.l, x);
|
|
417
|
+
a.r = Math.max(a.r, x);
|
|
418
|
+
a.t = Math.min(a.t, t);
|
|
419
|
+
a.b = Math.max(a.b, b);
|
|
420
|
+
out.set(g, a);
|
|
421
|
+
return a;
|
|
422
|
+
};
|
|
423
|
+
for (const s of (gd.layout && gd.layout.shapes) || []) {
|
|
424
|
+
if (s.visible === false) continue;
|
|
425
|
+
const grip = gripOf(info, s.name);
|
|
426
|
+
const g = grip ? grip.group : ringOf(info, s.name);
|
|
427
|
+
if (g === null) continue;
|
|
428
|
+
const at = placed(s, fl, grip ? EDGE : radius(s));
|
|
429
|
+
if (!at) continue;
|
|
430
|
+
if (grip) gripped.add(g);
|
|
431
|
+
if (at.off) continue;
|
|
432
|
+
if (!grip) {
|
|
433
|
+
if (!at.paper) rings.push([g, at.x, at.y]);
|
|
434
|
+
} else if (at.paper) {
|
|
435
|
+
reach(g, at.x, at.top, at.bottom);
|
|
436
|
+
} else {
|
|
437
|
+
reach(g, at.x, at.y, at.y);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
for (const [g, x, y] of rings) {
|
|
441
|
+
if (gripped.has(g)) reach(g, x, y, y).rings.push([x, y]);
|
|
442
|
+
}
|
|
443
|
+
// To the hundredth of a pixel, the precision Plotly places a shape
|
|
444
|
+
// at (its `l2p`): two areas that touch on the screen touch here too,
|
|
445
|
+
// whatever an axis' offset adds in the last bit of a sum.
|
|
446
|
+
const px = (v) => Math.round(v * 100) / 100;
|
|
447
|
+
for (const a of out.values()) {
|
|
448
|
+
a.l = px(a.l - MARGIN);
|
|
449
|
+
a.r = px(a.r + MARGIN);
|
|
450
|
+
a.t = px(a.t - MARGIN);
|
|
451
|
+
a.b = px(a.b + MARGIN);
|
|
452
|
+
}
|
|
453
|
+
return out;
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// The clusters: groups whose areas overlap — touching counts — are of
|
|
457
|
+
// one, and so on through every group between (three in a row, each
|
|
458
|
+
// overlapping the next, are one). A cluster's own area is the UNION of
|
|
459
|
+
// its members' rectangles, never their bounding box: an L of two shows
|
|
460
|
+
// nothing from its empty corner. In PIXELS of the figure as it is
|
|
461
|
+
// drawn, so they are found again wherever the shapes are drawn again
|
|
462
|
+
// (`remark`) — a zoom parts a cluster, a dragged anchor joins two —
|
|
463
|
+
// and never on a move of the pointer. `hover` is `areas()`'s.
|
|
464
|
+
const clustered = (hover) => {
|
|
465
|
+
const meet = (p, q) => p.l <= q.r && q.l <= p.r && p.t <= q.b && q.t <= p.b;
|
|
466
|
+
let clusters = [];
|
|
467
|
+
for (const [g, area] of hover) {
|
|
468
|
+
const joined = { groups: new Set([g]), areas: [area], rings: [...area.rings] };
|
|
469
|
+
const apart = [];
|
|
470
|
+
// Every cluster the new area overlaps becomes one with it.
|
|
471
|
+
for (const c of clusters) {
|
|
472
|
+
if (!c.areas.some((other) => meet(other, area))) {
|
|
473
|
+
apart.push(c);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
for (const member of c.groups) joined.groups.add(member);
|
|
477
|
+
joined.areas.push(...c.areas);
|
|
478
|
+
joined.rings.push(...c.rings);
|
|
479
|
+
}
|
|
480
|
+
clusters = [...apart, joined];
|
|
481
|
+
}
|
|
482
|
+
return clusters;
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// STAGGER. A cluster shows all its members' arrows at once, and two of
|
|
486
|
+
// them can be drawn on one spot: with the facing edges of two
|
|
487
|
+
// neighbours 20 to 34 px apart at one height — two event windows
|
|
488
|
+
// always are at one — the `after` arrow of the first and the `before`
|
|
489
|
+
// of the second were one triangle on the screen, the later-drawn took
|
|
490
|
+
// the press, and a press on what looked like the first's arrow moved
|
|
491
|
+
// the second's side. The rule:
|
|
492
|
+
//
|
|
493
|
+
// 1. Two arrows COLLIDE when both are on the plot, are of DIFFERENT
|
|
494
|
+
// groups of ONE cluster, and what is painted of them (`painted`)
|
|
495
|
+
// shares more than OVERLAP px on both axes. (Arrows of two clusters
|
|
496
|
+
// are never shown together, and never meet: by 4 each is inside its
|
|
497
|
+
// own group's area, and two clusters' areas do not overlap.)
|
|
498
|
+
// 2. An arrow that collides is moved ACROSS its own axis — a time or a
|
|
499
|
+
// frequency arrow up or down, a value arrow left or right — by
|
|
500
|
+
// whole steps of STEP px. In order along their axis (then as
|
|
501
|
+
// drawn), each takes the first of LEVELS — one step up (left), one
|
|
502
|
+
// down (right), three up, three down — at which it clears every
|
|
503
|
+
// arrow already placed: those that collide with nothing, where the
|
|
504
|
+
// figure draws them, and those moved before it. One that already
|
|
505
|
+
// stands LEAN px or more below (right of) the arrows it collides
|
|
506
|
+
// with tries down (right) first: a pair parts the way it leans.
|
|
507
|
+
// 3. Found here, after every redraw, with the marks and the clusters
|
|
508
|
+
// (`remark`), and never on a move of the pointer: what is drawn
|
|
509
|
+
// with the pointer near is what was there before it came.
|
|
510
|
+
// 4. A moved arrow stays REACHABLE: as drawn it lies inside its own
|
|
511
|
+
// group's hover area, so the pointer on its way from the ring to
|
|
512
|
+
// it never leaves the area and hides it. A level that would put any
|
|
513
|
+
// of it outside is not one it may take: three steps is 33.5 px from
|
|
514
|
+
// the arrow's own line and an area reaches 25 px past its group's
|
|
515
|
+
// anchors, so a group that is flat across the arrow's axis — a
|
|
516
|
+
// checkpoint whose value tolerance is under 8.5 px, for a time
|
|
517
|
+
// arrow — has one step each way and no more.
|
|
518
|
+
// 5. An arrow that clears at no level it may take is left at the first
|
|
519
|
+
// it tried, still colliding: the third arrow on one spot where its
|
|
520
|
+
// group is flat, the fifth on one spot anywhere.
|
|
521
|
+
//
|
|
522
|
+
// The move is an ATTRIBUTE of the path, `data-grip-shift`, the steps
|
|
523
|
+
// as a number; the stylesheet (style.py) makes it a `transform`. So
|
|
524
|
+
// the path Plotly drags is the one it drew: held, it follows the
|
|
525
|
+
// pointer by the pointer's own travel, and a release says its edge
|
|
526
|
+
// where the pointer took it, never the step it is drawn aside by.
|
|
527
|
+
// `arrows` is `mark()`'s, `hover` `areas()`'s.
|
|
528
|
+
const stagger = (arrows, hover) => {
|
|
529
|
+
// To the hundredth of a pixel, as an area's edge is (`areas`).
|
|
530
|
+
const shared = (lo, hi) => Math.round((hi - lo) * 100) / 100;
|
|
531
|
+
const collide = (p, q) =>
|
|
532
|
+
shared(Math.max(p.l, q.l), Math.min(p.r, q.r)) > OVERLAP &&
|
|
533
|
+
shared(Math.max(p.t, q.t), Math.min(p.b, q.b)) > OVERLAP;
|
|
534
|
+
const across = (a) => (a.axis === "x" ? a.y : a.x);
|
|
535
|
+
const along = (a) => (a.axis === "x" ? a.x : a.y);
|
|
536
|
+
// Arrow `a` as it is painted `k` steps across its axis.
|
|
537
|
+
const moved = (a, k) =>
|
|
538
|
+
a.axis === "x"
|
|
539
|
+
? { ...a, t: a.t + k * STEP, b: a.b + k * STEP }
|
|
540
|
+
: { ...a, l: a.l + k * STEP, r: a.r + k * STEP };
|
|
541
|
+
const inside = (box, area) =>
|
|
542
|
+
box.l >= area.l && box.r <= area.r && box.t >= area.t && box.b <= area.b;
|
|
543
|
+
for (const cluster of st.clusters) {
|
|
544
|
+
const members = arrows.filter((a) => cluster.groups.has(a.group));
|
|
545
|
+
// Per colliding arrow, those it collides with.
|
|
546
|
+
const rivals = new Map();
|
|
547
|
+
const rival = (a, other) => rivals.set(a, [...(rivals.get(a) || []), other]);
|
|
548
|
+
members.forEach((p, i) => {
|
|
549
|
+
for (const q of members.slice(i + 1)) {
|
|
550
|
+
if (p.group === q.group || !collide(p, q)) continue;
|
|
551
|
+
rival(p, q);
|
|
552
|
+
rival(q, p);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
const set = members.filter((a) => !rivals.has(a));
|
|
556
|
+
const moving = [...rivals.keys()];
|
|
557
|
+
moving.sort((p, q) => along(p) - along(q) || p.index - q.index);
|
|
558
|
+
for (const a of moving) {
|
|
559
|
+
const others = rivals.get(a);
|
|
560
|
+
const lean = across(a) - others.reduce((sum, o) => sum + across(o), 0) / others.length;
|
|
561
|
+
const levels = lean >= LEAN ? LEVELS.map((k) => -k) : LEVELS;
|
|
562
|
+
const area = hover.get(a.group);
|
|
563
|
+
const clears = (k) => {
|
|
564
|
+
const there = moved(a, k);
|
|
565
|
+
return inside(there, area) && !set.some((o) => collide(o, there));
|
|
566
|
+
};
|
|
567
|
+
const level = levels.find(clears);
|
|
568
|
+
const taken = level === undefined ? levels[0] : level;
|
|
569
|
+
set.push(moved(a, taken));
|
|
570
|
+
a.el.setAttribute("data-grip-shift", String(taken));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// The cluster whose area holds the pointer (viewport px), or null: at
|
|
576
|
+
// most one does.
|
|
577
|
+
const under = (cx, cy) => {
|
|
578
|
+
const box = gd.getBoundingClientRect();
|
|
579
|
+
const x = cx - box.left;
|
|
580
|
+
const y = cy - box.top;
|
|
581
|
+
const holds = (a) => x >= a.l && x <= a.r && y >= a.t && y <= a.b;
|
|
582
|
+
return st.clusters.find((c) => c.areas.some(holds)) || null;
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
// How far the pointer (viewport px) is from the nearest SHOWN arrow's
|
|
586
|
+
// box — 0 on it, Infinity with none shown. The box is the path's own,
|
|
587
|
+
// as drawn: whichever way its arrow points, and wherever it is
|
|
588
|
+
// staggered to. An arrow off the plot is of a shown group and is not
|
|
589
|
+
// displayed (style.py): it has no box, and is passed over.
|
|
590
|
+
const arrow = (cx, cy) => {
|
|
591
|
+
let nearest = Infinity;
|
|
592
|
+
for (const el of gd.querySelectorAll(".shapelayer path[data-grip-on]")) {
|
|
593
|
+
const b = el.getBoundingClientRect();
|
|
594
|
+
if (!b.width && !b.height) continue;
|
|
595
|
+
const dx = cx < b.left ? b.left - cx : cx > b.right ? cx - b.right : 0;
|
|
596
|
+
const dy = cy < b.top ? b.top - cy : cy > b.bottom ? cy - b.bottom : 0;
|
|
597
|
+
nearest = Math.min(nearest, Math.hypot(dx, dy));
|
|
598
|
+
}
|
|
599
|
+
return nearest;
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// Within HUSH px of a shown arrow, or of a ring a shown group stands
|
|
603
|
+
// around, the plot's own hover label is not shown (`data-grip-near`,
|
|
604
|
+
// style.py): the unified label and its spike line were drawn over
|
|
605
|
+
// them. Only there: hidden for as long as arrows were shown, the curve
|
|
606
|
+
// could not be read anywhere in an event's window — the panel's whole
|
|
607
|
+
// height — while its criterion was open. An ATTRIBUTE of the graph
|
|
608
|
+
// div, not a class: a server update of the element's classes wipes one
|
|
609
|
+
// the page set, and keeps an attribute.
|
|
610
|
+
const hush = (xy) => {
|
|
611
|
+
let near = false;
|
|
612
|
+
if (xy && st.on) {
|
|
613
|
+
near = arrow(xy[0], xy[1]) <= HUSH;
|
|
614
|
+
if (!near) {
|
|
615
|
+
const box = gd.getBoundingClientRect();
|
|
616
|
+
const x = xy[0] - box.left;
|
|
617
|
+
const y = xy[1] - box.top;
|
|
618
|
+
near = st.on.rings.some(([px, py]) => Math.hypot(x - px, y - py) <= HUSH);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
gd.toggleAttribute("data-grip-near", near);
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
// Where the pointer is now — viewport px, or null off this plot: the
|
|
625
|
+
// cluster it is in shows. Held, nothing changes.
|
|
626
|
+
const here = (xy) => {
|
|
627
|
+
st.xy = xy;
|
|
628
|
+
if (st.latched) return;
|
|
629
|
+
show(xy ? under(xy[0], xy[1]) : null);
|
|
630
|
+
hush(xy);
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const move = guarded((e) => here([e.clientX, e.clientY]));
|
|
634
|
+
const leave = guarded(() => here(null));
|
|
635
|
+
// Once a drag starts the graph div gets one `mouseleave` and no
|
|
636
|
+
// `mousemove` (Plotly's drag cover takes the pointer): what is shown is
|
|
637
|
+
// held from the press to the release.
|
|
638
|
+
const down = guarded((e) => {
|
|
639
|
+
if (!gd.contains(e.target)) return;
|
|
640
|
+
st.latched = true;
|
|
641
|
+
st.press = [e.clientX, e.clientY];
|
|
642
|
+
st.travel = null;
|
|
643
|
+
// The ranges the drag is made in (`released`): the release's own
|
|
644
|
+
// relayout autoranges the axes over the dragged shape before anyone
|
|
645
|
+
// hears of it.
|
|
646
|
+
st.ranges = {};
|
|
647
|
+
for (const [key, axis] of Object.entries(gd._fullLayout || {})) {
|
|
648
|
+
if (/^[xy]axis\d*$/.test(key) && axis && axis.range) {
|
|
649
|
+
st.ranges[key] = axis.range.slice();
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// The handle this press holds, if it is on one (`follow`): the
|
|
653
|
+
// shape an editor moves, where the figure has it NOW — Plotly moves
|
|
654
|
+
// the drawn path under the pointer and writes the figure only at
|
|
655
|
+
// the release. Not under Ctrl: Plotly takes that press as a
|
|
656
|
+
// right-click.
|
|
657
|
+
st.held = st.ended = null;
|
|
658
|
+
const el = e.target instanceof Element ? e.target.closest(".shapelayer [data-index]") : null;
|
|
659
|
+
if (el && e.button === 0 && !e.ctrlKey && !el.hasAttribute("data-inert")) {
|
|
660
|
+
const index = +el.getAttribute("data-index");
|
|
661
|
+
const s = ((gd.layout && gd.layout.shapes) || [])[index];
|
|
662
|
+
const info = said(gd);
|
|
663
|
+
if (s && info && info.draggable.has(s.name)) {
|
|
664
|
+
st.held = { index, drag: ++seq, last: 0, pending: false, to: null };
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
const up = guarded((e) => {
|
|
669
|
+
if (!st.latched) return;
|
|
670
|
+
st.latched = false;
|
|
671
|
+
// How far the pointer went, per axis (`released`): heard here, in
|
|
672
|
+
// the capture phase, before Plotly ends its drag and relayouts.
|
|
673
|
+
st.travel = st.press && {
|
|
674
|
+
x: Math.abs(e.clientX - st.press[0]),
|
|
675
|
+
y: Math.abs(e.clientY - st.press[1]),
|
|
676
|
+
};
|
|
677
|
+
// The held handle is let go: no move of it is sent from here on,
|
|
678
|
+
// and the release is numbered past every one that was (`released`).
|
|
679
|
+
// Where it is let go is said ONCE MORE first: a move is sent a frame
|
|
680
|
+
// late and PACE ms apart, so the last one the fields showed could be
|
|
681
|
+
// short of the release — and stayed so for as long as the server
|
|
682
|
+
// took to stage it. Under MOVED px on both axes Plotly moves nothing
|
|
683
|
+
// and says nothing: the drag ends here, and the server is told so.
|
|
684
|
+
if (st.held) {
|
|
685
|
+
const held = st.held;
|
|
686
|
+
st.held = null;
|
|
687
|
+
const unmoved = st.travel && st.travel.x < MOVED && st.travel.y < MOVED;
|
|
688
|
+
held.to = [e.clientX, e.clientY];
|
|
689
|
+
const update = unmoved ? null : would(held);
|
|
690
|
+
if (update) emitEvent("dstf_grip_move", { plot: id, seq: ++seq, update });
|
|
691
|
+
st.ended = { index: held.index, seq: ++seq };
|
|
692
|
+
if (unmoved) {
|
|
693
|
+
emitEvent("dstf_grip_move", { plot: id, seq: st.ended.seq, end: true });
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const box = gd.getBoundingClientRect();
|
|
697
|
+
const over =
|
|
698
|
+
e.clientX >= box.left && e.clientX <= box.right &&
|
|
699
|
+
e.clientY >= box.top && e.clientY <= box.bottom;
|
|
700
|
+
here(over ? [e.clientX, e.clientY] : null);
|
|
701
|
+
});
|
|
702
|
+
// A release outside the window never arrives.
|
|
703
|
+
const blur = guarded(() => {
|
|
704
|
+
st.latched = false;
|
|
705
|
+
if (st.held) {
|
|
706
|
+
st.held = null;
|
|
707
|
+
emitEvent("dstf_grip_move", { plot: id, seq: ++seq, end: true });
|
|
708
|
+
}
|
|
709
|
+
here(null);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// What the axes of shape `s` SHOWED at the press, as spans: what the
|
|
713
|
+
// server snaps a placed value by (`comparison/snap.py`). An axis the
|
|
714
|
+
// shape is not on in data — an arrow anchored to the paper — has none.
|
|
715
|
+
const visible = (s) => {
|
|
716
|
+
const span = (letter, ref) => {
|
|
717
|
+
const range = /^[xy]\d*$/.test(ref) ? st.ranges[letter + "axis" + ref.slice(1)] : null;
|
|
718
|
+
return range ? Math.abs(range[1] - range[0]) : null;
|
|
719
|
+
};
|
|
720
|
+
return { x: span("x", s.xref || "x"), y: span("y", s.yref || "y") };
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
// Where the release of the held handle would put it, as the relayout
|
|
724
|
+
// that release would send: the figure's position at the press plus
|
|
725
|
+
// the pointer's travel, an axis the pointer went under MOVED px along
|
|
726
|
+
// not moved (Plotly's own rule, and `released`'s) — measured in
|
|
727
|
+
// Chromium to be the release's own number, bit for bit — and held to
|
|
728
|
+
// the range of the press, as `released` holds a release. A bound line
|
|
729
|
+
// moves in y, both its ends; a ring or an arrow by its anchor.
|
|
730
|
+
const would = (held) => {
|
|
731
|
+
const s = ((gd.layout && gd.layout.shapes) || [])[held.index];
|
|
732
|
+
const fl = gd._fullLayout;
|
|
733
|
+
if (!s || !fl || !st.press || !held.to) return null;
|
|
734
|
+
const out = {};
|
|
735
|
+
const put = (letter, key, from) => {
|
|
736
|
+
const ref = s[letter + "ref"] || letter;
|
|
737
|
+
if (!/^[xy]\d*$/.test(ref) || typeof from !== "number") return;
|
|
738
|
+
const axis = fl[letter + "axis" + ref.slice(1)];
|
|
739
|
+
const range = st.ranges[letter + "axis" + ref.slice(1)];
|
|
740
|
+
if (!axis || !range) return;
|
|
741
|
+
const travel = held.to[letter === "x" ? 0 : 1] - st.press[letter === "x" ? 0 : 1];
|
|
742
|
+
let value = null;
|
|
743
|
+
if (Math.abs(travel) >= MOVED) {
|
|
744
|
+
value = axis.p2d(axis.d2p(from) + travel);
|
|
745
|
+
value = Math.min(Math.max(value, Math.min(...range)), Math.max(...range));
|
|
746
|
+
}
|
|
747
|
+
out[`shapes[${held.index}].${key}`] = value;
|
|
748
|
+
};
|
|
749
|
+
if (s.type === "line") {
|
|
750
|
+
put("y", "y0", s.y0);
|
|
751
|
+
put("y", "y1", s.y1);
|
|
752
|
+
} else {
|
|
753
|
+
put("x", "xanchor", s.xanchor);
|
|
754
|
+
put("y", "yanchor", s.yanchor);
|
|
755
|
+
}
|
|
756
|
+
out[`shapes[${held.index}].visible`] = visible(s);
|
|
757
|
+
out[`shapes[${held.index}].drag`] = held.drag;
|
|
758
|
+
return out;
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
// A handle is HELD and the pointer moved: its would-be position goes
|
|
762
|
+
// to the server, once a frame at the most and PACE ms apart, the last
|
|
763
|
+
// one always sent. Heard on the document, in the capture phase:
|
|
764
|
+
// Plotly's drag cover takes every move from the plot itself.
|
|
765
|
+
const send = guarded((held) => {
|
|
766
|
+
held.pending = false;
|
|
767
|
+
if (st.held !== held) return; // let go meanwhile
|
|
768
|
+
const wait = PACE - (performance.now() - held.last);
|
|
769
|
+
if (wait > 0) {
|
|
770
|
+
held.pending = true;
|
|
771
|
+
setTimeout(() => requestAnimationFrame(() => send(held)), wait);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const update = would(held);
|
|
775
|
+
if (!update) return;
|
|
776
|
+
held.last = performance.now();
|
|
777
|
+
emitEvent("dstf_grip_move", { plot: id, seq: ++seq, update });
|
|
778
|
+
});
|
|
779
|
+
const follow = (e) => {
|
|
780
|
+
const held = st.held;
|
|
781
|
+
if (!held) return;
|
|
782
|
+
held.to = [e.clientX, e.clientY];
|
|
783
|
+
if (held.pending) return;
|
|
784
|
+
held.pending = true;
|
|
785
|
+
requestAnimationFrame(() => send(held));
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
// The panel under the pointer (viewport px) and the pointer in its
|
|
789
|
+
// axes — a panel is a pair of axes that overlays no other: the time
|
|
790
|
+
// plot's, the spectrum's — with what each axis shows; null outside
|
|
791
|
+
// every panel's plot area.
|
|
792
|
+
const spot = (cx, cy) => {
|
|
793
|
+
const fl = gd._fullLayout;
|
|
794
|
+
if (!fl || !fl._plots) return null;
|
|
795
|
+
const box = gd.getBoundingClientRect();
|
|
796
|
+
for (const plot of Object.values(fl._plots)) {
|
|
797
|
+
const xa = plot.xaxis;
|
|
798
|
+
const ya = plot.yaxis;
|
|
799
|
+
if (!xa || !ya || xa.overlaying || ya.overlaying) continue;
|
|
800
|
+
const px = cx - box.left - xa._offset;
|
|
801
|
+
const py = cy - box.top - ya._offset;
|
|
802
|
+
if (px < 0 || px > xa._length || py < 0 || py > ya._length) continue;
|
|
803
|
+
return {
|
|
804
|
+
xaxis: xa._id, yaxis: ya._id, x: xa.p2d(px), y: ya.p2d(py),
|
|
805
|
+
xrange: xa.range.slice(), yrange: ya.range.slice(),
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
return null;
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
// A CLICK on the plot, said to the server with where it is. Not a
|
|
812
|
+
// double-click (it resets the axes: `detail` counts the clicks); not
|
|
813
|
+
// the click that ends a drag — a Ctrl+drag draws Plotly's zoom box
|
|
814
|
+
// and ends in a real click where it is let go; not one on a shape an
|
|
815
|
+
// editor moves (a ring, an arrow, a bound: a press there is a drag's);
|
|
816
|
+
// not one outside every panel — where the toolbar and the legend
|
|
817
|
+
// stand, so a click on either is none of the plot's. Heard
|
|
818
|
+
// in the capture phase: a plain click here is one Plotly makes itself
|
|
819
|
+
// after its own handling, and it is still this plot's.
|
|
820
|
+
const clicked = guarded((e) => {
|
|
821
|
+
if (e.detail >= 2 || !(e.target instanceof Element)) return;
|
|
822
|
+
if (st.press && Math.hypot(e.clientX - st.press[0], e.clientY - st.press[1]) > CLICKED) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const shape = e.target.closest(".shapelayer [data-index]");
|
|
826
|
+
if (shape && !shape.hasAttribute("data-inert")) return;
|
|
827
|
+
const at = spot(e.clientX, e.clientY);
|
|
828
|
+
if (!at) return;
|
|
829
|
+
emitEvent("dstf_plot_click", {
|
|
830
|
+
plot: id, ...at,
|
|
831
|
+
ctrl: e.ctrlKey, shift: e.shiftKey, alt: e.altKey, meta: e.metaKey,
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
// A scroll moves the plot under a pointer at rest, and no mouse event
|
|
835
|
+
// says so: the arrows of a checkpoint wheeled out from under the
|
|
836
|
+
// pointer stayed shown until it moved, and one wheeled under it showed
|
|
837
|
+
// none. The pointer is where it was on the screen (the page's
|
|
838
|
+
// `pointer`) and the plot is not: the same test as a move, against
|
|
839
|
+
// the plot where it is now — if the pointer is over this plot at all,
|
|
840
|
+
// by what a press there would land on. Once per frame, however many
|
|
841
|
+
// scroll events one brings.
|
|
842
|
+
const rehere = guarded(() => {
|
|
843
|
+
const under = pointer && document.elementFromPoint(pointer[0], pointer[1]);
|
|
844
|
+
here(under && gd.contains(under) ? pointer : null);
|
|
845
|
+
});
|
|
846
|
+
const scrolled = () => {
|
|
847
|
+
if (st.frame) return;
|
|
848
|
+
st.frame = requestAnimationFrame(() => {
|
|
849
|
+
st.frame = 0;
|
|
850
|
+
rehere();
|
|
851
|
+
});
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// What a release sends of the shapes it moved: `update` is a relayout's
|
|
855
|
+
// (`shapes[N].xanchor`, `shapes[N].y0`, ...), read against the figure
|
|
856
|
+
// this plot holds; a copy is returned.
|
|
857
|
+
//
|
|
858
|
+
// A shape let go OUTSIDE the plot area — on the page above it, off the
|
|
859
|
+
// spectrum's left end — was released where the axis would be out
|
|
860
|
+
// there, and that number was written (an arrow's side at 2.5 on an
|
|
861
|
+
// axis ending at 1.3; a ring's time at 1.289 on a run ending at 1.0).
|
|
862
|
+
// Every position of a shape an editor moves — an arrow's anchor, a
|
|
863
|
+
// ring's, a bound line's ends — is held to the range that was VISIBLE
|
|
864
|
+
// through the drag, the press's (`down`): it lands on the plot's
|
|
865
|
+
// edge, where it was last drawn. A position on the paper, and the
|
|
866
|
+
// pixel offsets of a shape sized in pixels, are no data to hold.
|
|
867
|
+
//
|
|
868
|
+
// And an axis the pointer barely moved along is not moved. Under
|
|
869
|
+
// MOVED px of travel along an axis Plotly zeroes that travel, moves
|
|
870
|
+
// the shape on the other axis alone and re-snaps the position it did
|
|
871
|
+
// not move: a value side's arrow dragged sideways came back 0.0800462
|
|
872
|
+
// for 0.08, a ring dragged sideways wrote its value 0.5 as
|
|
873
|
+
// 0.500009668842, a bound line 1.0 as 1.0000266 — each staged. For
|
|
874
|
+
// EVERY shape an editor moves, a release says no position (null) on
|
|
875
|
+
// an axis the pointer went under MOVED px along, press to release:
|
|
876
|
+
// the editor reads no move there — under MOVED px on both, no move at
|
|
877
|
+
// all — and the redraw that answers every release puts the shape
|
|
878
|
+
// back.
|
|
879
|
+
const released = (update) => {
|
|
880
|
+
const shapes = (gd.layout && gd.layout.shapes) || [];
|
|
881
|
+
const info = said(gd);
|
|
882
|
+
const travel = st.travel;
|
|
883
|
+
st.travel = null;
|
|
884
|
+
const ended = st.ended;
|
|
885
|
+
st.ended = null;
|
|
886
|
+
const out = { ...update };
|
|
887
|
+
if (!info) return out;
|
|
888
|
+
for (const [key, value] of Object.entries(update)) {
|
|
889
|
+
const m = /^shapes\[(\d+)\]\.([xy])(anchor|0|1)$/.exec(key);
|
|
890
|
+
if (!m || typeof value !== "number") continue;
|
|
891
|
+
const s = shapes[+m[1]];
|
|
892
|
+
if (!s || !info.draggable.has(s.name)) continue;
|
|
893
|
+
// What its axes showed, for the server's snap, and the release's
|
|
894
|
+
// number: past every move of the drag it ends.
|
|
895
|
+
out[`shapes[${m[1]}].visible`] = visible(s);
|
|
896
|
+
out[`shapes[${m[1]}].seq`] = ended ? ended.seq : ++seq;
|
|
897
|
+
if (travel && travel[m[2]] < MOVED) {
|
|
898
|
+
out[key] = null;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (m[3] !== "anchor" && s[m[2] + "sizemode"] === "pixel") continue;
|
|
902
|
+
const ref = m[2] === "x" ? s.xref || "x" : s.yref || "y";
|
|
903
|
+
const range = /^[xy]\d*$/.test(ref) ? st.ranges[m[2] + "axis" + ref.slice(1)] : null;
|
|
904
|
+
if (!range) continue;
|
|
905
|
+
out[key] = Math.min(Math.max(value, Math.min(...range)), Math.max(...range));
|
|
906
|
+
}
|
|
907
|
+
return out;
|
|
908
|
+
};
|
|
909
|
+
// A release that cannot be read goes on as it came.
|
|
910
|
+
st.released = (update) => guarded(released)(update) || update;
|
|
911
|
+
|
|
912
|
+
gd.addEventListener("mousemove", move);
|
|
913
|
+
gd.addEventListener("mouseleave", leave);
|
|
914
|
+
gd.addEventListener("click", clicked, true);
|
|
915
|
+
document.addEventListener("mousemove", follow, true);
|
|
916
|
+
document.addEventListener("mousedown", down, true);
|
|
917
|
+
document.addEventListener("mouseup", up, true);
|
|
918
|
+
window.addEventListener("blur", blur);
|
|
919
|
+
// On the window, as it is captured: a scroll does not bubble, and
|
|
920
|
+
// whichever ancestor of the plot scrolls is heard on the way down.
|
|
921
|
+
window.addEventListener("scroll", scrolled, { capture: true, passive: true });
|
|
922
|
+
|
|
923
|
+
// The marks, the clusters, the colliding arrows' moves, and which
|
|
924
|
+
// cluster the pointer is in: after every redraw — the one after a
|
|
925
|
+
// release too, shown if the pointer is still there. Whatever moves a
|
|
926
|
+
// shape's pixels draws it again: a zoom, a pan, a reset, a resize of
|
|
927
|
+
// the window, a trace hidden from the legend, the server's own update
|
|
928
|
+
// are each a relayout or a replot, and Plotly draws every shape anew
|
|
929
|
+
// in each (`shapes.draw`).
|
|
930
|
+
const remark = guarded(() => {
|
|
931
|
+
const arrows = mark();
|
|
932
|
+
const hover = areas();
|
|
933
|
+
st.clusters = clustered(hover);
|
|
934
|
+
stagger(arrows, hover);
|
|
935
|
+
here(st.xy);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
// Before paint, so no frame shows an unmarked grip. A `plotly_afterplot`
|
|
939
|
+
// listener would be purged by the next newPlot.
|
|
940
|
+
const observer = new MutationObserver((records) => {
|
|
941
|
+
for (const record of records) {
|
|
942
|
+
for (const node of record.addedNodes) {
|
|
943
|
+
if (
|
|
944
|
+
node.nodeType === 1 &&
|
|
945
|
+
(node.hasAttribute("data-index") ||
|
|
946
|
+
node.classList.contains("shapelayer") ||
|
|
947
|
+
node.querySelector(".shapelayer"))
|
|
948
|
+
) {
|
|
949
|
+
remark();
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
observer.observe(gd, { childList: true, subtree: true });
|
|
956
|
+
|
|
957
|
+
st.destroy = () => {
|
|
958
|
+
gd.removeEventListener("mousemove", move);
|
|
959
|
+
gd.removeEventListener("mouseleave", leave);
|
|
960
|
+
gd.removeEventListener("click", clicked, true);
|
|
961
|
+
document.removeEventListener("mousemove", follow, true);
|
|
962
|
+
document.removeEventListener("mousedown", down, true);
|
|
963
|
+
st.held = null;
|
|
964
|
+
document.removeEventListener("mouseup", up, true);
|
|
965
|
+
window.removeEventListener("blur", blur);
|
|
966
|
+
window.removeEventListener("scroll", scrolled, true);
|
|
967
|
+
cancelAnimationFrame(st.frame);
|
|
968
|
+
observer.disconnect();
|
|
969
|
+
unmark();
|
|
970
|
+
};
|
|
971
|
+
remark();
|
|
972
|
+
return st;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
window.DstfGrips = {
|
|
976
|
+
instances,
|
|
977
|
+
// The element may not be in the page yet: NiceGUI's update that creates
|
|
978
|
+
// it and this call travel separately.
|
|
979
|
+
install(id, tries = 300) {
|
|
980
|
+
const gd = document.getElementById("c" + id);
|
|
981
|
+
if (!gd) {
|
|
982
|
+
if (tries > 0) requestAnimationFrame(() => this.install(id, tries - 1));
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
this.destroy(id);
|
|
986
|
+
instances.set(id, create(gd, id));
|
|
987
|
+
},
|
|
988
|
+
destroy(id) {
|
|
989
|
+
const st = instances.get(id);
|
|
990
|
+
if (st) st.destroy();
|
|
991
|
+
instances.delete(id);
|
|
992
|
+
},
|
|
993
|
+
// A relayout of plot `id` on its way to the server (plot_card.py):
|
|
994
|
+
// what that plot's instance says of a release.
|
|
995
|
+
released(id, update) {
|
|
996
|
+
const st = instances.get(id);
|
|
997
|
+
return st && update && typeof update === "object" ? st.released(update) : update;
|
|
998
|
+
},
|
|
999
|
+
};
|
|
1000
|
+
})();
|