MarcSkovMadsen commited on
Commit
40a04b7
·
1 Parent(s): 0f596ad

Upload 3 files

Browse files
Files changed (3) hide show
  1. index.html +0 -0
  2. index.jss +629 -0
  3. index.py +524 -0
index.html CHANGED
The diff for this file is too large to render. See raw diff
 
index.jss ADDED
@@ -0,0 +1,629 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ importScripts("https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js");
2
+
3
+ function sendPatch(patch, buffers, msg_id) {
4
+ self.postMessage({
5
+ type: 'patch',
6
+ patch: patch,
7
+ buffers: buffers
8
+ })
9
+ }
10
+
11
+ async function startApplication() {
12
+ console.log("Loading pyodide!");
13
+ self.postMessage({type: 'status', msg: 'Loading pyodide'})
14
+ self.pyodide = await loadPyodide();
15
+ self.pyodide.globals.set("sendPatch", sendPatch);
16
+ console.log("Loaded!");
17
+ await self.pyodide.loadPackage("micropip");
18
+ const env_spec = ['https://cdn.holoviz.org/panel/wheels/bokeh-3.3.2-py3-none-any.whl', 'https://cdn.holoviz.org/panel/1.3.6/dist/wheels/panel-1.3.6-py3-none-any.whl', 'pyodide-http==0.2.1', 'pandas']
19
+ for (const pkg of env_spec) {
20
+ let pkg_name;
21
+ if (pkg.endsWith('.whl')) {
22
+ pkg_name = pkg.split('/').slice(-1)[0].split('-')[0]
23
+ } else {
24
+ pkg_name = pkg
25
+ }
26
+ self.postMessage({type: 'status', msg: `Installing ${pkg_name}`})
27
+ try {
28
+ await self.pyodide.runPythonAsync(`
29
+ import micropip
30
+ await micropip.install('${pkg}');
31
+ `);
32
+ } catch(e) {
33
+ console.log(e)
34
+ self.postMessage({
35
+ type: 'status',
36
+ msg: `Error while installing ${pkg_name}`
37
+ });
38
+ }
39
+ }
40
+ console.log("Packages loaded!");
41
+ self.postMessage({type: 'status', msg: 'Executing code'})
42
+ const code = `
43
+
44
+ import asyncio
45
+
46
+ from panel.io.pyodide import init_doc, write_doc
47
+
48
+ init_doc()
49
+
50
+ #!/usr/bin/env python
51
+
52
+ import panel as pn
53
+ import pandas as pd
54
+
55
+ from bokeh.plotting import figure
56
+ from bokeh.layouts import layout
57
+ from bokeh.models import (
58
+ ColumnDataSource,
59
+ Range1d,
60
+ Slider,
61
+ Button,
62
+ TextInput,
63
+ LabelSet,
64
+ Circle,
65
+ Div,
66
+ )
67
+
68
+ class StumpyBokehDashboard:
69
+ def __init__(self):
70
+ self.sizing_mode = "stretch_both"
71
+ self.window = 0
72
+ self.m = None
73
+
74
+ self.df = None
75
+ self.ts_cds = None
76
+ self.quad_cds = None
77
+ self.pattern_match_cds = None
78
+ self.dist_cds = None
79
+ self.circle_cds = None
80
+
81
+ self.ts_plot = None
82
+ self.mp_plot = None
83
+ self.pm_plot = None
84
+ self.logo_div = None
85
+ self.heroku_div = None
86
+
87
+ self.slider = None
88
+ self.play_btn = None
89
+ self.txt_inp = None
90
+ self.pattern_btn = None
91
+ self.match_btn = None
92
+ self.reset_btn = None
93
+ self.idx = None
94
+ self.min_distance_idx = None
95
+
96
+ self.animation = pn.state.add_periodic_callback(
97
+ self.update_animate, 50, start=False
98
+ )
99
+
100
+ def get_df_from_file(self):
101
+ raw_df = pd.read_csv(
102
+ "https://raw.githubusercontent.com/seanlaw/stumpy-live-demo/master/raw.csv"
103
+ )
104
+
105
+ mp_df = pd.read_csv(
106
+ "https://raw.githubusercontent.com/seanlaw/stumpy-live-demo/master/matrix_profile.csv"
107
+ )
108
+
109
+ self.window = raw_df.shape[0] - mp_df.shape[0] + 1
110
+ self.m = raw_df.shape[0] - mp_df.shape[0] + 1
111
+ self.min_distance_idx = mp_df["distance"].argmin()
112
+
113
+ df = pd.merge(raw_df, mp_df, left_index=True, how="left", right_index=True)
114
+
115
+ return df.reset_index()
116
+
117
+ def get_ts_dict(self, df):
118
+ return self.df.to_dict(orient="list")
119
+
120
+ def get_circle_dict(self, df):
121
+ return self.df[["index", "y"]].to_dict(orient="list")
122
+
123
+ def get_quad_dict(self, df, pattern_idx=0, match_idx=None):
124
+ if match_idx is None:
125
+ match_idx = df.loc[pattern_idx, "idx"].astype(int)
126
+ quad_dict = dict(
127
+ pattern_left=[pattern_idx],
128
+ pattern_right=[pattern_idx + self.window - 1],
129
+ pattern_top=[max(df["y"])],
130
+ pattern_bottom=[0],
131
+ match_left=[match_idx],
132
+ match_right=[match_idx + self.window - 1],
133
+ match_top=[max(df["y"])],
134
+ match_bottom=[0],
135
+ vert_line_left=[pattern_idx - 5],
136
+ vert_line_right=[pattern_idx + 5],
137
+ vert_line_top=[max(df["distance"])],
138
+ vert_line_bottom=[0],
139
+ hori_line_left=[0],
140
+ hori_line_right=[max(df["index"])],
141
+ hori_line_top=[df.loc[pattern_idx, "distance"] - 0.01],
142
+ hori_line_bottom=[df.loc[pattern_idx, "distance"] + 0.01],
143
+ )
144
+ return quad_dict
145
+
146
+ def get_custom_quad_dict(self, df, pattern_idx=0, match_idx=None):
147
+ if match_idx is None:
148
+ match_idx = df.loc[pattern_idx, "idx"].astype(int)
149
+ quad_dict = dict(
150
+ pattern_left=[pattern_idx],
151
+ pattern_right=[pattern_idx + self.window - 1],
152
+ pattern_top=[max(df["y"])],
153
+ pattern_bottom=[0],
154
+ match_left=[match_idx],
155
+ match_right=[match_idx + self.window - 1],
156
+ match_top=[max(df["y"])],
157
+ match_bottom=[0],
158
+ vert_line_left=[match_idx - 5],
159
+ vert_line_right=[match_idx + 5],
160
+ vert_line_top=[max(df["distance"])],
161
+ vert_line_bottom=[0],
162
+ hori_line_left=[0],
163
+ hori_line_right=[max(df["index"])],
164
+ hori_line_top=[df.loc[match_idx, "distance"] - 0.01],
165
+ hori_line_bottom=[df.loc[match_idx, "distance"] + 0.01],
166
+ )
167
+ return quad_dict
168
+
169
+ def get_pattern_match_dict(self, df, pattern_idx=0, match_idx=None):
170
+ if match_idx is None:
171
+ match_idx = df["idx"].loc[pattern_idx].astype(int)
172
+ pattern_match_dict = dict(
173
+ index=list(range(self.window)),
174
+ pattern=df["y"].loc[pattern_idx : pattern_idx + self.window - 1],
175
+ match=df["y"].loc[match_idx : match_idx + self.window - 1],
176
+ )
177
+
178
+ return pattern_match_dict
179
+
180
+ def get_ts_plot(self, color="black"):
181
+ """
182
+ Time Series Plot
183
+ """
184
+ ts_plot = figure(
185
+ toolbar_location="above",
186
+ sizing_mode=self.sizing_mode,
187
+ title="Raw Time Series or Sequence",
188
+ tools=["reset"],
189
+ )
190
+ q = ts_plot.quad(
191
+ "pattern_left",
192
+ "pattern_right",
193
+ "pattern_top",
194
+ "pattern_bottom",
195
+ source=self.quad_cds,
196
+ name="pattern_quad",
197
+ color="#54b847",
198
+ )
199
+ q.visible = False
200
+ q = ts_plot.quad(
201
+ "match_left",
202
+ "match_right",
203
+ "match_top",
204
+ "match_bottom",
205
+ source=self.quad_cds,
206
+ name="match_quad",
207
+ color="#696969",
208
+ alpha=0.5,
209
+ )
210
+ q.visible = False
211
+ l = ts_plot.line(x="index", y="y", source=self.ts_cds, color=color)
212
+ ts_plot.x_range = Range1d(
213
+ 0, max(self.df["index"]), bounds=(0, max(self.df["x"]))
214
+ )
215
+ ts_plot.y_range = Range1d(0, max(self.df["y"]), bounds=(0, max(self.df["y"])))
216
+
217
+ c = ts_plot.circle(
218
+ x="index", y="y", source=self.circle_cds, size=0, line_color="white"
219
+ )
220
+ c.selection_glyph = Circle(line_color="white")
221
+ c.nonselection_glyph = Circle(line_color="white")
222
+
223
+ return ts_plot
224
+
225
+ def get_dist_dict(self, df, pattern_idx=0):
226
+ dist = df["distance"]
227
+ max_dist = dist.max()
228
+ min_dist = dist.min()
229
+ x_offset = self.df.shape[0] - self.window / 2
230
+ y_offset = max_dist / 2
231
+ distance = dist.loc[pattern_idx]
232
+ text = distance.round(1).astype(str)
233
+ gauge_dict = dict(x=[0 + x_offset], y=[0 + y_offset], text=[text])
234
+
235
+ return gauge_dict
236
+
237
+ def get_mp_plot(self):
238
+ """
239
+ Matrix Profile Plot
240
+ """
241
+ mp_plot = figure(
242
+ x_range=self.ts_plot.x_range,
243
+ toolbar_location=None,
244
+ sizing_mode=self.sizing_mode,
245
+ title="Matrix Profile (All Minimum Distances)",
246
+ )
247
+ q = mp_plot.quad(
248
+ "vert_line_left",
249
+ "vert_line_right",
250
+ "vert_line_top",
251
+ "vert_line_bottom",
252
+ source=self.quad_cds,
253
+ name="pattern_start",
254
+ color="#54b847",
255
+ )
256
+ q.visible = False
257
+ q = mp_plot.quad(
258
+ "hori_line_left",
259
+ "hori_line_right",
260
+ "hori_line_top",
261
+ "hori_line_bottom",
262
+ source=self.quad_cds,
263
+ name="match_dist",
264
+ color="#696969",
265
+ alpha=0.5,
266
+ )
267
+ q.visible = False
268
+ mp_plot.line(x="index", y="distance", source=self.ts_cds, color="black")
269
+ # mp_plot.x_range = Range1d(0, self.df.shape[0]-self.window+1, bounds=(0, self.df.shape[0]-self.window+1))
270
+ mp_plot.x_range = Range1d(
271
+ 0, self.df.shape[0] + 1, bounds=(0, self.df.shape[0] + 1)
272
+ )
273
+ mp_plot.y_range = Range1d(
274
+ 0, max(self.df["distance"]), bounds=(0, max(self.df["distance"]))
275
+ )
276
+
277
+ label = LabelSet(
278
+ x="x",
279
+ y="y",
280
+ text="text",
281
+ source=self.dist_cds,
282
+ text_align="center",
283
+ name="gauge_label",
284
+ text_color="black",
285
+ text_font_size="30pt",
286
+ )
287
+ mp_plot.add_layout(label)
288
+
289
+ return mp_plot
290
+
291
+ def get_pm_plot(self):
292
+ """
293
+ Pattern-Match Plot
294
+ """
295
+ pm_plot = figure(
296
+ toolbar_location=None,
297
+ sizing_mode=self.sizing_mode,
298
+ title="Pattern Match Overlay",
299
+ )
300
+ l = pm_plot.line(
301
+ "index",
302
+ "pattern",
303
+ source=self.pattern_match_cds,
304
+ name="pattern_line",
305
+ color="#54b847",
306
+ line_width=2,
307
+ )
308
+ l.visible = False
309
+ l = pm_plot.line(
310
+ "index",
311
+ "match",
312
+ source=self.pattern_match_cds,
313
+ name="match_line",
314
+ color="#696969",
315
+ alpha=0.5,
316
+ line_width=2,
317
+ )
318
+ l.visible = False
319
+
320
+ return pm_plot
321
+
322
+ def get_logo_div(self):
323
+ """
324
+ STUMPY logo
325
+ """
326
+
327
+ logo_div = Div(
328
+ text="<a href='https://stumpy.readthedocs.io/en/latest/'><img src='https://raw.githubusercontent.com/TDAmeritrade/stumpy/main/docs/images/stumpy_logo_small.png' style='width:100%'></a>", sizing_mode="stretch_width"
329
+ )
330
+
331
+ return logo_div
332
+
333
+ def get_heroku_div(self):
334
+ """
335
+ STUMPY Heroku App Link
336
+ """
337
+
338
+ heroku_div = Div(text="http://tiny.cc/stumpy-demo")
339
+
340
+ return heroku_div
341
+
342
+ def get_slider(self, value=0):
343
+ slider = Slider(
344
+ start=0.0,
345
+ end=max(self.df["index"]) - self.window,
346
+ value=value,
347
+ step=1,
348
+ title="Subsequence",
349
+ sizing_mode=self.sizing_mode,
350
+ )
351
+ return slider
352
+
353
+ def get_play_button(self):
354
+ play_btn = Button(label="► Play")
355
+ play_btn.on_click(self.animate)
356
+ return play_btn
357
+
358
+ def get_text_input(self):
359
+ txt_inp = TextInput(sizing_mode=self.sizing_mode)
360
+ return txt_inp
361
+
362
+ def get_buttons(self):
363
+ pattern_btn = Button(label="Show Motif", sizing_mode=self.sizing_mode)
364
+ match_btn = Button(label="Show Nearest Neighbor", sizing_mode=self.sizing_mode)
365
+ reset_btn = Button(label="Reset", sizing_mode=self.sizing_mode, button_type="primary")
366
+ return pattern_btn, match_btn, reset_btn
367
+
368
+ def update_plots(self, attr, new, old):
369
+ self.quad_cds.data = self.get_quad_dict(self.df, self.slider.value)
370
+ self.pattern_match_cds.data = self.get_pattern_match_dict(
371
+ self.df, self.slider.value
372
+ )
373
+ self.dist_cds.data = self.get_dist_dict(self.df, self.slider.value)
374
+
375
+ def custom_update_plots(self, attr, new, old):
376
+ self.quad_cds.data = self.get_custom_quad_dict(
377
+ self.df, self.pattern_idx, self.slider.value
378
+ )
379
+ self.pattern_match_cds.data = self.get_pattern_match_dict(
380
+ self.df, self.pattern_idx, self.slider.value
381
+ )
382
+ self.dist_cds.data = self.get_dist_dict(self.df, self.slider.value)
383
+ dist = self.df["distance"].loc[self.slider.value]
384
+
385
+ def show_hide_pattern(self):
386
+ pattern_quad = self.ts_plot.select(name="pattern_quad")[0]
387
+ pattern_start = self.mp_plot.select(name="pattern_start")[0]
388
+ pattern_line = self.pm_plot.select(name="pattern_line")[0]
389
+ if pattern_quad.visible:
390
+ pattern_start.visible = False
391
+ pattern_line.visible = False
392
+ pattern_quad.visible = False
393
+ self.pattern_btn.label = "Show Motif"
394
+ else:
395
+ pattern_start.visible = True
396
+ pattern_line.visible = True
397
+ pattern_quad.visible = True
398
+ self.pattern_btn.label = "Hide Motif"
399
+
400
+ def show_hide_match(self):
401
+ match_quad = self.ts_plot.select(name="match_quad")[0]
402
+ match_dist = self.mp_plot.select(name="match_dist")[0]
403
+ match_line = self.pm_plot.select(name="match_line")[0]
404
+ if match_quad.visible:
405
+ match_dist.visible = False
406
+ match_line.visible = False
407
+ match_quad.visible = False
408
+ self.match_btn.label = "Show Nearest Neighbor"
409
+ else:
410
+ match_dist.visible = True
411
+ match_line.visible = True
412
+ match_quad.visible = True
413
+ self.match_btn.label = "Hide Nearest Neighbor"
414
+
415
+ def update_slider(self, attr, old, new):
416
+ self.slider.value = int(self.txt_inp.value)
417
+
418
+ def animate(self):
419
+ if self.play_btn.label == "► Play":
420
+ self.play_btn.label = "❚❚ Pause"
421
+ self.animation.start()
422
+ else:
423
+ self.play_btn.label = "► Play"
424
+ self.animation.stop()
425
+
426
+ def update_animate(self, shift=50):
427
+ if self.window < self.m: # Probably using box select
428
+ start = self.slider.value
429
+ end = start + shift
430
+ if self.df.loc[start:end, "distance"].min() <= 15:
431
+ self.slider.value = self.df.loc[start:end, "distance"].idxmin()
432
+ self.animate()
433
+ elif self.slider.value + shift <= self.slider.end:
434
+ self.slider.value = self.slider.value + shift
435
+ else:
436
+ self.slider.value = 0
437
+ elif self.slider.value + shift <= self.slider.end:
438
+ self.slider.value = self.slider.value + shift
439
+ else:
440
+ self.slider.value = 0
441
+
442
+ def reset(self):
443
+ self.sizing_mode = "stretch_both"
444
+ self.window = self.m
445
+
446
+ self.default_idx = self.min_distance_idx
447
+ self.df = self.get_df_from_file()
448
+ self.ts_cds.data = self.get_ts_dict(self.df)
449
+ self.mp_plot.y_range.end = max(self.df["distance"])
450
+ self.mp_plot.title.text = "Matrix Profile (All Minimum Distances)"
451
+ self.mp_plot.y_range.bounds = (0, max(self.df["distance"]))
452
+ self.quad_cds.data = self.get_quad_dict(self.df, pattern_idx=self.default_idx)
453
+ self.pattern_match_cds.data = self.get_pattern_match_dict(
454
+ self.df, pattern_idx=self.default_idx
455
+ )
456
+ self.dist_cds.data = self.get_dist_dict(self.df, pattern_idx=self.default_idx)
457
+ self.circle_cds.data = self.get_circle_dict(self.df)
458
+ # Remove callback and add old callback
459
+ if self.custom_update_plots in self.slider._callbacks["value"]:
460
+ self.slider.remove_on_change("value", self.custom_update_plots)
461
+ self.slider.on_change("value", self.update_plots)
462
+ self.slider.end = self.df.shape[0] - self.window
463
+ self.slider.value = self.default_idx
464
+
465
+ def get_data(self):
466
+ self.df = self.get_df_from_file()
467
+ self.default_idx = self.min_distance_idx
468
+ self.ts_cds = ColumnDataSource(self.get_ts_dict(self.df))
469
+ self.quad_cds = ColumnDataSource(
470
+ self.get_quad_dict(self.df, pattern_idx=self.default_idx)
471
+ )
472
+ self.pattern_match_cds = ColumnDataSource(
473
+ self.get_pattern_match_dict(self.df, pattern_idx=self.default_idx)
474
+ )
475
+ self.dist_cds = ColumnDataSource(
476
+ self.get_dist_dict(self.df, pattern_idx=self.default_idx)
477
+ )
478
+ self.circle_cds = ColumnDataSource(self.get_circle_dict(self.df))
479
+
480
+ def get_plots(self, ts_plot_color="black"):
481
+ self.ts_plot = self.get_ts_plot(color=ts_plot_color)
482
+ self.mp_plot = self.get_mp_plot()
483
+ self.pm_plot = self.get_pm_plot()
484
+
485
+ def get_widgets(self):
486
+ self.slider = self.get_slider(value=self.default_idx)
487
+ self.play_btn = self.get_play_button()
488
+ self.txt_inp = self.get_text_input()
489
+ self.pattern_btn, self.match_btn, self.reset_btn = self.get_buttons()
490
+ self.logo_div = self.get_logo_div()
491
+ self.heroku_div = self.get_heroku_div()
492
+
493
+ def set_callbacks(self):
494
+ self.slider.on_change("value", self.update_plots)
495
+ self.pattern_btn.on_click(self.show_hide_pattern)
496
+ self.show_hide_pattern()
497
+ self.match_btn.on_click(self.show_hide_match)
498
+ self.show_hide_match()
499
+ self.reset_btn.on_click(self.reset)
500
+ self.txt_inp.on_change("value", self.update_slider)
501
+
502
+ def get_layout(self):
503
+ self.get_data()
504
+ self.get_plots()
505
+ self.get_widgets()
506
+ self.set_callbacks()
507
+
508
+ l = layout(
509
+ [
510
+ [self.ts_plot],
511
+ [self.mp_plot],
512
+ [self.pm_plot],
513
+ [self.slider],
514
+ [self.pattern_btn, self.match_btn, self.play_btn, self.logo_div],
515
+ ],
516
+ sizing_mode=self.sizing_mode,
517
+ )
518
+
519
+ return l
520
+
521
+ def get_raw_layout(self):
522
+ self.get_data()
523
+ self.get_plots(ts_plot_color="#54b847")
524
+
525
+ l = layout([[self.ts_plot], [self.mp_plot]], sizing_mode=self.sizing_mode)
526
+
527
+ return l
528
+
529
+
530
+ dashboard = StumpyBokehDashboard()
531
+
532
+ def get_components(dashboard: StumpyBokehDashboard=dashboard):
533
+ dashboard.get_data()
534
+ dashboard.get_plots()
535
+ dashboard.get_widgets()
536
+ dashboard.set_callbacks()
537
+
538
+ logo = dashboard.logo_div
539
+ settings = layout(
540
+ dashboard.pattern_btn,
541
+ dashboard.match_btn,
542
+ dashboard.play_btn,
543
+ dashboard.slider,
544
+ height=150,
545
+ sizing_mode="stretch_width",
546
+ )
547
+ main = layout(
548
+ [
549
+ [dashboard.ts_plot],
550
+ [dashboard.mp_plot],
551
+ [dashboard.pm_plot],
552
+ ],
553
+ sizing_mode=dashboard.sizing_mode,
554
+ )
555
+ return logo, settings, main
556
+
557
+ pn.extension(template="fast")
558
+ pn.state.template.param.update(
559
+ site_url="https://awesome-panel.org",
560
+ site="Awesome Panel",
561
+ title="Stumpy Timeseries Analysis",
562
+ favicon="https://raw.githubusercontent.com/MarcSkovMadsen/awesome-panel-assets/320297ccb92773da099f6b97d267cc0433b67c23/favicon/ap-1f77b4.ico",
563
+ header_background="#459db9",
564
+ theme_toggle=False,
565
+ )
566
+
567
+ logo, settings, main = get_components()
568
+
569
+ pn.Column(
570
+ logo,
571
+ settings, sizing_mode="stretch_width",
572
+ ).servable(target="sidebar")
573
+ pn.panel(main, sizing_mode="stretch_both", max_height=800).servable(target="main")
574
+
575
+
576
+ await write_doc()
577
+ `
578
+
579
+ try {
580
+ const [docs_json, render_items, root_ids] = await self.pyodide.runPythonAsync(code)
581
+ self.postMessage({
582
+ type: 'render',
583
+ docs_json: docs_json,
584
+ render_items: render_items,
585
+ root_ids: root_ids
586
+ })
587
+ } catch(e) {
588
+ const traceback = `${e}`
589
+ const tblines = traceback.split('\n')
590
+ self.postMessage({
591
+ type: 'status',
592
+ msg: tblines[tblines.length-2]
593
+ });
594
+ throw e
595
+ }
596
+ }
597
+
598
+ self.onmessage = async (event) => {
599
+ const msg = event.data
600
+ if (msg.type === 'rendered') {
601
+ self.pyodide.runPythonAsync(`
602
+ from panel.io.state import state
603
+ from panel.io.pyodide import _link_docs_worker
604
+
605
+ _link_docs_worker(state.curdoc, sendPatch, setter='js')
606
+ `)
607
+ } else if (msg.type === 'patch') {
608
+ self.pyodide.globals.set('patch', msg.patch)
609
+ self.pyodide.runPythonAsync(`
610
+ state.curdoc.apply_json_patch(patch.to_py(), setter='js')
611
+ `)
612
+ self.postMessage({type: 'idle'})
613
+ } else if (msg.type === 'location') {
614
+ self.pyodide.globals.set('location', msg.location)
615
+ self.pyodide.runPythonAsync(`
616
+ import json
617
+ from panel.io.state import state
618
+ from panel.util import edit_readonly
619
+ if state.location:
620
+ loc_data = json.loads(location)
621
+ with edit_readonly(state.location):
622
+ state.location.param.update({
623
+ k: v for k, v in loc_data.items() if k in state.location.param
624
+ })
625
+ `)
626
+ }
627
+ }
628
+
629
+ startApplication()
index.py ADDED
@@ -0,0 +1,524 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+
3
+ import panel as pn
4
+ import pandas as pd
5
+
6
+ from bokeh.plotting import figure
7
+ from bokeh.layouts import layout
8
+ from bokeh.models import (
9
+ ColumnDataSource,
10
+ Range1d,
11
+ Slider,
12
+ Button,
13
+ TextInput,
14
+ LabelSet,
15
+ Circle,
16
+ Div,
17
+ )
18
+
19
+ class StumpyBokehDashboard:
20
+ def __init__(self):
21
+ self.sizing_mode = "stretch_both"
22
+ self.window = 0
23
+ self.m = None
24
+
25
+ self.df = None
26
+ self.ts_cds = None
27
+ self.quad_cds = None
28
+ self.pattern_match_cds = None
29
+ self.dist_cds = None
30
+ self.circle_cds = None
31
+
32
+ self.ts_plot = None
33
+ self.mp_plot = None
34
+ self.pm_plot = None
35
+ self.logo_div = None
36
+ self.heroku_div = None
37
+
38
+ self.slider = None
39
+ self.play_btn = None
40
+ self.txt_inp = None
41
+ self.pattern_btn = None
42
+ self.match_btn = None
43
+ self.reset_btn = None
44
+ self.idx = None
45
+ self.min_distance_idx = None
46
+
47
+ self.animation = pn.state.add_periodic_callback(
48
+ self.update_animate, 50, start=False
49
+ )
50
+
51
+ def get_df_from_file(self):
52
+ raw_df = pd.read_csv(
53
+ "https://raw.githubusercontent.com/seanlaw/stumpy-live-demo/master/raw.csv"
54
+ )
55
+
56
+ mp_df = pd.read_csv(
57
+ "https://raw.githubusercontent.com/seanlaw/stumpy-live-demo/master/matrix_profile.csv"
58
+ )
59
+
60
+ self.window = raw_df.shape[0] - mp_df.shape[0] + 1
61
+ self.m = raw_df.shape[0] - mp_df.shape[0] + 1
62
+ self.min_distance_idx = mp_df["distance"].argmin()
63
+
64
+ df = pd.merge(raw_df, mp_df, left_index=True, how="left", right_index=True)
65
+
66
+ return df.reset_index()
67
+
68
+ def get_ts_dict(self, df):
69
+ return self.df.to_dict(orient="list")
70
+
71
+ def get_circle_dict(self, df):
72
+ return self.df[["index", "y"]].to_dict(orient="list")
73
+
74
+ def get_quad_dict(self, df, pattern_idx=0, match_idx=None):
75
+ if match_idx is None:
76
+ match_idx = df.loc[pattern_idx, "idx"].astype(int)
77
+ quad_dict = dict(
78
+ pattern_left=[pattern_idx],
79
+ pattern_right=[pattern_idx + self.window - 1],
80
+ pattern_top=[max(df["y"])],
81
+ pattern_bottom=[0],
82
+ match_left=[match_idx],
83
+ match_right=[match_idx + self.window - 1],
84
+ match_top=[max(df["y"])],
85
+ match_bottom=[0],
86
+ vert_line_left=[pattern_idx - 5],
87
+ vert_line_right=[pattern_idx + 5],
88
+ vert_line_top=[max(df["distance"])],
89
+ vert_line_bottom=[0],
90
+ hori_line_left=[0],
91
+ hori_line_right=[max(df["index"])],
92
+ hori_line_top=[df.loc[pattern_idx, "distance"] - 0.01],
93
+ hori_line_bottom=[df.loc[pattern_idx, "distance"] + 0.01],
94
+ )
95
+ return quad_dict
96
+
97
+ def get_custom_quad_dict(self, df, pattern_idx=0, match_idx=None):
98
+ if match_idx is None:
99
+ match_idx = df.loc[pattern_idx, "idx"].astype(int)
100
+ quad_dict = dict(
101
+ pattern_left=[pattern_idx],
102
+ pattern_right=[pattern_idx + self.window - 1],
103
+ pattern_top=[max(df["y"])],
104
+ pattern_bottom=[0],
105
+ match_left=[match_idx],
106
+ match_right=[match_idx + self.window - 1],
107
+ match_top=[max(df["y"])],
108
+ match_bottom=[0],
109
+ vert_line_left=[match_idx - 5],
110
+ vert_line_right=[match_idx + 5],
111
+ vert_line_top=[max(df["distance"])],
112
+ vert_line_bottom=[0],
113
+ hori_line_left=[0],
114
+ hori_line_right=[max(df["index"])],
115
+ hori_line_top=[df.loc[match_idx, "distance"] - 0.01],
116
+ hori_line_bottom=[df.loc[match_idx, "distance"] + 0.01],
117
+ )
118
+ return quad_dict
119
+
120
+ def get_pattern_match_dict(self, df, pattern_idx=0, match_idx=None):
121
+ if match_idx is None:
122
+ match_idx = df["idx"].loc[pattern_idx].astype(int)
123
+ pattern_match_dict = dict(
124
+ index=list(range(self.window)),
125
+ pattern=df["y"].loc[pattern_idx : pattern_idx + self.window - 1],
126
+ match=df["y"].loc[match_idx : match_idx + self.window - 1],
127
+ )
128
+
129
+ return pattern_match_dict
130
+
131
+ def get_ts_plot(self, color="black"):
132
+ """
133
+ Time Series Plot
134
+ """
135
+ ts_plot = figure(
136
+ toolbar_location="above",
137
+ sizing_mode=self.sizing_mode,
138
+ title="Raw Time Series or Sequence",
139
+ tools=["reset"],
140
+ )
141
+ q = ts_plot.quad(
142
+ "pattern_left",
143
+ "pattern_right",
144
+ "pattern_top",
145
+ "pattern_bottom",
146
+ source=self.quad_cds,
147
+ name="pattern_quad",
148
+ color="#54b847",
149
+ )
150
+ q.visible = False
151
+ q = ts_plot.quad(
152
+ "match_left",
153
+ "match_right",
154
+ "match_top",
155
+ "match_bottom",
156
+ source=self.quad_cds,
157
+ name="match_quad",
158
+ color="#696969",
159
+ alpha=0.5,
160
+ )
161
+ q.visible = False
162
+ l = ts_plot.line(x="index", y="y", source=self.ts_cds, color=color)
163
+ ts_plot.x_range = Range1d(
164
+ 0, max(self.df["index"]), bounds=(0, max(self.df["x"]))
165
+ )
166
+ ts_plot.y_range = Range1d(0, max(self.df["y"]), bounds=(0, max(self.df["y"])))
167
+
168
+ c = ts_plot.circle(
169
+ x="index", y="y", source=self.circle_cds, size=0, line_color="white"
170
+ )
171
+ c.selection_glyph = Circle(line_color="white")
172
+ c.nonselection_glyph = Circle(line_color="white")
173
+
174
+ return ts_plot
175
+
176
+ def get_dist_dict(self, df, pattern_idx=0):
177
+ dist = df["distance"]
178
+ max_dist = dist.max()
179
+ min_dist = dist.min()
180
+ x_offset = self.df.shape[0] - self.window / 2
181
+ y_offset = max_dist / 2
182
+ distance = dist.loc[pattern_idx]
183
+ text = distance.round(1).astype(str)
184
+ gauge_dict = dict(x=[0 + x_offset], y=[0 + y_offset], text=[text])
185
+
186
+ return gauge_dict
187
+
188
+ def get_mp_plot(self):
189
+ """
190
+ Matrix Profile Plot
191
+ """
192
+ mp_plot = figure(
193
+ x_range=self.ts_plot.x_range,
194
+ toolbar_location=None,
195
+ sizing_mode=self.sizing_mode,
196
+ title="Matrix Profile (All Minimum Distances)",
197
+ )
198
+ q = mp_plot.quad(
199
+ "vert_line_left",
200
+ "vert_line_right",
201
+ "vert_line_top",
202
+ "vert_line_bottom",
203
+ source=self.quad_cds,
204
+ name="pattern_start",
205
+ color="#54b847",
206
+ )
207
+ q.visible = False
208
+ q = mp_plot.quad(
209
+ "hori_line_left",
210
+ "hori_line_right",
211
+ "hori_line_top",
212
+ "hori_line_bottom",
213
+ source=self.quad_cds,
214
+ name="match_dist",
215
+ color="#696969",
216
+ alpha=0.5,
217
+ )
218
+ q.visible = False
219
+ mp_plot.line(x="index", y="distance", source=self.ts_cds, color="black")
220
+ # mp_plot.x_range = Range1d(0, self.df.shape[0]-self.window+1, bounds=(0, self.df.shape[0]-self.window+1))
221
+ mp_plot.x_range = Range1d(
222
+ 0, self.df.shape[0] + 1, bounds=(0, self.df.shape[0] + 1)
223
+ )
224
+ mp_plot.y_range = Range1d(
225
+ 0, max(self.df["distance"]), bounds=(0, max(self.df["distance"]))
226
+ )
227
+
228
+ label = LabelSet(
229
+ x="x",
230
+ y="y",
231
+ text="text",
232
+ source=self.dist_cds,
233
+ text_align="center",
234
+ name="gauge_label",
235
+ text_color="black",
236
+ text_font_size="30pt",
237
+ )
238
+ mp_plot.add_layout(label)
239
+
240
+ return mp_plot
241
+
242
+ def get_pm_plot(self):
243
+ """
244
+ Pattern-Match Plot
245
+ """
246
+ pm_plot = figure(
247
+ toolbar_location=None,
248
+ sizing_mode=self.sizing_mode,
249
+ title="Pattern Match Overlay",
250
+ )
251
+ l = pm_plot.line(
252
+ "index",
253
+ "pattern",
254
+ source=self.pattern_match_cds,
255
+ name="pattern_line",
256
+ color="#54b847",
257
+ line_width=2,
258
+ )
259
+ l.visible = False
260
+ l = pm_plot.line(
261
+ "index",
262
+ "match",
263
+ source=self.pattern_match_cds,
264
+ name="match_line",
265
+ color="#696969",
266
+ alpha=0.5,
267
+ line_width=2,
268
+ )
269
+ l.visible = False
270
+
271
+ return pm_plot
272
+
273
+ def get_logo_div(self):
274
+ """
275
+ STUMPY logo
276
+ """
277
+
278
+ logo_div = Div(
279
+ text="<a href='https://stumpy.readthedocs.io/en/latest/'><img src='https://raw.githubusercontent.com/TDAmeritrade/stumpy/main/docs/images/stumpy_logo_small.png' style='width:100%'></a>", sizing_mode="stretch_width"
280
+ )
281
+
282
+ return logo_div
283
+
284
+ def get_heroku_div(self):
285
+ """
286
+ STUMPY Heroku App Link
287
+ """
288
+
289
+ heroku_div = Div(text="http://tiny.cc/stumpy-demo")
290
+
291
+ return heroku_div
292
+
293
+ def get_slider(self, value=0):
294
+ slider = Slider(
295
+ start=0.0,
296
+ end=max(self.df["index"]) - self.window,
297
+ value=value,
298
+ step=1,
299
+ title="Subsequence",
300
+ sizing_mode=self.sizing_mode,
301
+ )
302
+ return slider
303
+
304
+ def get_play_button(self):
305
+ play_btn = Button(label="► Play")
306
+ play_btn.on_click(self.animate)
307
+ return play_btn
308
+
309
+ def get_text_input(self):
310
+ txt_inp = TextInput(sizing_mode=self.sizing_mode)
311
+ return txt_inp
312
+
313
+ def get_buttons(self):
314
+ pattern_btn = Button(label="Show Motif", sizing_mode=self.sizing_mode)
315
+ match_btn = Button(label="Show Nearest Neighbor", sizing_mode=self.sizing_mode)
316
+ reset_btn = Button(label="Reset", sizing_mode=self.sizing_mode, button_type="primary")
317
+ return pattern_btn, match_btn, reset_btn
318
+
319
+ def update_plots(self, attr, new, old):
320
+ self.quad_cds.data = self.get_quad_dict(self.df, self.slider.value)
321
+ self.pattern_match_cds.data = self.get_pattern_match_dict(
322
+ self.df, self.slider.value
323
+ )
324
+ self.dist_cds.data = self.get_dist_dict(self.df, self.slider.value)
325
+
326
+ def custom_update_plots(self, attr, new, old):
327
+ self.quad_cds.data = self.get_custom_quad_dict(
328
+ self.df, self.pattern_idx, self.slider.value
329
+ )
330
+ self.pattern_match_cds.data = self.get_pattern_match_dict(
331
+ self.df, self.pattern_idx, self.slider.value
332
+ )
333
+ self.dist_cds.data = self.get_dist_dict(self.df, self.slider.value)
334
+ dist = self.df["distance"].loc[self.slider.value]
335
+
336
+ def show_hide_pattern(self):
337
+ pattern_quad = self.ts_plot.select(name="pattern_quad")[0]
338
+ pattern_start = self.mp_plot.select(name="pattern_start")[0]
339
+ pattern_line = self.pm_plot.select(name="pattern_line")[0]
340
+ if pattern_quad.visible:
341
+ pattern_start.visible = False
342
+ pattern_line.visible = False
343
+ pattern_quad.visible = False
344
+ self.pattern_btn.label = "Show Motif"
345
+ else:
346
+ pattern_start.visible = True
347
+ pattern_line.visible = True
348
+ pattern_quad.visible = True
349
+ self.pattern_btn.label = "Hide Motif"
350
+
351
+ def show_hide_match(self):
352
+ match_quad = self.ts_plot.select(name="match_quad")[0]
353
+ match_dist = self.mp_plot.select(name="match_dist")[0]
354
+ match_line = self.pm_plot.select(name="match_line")[0]
355
+ if match_quad.visible:
356
+ match_dist.visible = False
357
+ match_line.visible = False
358
+ match_quad.visible = False
359
+ self.match_btn.label = "Show Nearest Neighbor"
360
+ else:
361
+ match_dist.visible = True
362
+ match_line.visible = True
363
+ match_quad.visible = True
364
+ self.match_btn.label = "Hide Nearest Neighbor"
365
+
366
+ def update_slider(self, attr, old, new):
367
+ self.slider.value = int(self.txt_inp.value)
368
+
369
+ def animate(self):
370
+ if self.play_btn.label == "► Play":
371
+ self.play_btn.label = "❚❚ Pause"
372
+ self.animation.start()
373
+ else:
374
+ self.play_btn.label = "► Play"
375
+ self.animation.stop()
376
+
377
+ def update_animate(self, shift=50):
378
+ if self.window < self.m: # Probably using box select
379
+ start = self.slider.value
380
+ end = start + shift
381
+ if self.df.loc[start:end, "distance"].min() <= 15:
382
+ self.slider.value = self.df.loc[start:end, "distance"].idxmin()
383
+ self.animate()
384
+ elif self.slider.value + shift <= self.slider.end:
385
+ self.slider.value = self.slider.value + shift
386
+ else:
387
+ self.slider.value = 0
388
+ elif self.slider.value + shift <= self.slider.end:
389
+ self.slider.value = self.slider.value + shift
390
+ else:
391
+ self.slider.value = 0
392
+
393
+ def reset(self):
394
+ self.sizing_mode = "stretch_both"
395
+ self.window = self.m
396
+
397
+ self.default_idx = self.min_distance_idx
398
+ self.df = self.get_df_from_file()
399
+ self.ts_cds.data = self.get_ts_dict(self.df)
400
+ self.mp_plot.y_range.end = max(self.df["distance"])
401
+ self.mp_plot.title.text = "Matrix Profile (All Minimum Distances)"
402
+ self.mp_plot.y_range.bounds = (0, max(self.df["distance"]))
403
+ self.quad_cds.data = self.get_quad_dict(self.df, pattern_idx=self.default_idx)
404
+ self.pattern_match_cds.data = self.get_pattern_match_dict(
405
+ self.df, pattern_idx=self.default_idx
406
+ )
407
+ self.dist_cds.data = self.get_dist_dict(self.df, pattern_idx=self.default_idx)
408
+ self.circle_cds.data = self.get_circle_dict(self.df)
409
+ # Remove callback and add old callback
410
+ if self.custom_update_plots in self.slider._callbacks["value"]:
411
+ self.slider.remove_on_change("value", self.custom_update_plots)
412
+ self.slider.on_change("value", self.update_plots)
413
+ self.slider.end = self.df.shape[0] - self.window
414
+ self.slider.value = self.default_idx
415
+
416
+ def get_data(self):
417
+ self.df = self.get_df_from_file()
418
+ self.default_idx = self.min_distance_idx
419
+ self.ts_cds = ColumnDataSource(self.get_ts_dict(self.df))
420
+ self.quad_cds = ColumnDataSource(
421
+ self.get_quad_dict(self.df, pattern_idx=self.default_idx)
422
+ )
423
+ self.pattern_match_cds = ColumnDataSource(
424
+ self.get_pattern_match_dict(self.df, pattern_idx=self.default_idx)
425
+ )
426
+ self.dist_cds = ColumnDataSource(
427
+ self.get_dist_dict(self.df, pattern_idx=self.default_idx)
428
+ )
429
+ self.circle_cds = ColumnDataSource(self.get_circle_dict(self.df))
430
+
431
+ def get_plots(self, ts_plot_color="black"):
432
+ self.ts_plot = self.get_ts_plot(color=ts_plot_color)
433
+ self.mp_plot = self.get_mp_plot()
434
+ self.pm_plot = self.get_pm_plot()
435
+
436
+ def get_widgets(self):
437
+ self.slider = self.get_slider(value=self.default_idx)
438
+ self.play_btn = self.get_play_button()
439
+ self.txt_inp = self.get_text_input()
440
+ self.pattern_btn, self.match_btn, self.reset_btn = self.get_buttons()
441
+ self.logo_div = self.get_logo_div()
442
+ self.heroku_div = self.get_heroku_div()
443
+
444
+ def set_callbacks(self):
445
+ self.slider.on_change("value", self.update_plots)
446
+ self.pattern_btn.on_click(self.show_hide_pattern)
447
+ self.show_hide_pattern()
448
+ self.match_btn.on_click(self.show_hide_match)
449
+ self.show_hide_match()
450
+ self.reset_btn.on_click(self.reset)
451
+ self.txt_inp.on_change("value", self.update_slider)
452
+
453
+ def get_layout(self):
454
+ self.get_data()
455
+ self.get_plots()
456
+ self.get_widgets()
457
+ self.set_callbacks()
458
+
459
+ l = layout(
460
+ [
461
+ [self.ts_plot],
462
+ [self.mp_plot],
463
+ [self.pm_plot],
464
+ [self.slider],
465
+ [self.pattern_btn, self.match_btn, self.play_btn, self.logo_div],
466
+ ],
467
+ sizing_mode=self.sizing_mode,
468
+ )
469
+
470
+ return l
471
+
472
+ def get_raw_layout(self):
473
+ self.get_data()
474
+ self.get_plots(ts_plot_color="#54b847")
475
+
476
+ l = layout([[self.ts_plot], [self.mp_plot]], sizing_mode=self.sizing_mode)
477
+
478
+ return l
479
+
480
+
481
+ dashboard = StumpyBokehDashboard()
482
+
483
+ def get_components(dashboard: StumpyBokehDashboard=dashboard):
484
+ dashboard.get_data()
485
+ dashboard.get_plots()
486
+ dashboard.get_widgets()
487
+ dashboard.set_callbacks()
488
+
489
+ logo = dashboard.logo_div
490
+ settings = layout(
491
+ dashboard.pattern_btn,
492
+ dashboard.match_btn,
493
+ dashboard.play_btn,
494
+ dashboard.slider,
495
+ height=150,
496
+ sizing_mode="stretch_width",
497
+ )
498
+ main = layout(
499
+ [
500
+ [dashboard.ts_plot],
501
+ [dashboard.mp_plot],
502
+ [dashboard.pm_plot],
503
+ ],
504
+ sizing_mode=dashboard.sizing_mode,
505
+ )
506
+ return logo, settings, main
507
+
508
+ pn.extension(template="fast")
509
+ pn.state.template.param.update(
510
+ site_url="https://awesome-panel.org",
511
+ site="Awesome Panel",
512
+ title="Stumpy Timeseries Analysis",
513
+ favicon="https://raw.githubusercontent.com/MarcSkovMadsen/awesome-panel-assets/320297ccb92773da099f6b97d267cc0433b67c23/favicon/ap-1f77b4.ico",
514
+ header_background="#459db9",
515
+ theme_toggle=False,
516
+ )
517
+
518
+ logo, settings, main = get_components()
519
+
520
+ pn.Column(
521
+ logo,
522
+ settings, sizing_mode="stretch_width",
523
+ ).servable(target="sidebar")
524
+ pn.panel(main, sizing_mode="stretch_both", max_height=800).servable(target="main")