arg-dashboard 0.1.19__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.
@@ -0,0 +1,412 @@
1
+ """
2
+ ARG Layout - Minimum Distance Constrained
3
+ ==========================================
4
+ Parents stay within children's x-range UNLESS moving outside is needed
5
+ to maintain minimum Euclidean distance (default 0.5) between nodes.
6
+ """
7
+
8
+ import numpy as np
9
+ from collections import defaultdict
10
+
11
+
12
+ def compute_arg_xpos(arg_data, min_distance=None, n_iterations=300):
13
+ """
14
+ Compute x-positions for an ARG with minimum distance constraint.
15
+
16
+ Parents stay within children's x-range unless they need to move
17
+ outside to maintain minimum Euclidean distance from other nodes.
18
+
19
+ Parameters
20
+ ----------
21
+ arg_data : dict
22
+ ARG data with 'Leaf', 'Coalescent', 'Recombination', and 'Lineage' keys
23
+ min_distance : float, optional
24
+ Minimum allowed Euclidean distance between any two nodes.
25
+ Default: 1/(2*n_samples) where n_samples is the number of leaves.
26
+ n_iterations : int
27
+ Number of iterations for constraint satisfaction
28
+
29
+ Returns
30
+ -------
31
+ dict
32
+ The input arg_data with updated 'xpos' values for all nodes
33
+ """
34
+ # Extract all nodes and heights
35
+ all_nodes = []
36
+ heights = {}
37
+
38
+ for leaf in arg_data['Leaf']:
39
+ nid = leaf['nodeid']
40
+ all_nodes.append(nid)
41
+ heights[nid] = leaf['height']
42
+
43
+ for coal in arg_data['Coalescent']:
44
+ nid = coal['nodeid']
45
+ all_nodes.append(nid)
46
+ heights[nid] = coal['height']
47
+
48
+ for recomb in arg_data['Recombination']:
49
+ nid = recomb['nodeid']
50
+ all_nodes.append(nid)
51
+ heights[nid] = recomb['height']
52
+
53
+ # Build edges from Lineage data
54
+ edges = []
55
+ for lineage in arg_data['Lineage']:
56
+ down = lineage['down']
57
+ up = lineage['up']
58
+ if up is not None:
59
+ edges.append((up, down))
60
+
61
+ # Build adjacency lists
62
+ children_map = defaultdict(list)
63
+ for parent, child in edges:
64
+ children_map[parent].append(child)
65
+
66
+ # Identify leaves
67
+ leaves = [n for n in all_nodes if heights[n] == 0]
68
+ n_leaves = len(leaves)
69
+
70
+ # x-range is [0, 1]
71
+ x_min, x_max = 0.0, 1.0
72
+
73
+ # Default min_distance = 1/(2*n_samples)
74
+ if min_distance is None:
75
+ min_distance = 1.0 / (2 * n_leaves)
76
+
77
+ # Group nodes by height
78
+ layers = defaultdict(list)
79
+ for node in all_nodes:
80
+ layers[heights[node]].append(node)
81
+ sorted_heights = sorted(layers.keys())
82
+
83
+ x_pos = {}
84
+
85
+ # Use original leaf positions if available (assuming they're in [0,1])
86
+ original_leaf_xpos = {}
87
+ for leaf in arg_data['Leaf']:
88
+ if 'xpos' in leaf and leaf['xpos'] is not None:
89
+ original_leaf_xpos[leaf['nodeid']] = leaf['xpos']
90
+
91
+ if original_leaf_xpos and len(original_leaf_xpos) == n_leaves:
92
+ for node in leaves:
93
+ x_pos[node] = original_leaf_xpos[node]
94
+ else:
95
+ # Default: space leaves evenly from 0 to 1
96
+ leaves_sorted = sorted(leaves)
97
+ for i, node in enumerate(leaves_sorted):
98
+ if n_leaves > 1:
99
+ x_pos[node] = i / (n_leaves - 1)
100
+ else:
101
+ x_pos[node] = 0.5
102
+
103
+ # Initialize internal nodes at children centroid
104
+ def get_child_centroid(node):
105
+ if children_map[node]:
106
+ child_xs = [x_pos[c] for c in children_map[node] if c in x_pos]
107
+ if child_xs:
108
+ return np.mean(child_xs)
109
+ return (x_min + x_max) / 2
110
+
111
+ def get_child_range(node):
112
+ """Get the x-range of immediate children."""
113
+ if children_map[node]:
114
+ child_xs = [x_pos[c] for c in children_map[node] if c in x_pos]
115
+ if child_xs:
116
+ return min(child_xs), max(child_xs)
117
+ return x_min, x_max
118
+
119
+ for h in sorted_heights:
120
+ if h == 0:
121
+ continue
122
+ for node in layers[h]:
123
+ x_pos[node] = get_child_centroid(node)
124
+
125
+ def euclidean_dist(n1, n2):
126
+ dx = x_pos[n1] - x_pos[n2]
127
+ dy = heights[n1] - heights[n2]
128
+ return np.sqrt(dx**2 + dy**2)
129
+
130
+ # Track which nodes are "released" to go outside children range
131
+ released = set()
132
+
133
+ # Iterative constraint satisfaction
134
+ for iteration in range(n_iterations):
135
+ lr = 0.4 * (1 - 0.5 * iteration / n_iterations)
136
+
137
+ # Find all distance violations
138
+ violations = []
139
+ for i, n1 in enumerate(all_nodes):
140
+ for n2 in all_nodes[i+1:]:
141
+ dist = euclidean_dist(n1, n2)
142
+ if dist < min_distance:
143
+ violations.append((n1, n2, dist))
144
+
145
+ if not violations:
146
+ break # All constraints satisfied
147
+
148
+ # Compute repulsion forces only for violating pairs
149
+ forces = defaultdict(float)
150
+
151
+ for n1, n2, dist in violations:
152
+ dx = x_pos[n1] - x_pos[n2]
153
+
154
+ # Deficit: how much more distance we need
155
+ deficit = min_distance - dist
156
+
157
+ # Direction to push
158
+ if abs(dx) < 0.001:
159
+ direction = 1 if n1 < n2 else -1
160
+ else:
161
+ direction = np.sign(dx)
162
+
163
+ # Force magnitude proportional to deficit
164
+ force_mag = deficit * 0.3
165
+
166
+ # Apply force to non-leaf nodes
167
+ if heights[n1] > 0:
168
+ forces[n1] += force_mag * direction
169
+ if heights[n2] > 0:
170
+ forces[n2] -= force_mag * direction
171
+
172
+ # Apply forces with conditional constraints
173
+ for node, force in forces.items():
174
+ if heights[node] == 0:
175
+ continue
176
+
177
+ new_x = x_pos[node] + lr * force
178
+ child_lo, child_hi = get_child_range(node)
179
+
180
+ # Check if new position would be within children's range
181
+ if child_lo <= new_x <= child_hi:
182
+ # Stay within range - no problem
183
+ x_pos[node] = new_x
184
+ else:
185
+ # Would go outside - check if we NEED to
186
+ # Release node if it still has violations at the boundary
187
+ clipped_x = np.clip(new_x, child_lo, child_hi)
188
+ temp_x = x_pos[node]
189
+ x_pos[node] = clipped_x
190
+
191
+ # Check if any violations remain
192
+ still_violated = False
193
+ for n1, n2, _ in violations:
194
+ if node in (n1, n2):
195
+ if euclidean_dist(n1, n2) < min_distance:
196
+ still_violated = True
197
+ break
198
+
199
+ if still_violated:
200
+ # Need to go outside children's range
201
+ released.add(node)
202
+ x_pos[node] = np.clip(new_x, x_min, x_max)
203
+ else:
204
+ x_pos[node] = clipped_x
205
+
206
+ # For released nodes, allow them to move freely
207
+ for node in released:
208
+ if node in forces:
209
+ new_x = x_pos[node] + lr * forces[node]
210
+ x_pos[node] = np.clip(new_x, x_min, x_max)
211
+
212
+ # Weak centering force to children centroid (only for unreleased nodes)
213
+ for node in all_nodes:
214
+ if heights[node] > 0 and node not in released:
215
+ if children_map[node]:
216
+ centroid = get_child_centroid(node)
217
+ x_pos[node] += 0.02 * (centroid - x_pos[node])
218
+
219
+ # Update the JSON data
220
+ for leaf in arg_data['Leaf']:
221
+ leaf['xpos'] = float(x_pos[leaf['nodeid']])
222
+
223
+ for coal in arg_data['Coalescent']:
224
+ coal['xpos'] = float(x_pos[coal['nodeid']])
225
+
226
+ for recomb in arg_data['Recombination']:
227
+ recomb['xpos'] = float(x_pos[recomb['nodeid']])
228
+
229
+ return arg_data
230
+
231
+
232
+ def count_crossings(arg_data):
233
+ """Count edge crossings in the layout."""
234
+ heights = {}
235
+ x_pos = {}
236
+
237
+ for leaf in arg_data['Leaf']:
238
+ nid = leaf['nodeid']
239
+ heights[nid] = leaf['height']
240
+ x_pos[nid] = leaf['xpos']
241
+
242
+ for coal in arg_data['Coalescent']:
243
+ nid = coal['nodeid']
244
+ heights[nid] = coal['height']
245
+ x_pos[nid] = coal['xpos']
246
+
247
+ for recomb in arg_data['Recombination']:
248
+ nid = recomb['nodeid']
249
+ heights[nid] = recomb['height']
250
+ x_pos[nid] = recomb['xpos']
251
+
252
+ edges = []
253
+ for lineage in arg_data['Lineage']:
254
+ if lineage['up'] is not None:
255
+ edges.append((lineage['up'], lineage['down']))
256
+
257
+ crossings = 0
258
+ for i, (p1, c1) in enumerate(edges):
259
+ for p2, c2 in edges[i+1:]:
260
+ if p1 == p2 or c1 == c2 or p1 == c2 or p2 == c1:
261
+ continue
262
+ h1_top, h1_bot = heights[p1], heights[c1]
263
+ h2_top, h2_bot = heights[p2], heights[c2]
264
+ if max(h1_bot, h2_bot) >= min(h1_top, h2_top):
265
+ continue
266
+ if (x_pos[p1] - x_pos[p2]) * (x_pos[c1] - x_pos[c2]) < 0:
267
+ crossings += 1
268
+
269
+ return crossings
270
+
271
+
272
+ def check_min_distances(arg_data):
273
+ """Check minimum distances between all node pairs."""
274
+ heights = {}
275
+ x_pos = {}
276
+
277
+ for leaf in arg_data['Leaf']:
278
+ nid = leaf['nodeid']
279
+ heights[nid] = leaf['height']
280
+ x_pos[nid] = leaf['xpos']
281
+
282
+ for coal in arg_data['Coalescent']:
283
+ nid = coal['nodeid']
284
+ heights[nid] = coal['height']
285
+ x_pos[nid] = coal['xpos']
286
+
287
+ for recomb in arg_data['Recombination']:
288
+ nid = recomb['nodeid']
289
+ heights[nid] = recomb['height']
290
+ x_pos[nid] = recomb['xpos']
291
+
292
+ all_nodes = list(x_pos.keys())
293
+ min_dist = float('inf')
294
+ min_pair = None
295
+
296
+ for i, n1 in enumerate(all_nodes):
297
+ for n2 in all_nodes[i+1:]:
298
+ dx = x_pos[n1] - x_pos[n2]
299
+ dy = heights[n1] - heights[n2]
300
+ dist = np.sqrt(dx**2 + dy**2)
301
+ if dist < min_dist:
302
+ min_dist = dist
303
+ min_pair = (n1, n2)
304
+
305
+ return {'min_distance': min_dist, 'min_pair': min_pair}
306
+
307
+
308
+ def check_parents_outside_children(arg_data):
309
+ """Find parents positioned outside their children's x-range."""
310
+ x_pos = {}
311
+ for leaf in arg_data['Leaf']:
312
+ x_pos[leaf['nodeid']] = leaf['xpos']
313
+ for coal in arg_data['Coalescent']:
314
+ x_pos[coal['nodeid']] = coal['xpos']
315
+ for recomb in arg_data['Recombination']:
316
+ x_pos[recomb['nodeid']] = recomb['xpos']
317
+
318
+ children_map = defaultdict(list)
319
+ for lineage in arg_data['Lineage']:
320
+ if lineage['up'] is not None:
321
+ children_map[lineage['up']].append(lineage['down'])
322
+
323
+ outside = []
324
+ for node, children in children_map.items():
325
+ if children:
326
+ child_xs = [x_pos[c] for c in children]
327
+ lo, hi = min(child_xs), max(child_xs)
328
+ node_x = x_pos[node]
329
+ if node_x < lo - 0.001 or node_x > hi + 0.001:
330
+ outside.append({
331
+ 'node': node,
332
+ 'x': node_x,
333
+ 'children_range': (lo, hi)
334
+ })
335
+
336
+ return outside
337
+
338
+
339
+ if __name__ == '__main__':
340
+ import copy
341
+
342
+ arg_json = {
343
+ "Coalescent": [
344
+ {"nodeid": 5, "height": 0.137, "xpos": 0.125},
345
+ {"nodeid": 7, "height": 0.390, "xpos": 0.5125},
346
+ {"nodeid": 8, "height": 0.550, "xpos": 0.606},
347
+ {"nodeid": 10, "height": 0.737, "xpos": 0.578},
348
+ {"nodeid": 11, "height": 0.991, "xpos": 0.312},
349
+ {"nodeid": 12, "height": 1.0, "xpos": 0.445}
350
+ ],
351
+ "Recombination": [
352
+ {"nodeid": 6, "height": 0.332, "xpos": 0.125},
353
+ {"nodeid": 9, "height": 0.593, "xpos": 0.5}
354
+ ],
355
+ "Leaf": [
356
+ {"nodeid": 0, "height": 0.0, "xpos": 0.5},
357
+ {"nodeid": 1, "height": 0.0, "xpos": 0.0},
358
+ {"nodeid": 2, "height": 0.0, "xpos": 0.25},
359
+ {"nodeid": 3, "height": 0.0, "xpos": 1.0},
360
+ {"nodeid": 4, "height": 0.0, "xpos": 0.75}
361
+ ],
362
+ "Lineage": [
363
+ {"lineageid": 0, "down": 0, "up": 9},
364
+ {"lineageid": 1, "down": 1, "up": 5},
365
+ {"lineageid": 2, "down": 2, "up": 5},
366
+ {"lineageid": 3, "down": 3, "up": 8},
367
+ {"lineageid": 4, "down": 4, "up": 7},
368
+ {"lineageid": 5, "down": 5, "up": 6},
369
+ {"lineageid": 6, "down": 6, "up": 11},
370
+ {"lineageid": 7, "down": 6, "up": 7},
371
+ {"lineageid": 8, "down": 7, "up": 8},
372
+ {"lineageid": 9, "down": 8, "up": 10},
373
+ {"lineageid": 10, "down": 9, "up": 10},
374
+ {"lineageid": 11, "down": 9, "up": 11},
375
+ {"lineageid": 12, "down": 10, "up": 12},
376
+ {"lineageid": 13, "down": 11, "up": 12},
377
+ {"lineageid": 14, "down": 12, "up": None}
378
+ ]
379
+ }
380
+
381
+ n_samples = len(arg_json['Leaf'])
382
+ default_min_dist = 1.0 / (2 * n_samples)
383
+
384
+ print(f"=== Minimum Distance Layout ===")
385
+ print(f"n_samples = {n_samples}")
386
+ print(f"x-range = [0, 1]")
387
+ print(f"default min_distance = 1/(2*{n_samples}) = {default_min_dist:.3f}")
388
+ print()
389
+
390
+ # With default min_distance
391
+ result = compute_arg_xpos(copy.deepcopy(arg_json))
392
+ result_info = check_min_distances(result)
393
+ outside = check_parents_outside_children(result)
394
+
395
+ print(f"Result with default min_distance={default_min_dist:.3f}:")
396
+ print(f" Actual min distance: {result_info['min_distance']:.3f} (nodes {result_info['min_pair']})")
397
+ print(f" Crossings: {count_crossings(result)}")
398
+ print(f" Parents outside children: {len(outside)}")
399
+ for o in outside:
400
+ print(f" Node {o['node']}: x={o['x']:.3f}, children=[{o['children_range'][0]:.3f}, {o['children_range'][1]:.3f}]")
401
+
402
+ print(f"\nLeaf positions: ", end="")
403
+ for leaf in sorted(result['Leaf'], key=lambda x: x['nodeid']):
404
+ print(f"{leaf['nodeid']}:{leaf['xpos']:.2f}", end=" ")
405
+ print()
406
+
407
+ print(f"Internal positions: ", end="")
408
+ for coal in sorted(result['Coalescent'], key=lambda x: x['nodeid']):
409
+ print(f"{coal['nodeid']}:{coal['xpos']:.2f}", end=" ")
410
+ for recomb in sorted(result['Recombination'], key=lambda x: x['nodeid']):
411
+ print(f"{recomb['nodeid']}:{recomb['xpos']:.2f}", end=" ")
412
+ print()
@@ -0,0 +1,139 @@
1
+ body {
2
+ font-size: 0.85rem;
3
+ /* background-color: #f9f9f9; */
4
+ /* background-color: #f1f1f1; */
5
+ /* background-color: #e7eff6; */
6
+ background-color: white;
7
+ /* background-color: #f9fcfe; */
8
+ font-family: "Asap", sans-serif;
9
+ /* margin: .5rem; */
10
+ }
11
+
12
+ .rc-slider-track {
13
+ background-color: #007bff;
14
+ }
15
+
16
+ .rc-slider-dot-active {
17
+ border-color: #007bff;
18
+ border: solid 2px #007bff;
19
+ }
20
+
21
+ .rc-slider-handle {
22
+ background-color: #007bff;
23
+ border-color: #007bff;
24
+ }
25
+ window_data.to_hdf('window_data.h5', 'df', format='table', mode='w')
26
+ .rc.slider-handle:hover {
27
+ border-color: #007bff;
28
+ }
29
+
30
+ .rc.slider-handle-active:active {
31
+ border-color: red;
32
+ }
33
+
34
+ .pretty_container {
35
+
36
+ border-radius: 0px;
37
+ background-color: white;
38
+ /* margin: 1.5rem; */
39
+ padding: 1rem;
40
+ /* position: relative; */
41
+ /* border: 1px solid #f1f1f1; */
42
+ /* border: 1px solid #e1e1e1; */
43
+ /* border: 1px solid #999999; */
44
+ border: 1px solid #999999;
45
+
46
+ /*
47
+ padding: 1rem;
48
+ border: 1px solid #ffffff;
49
+ -webkit-box-shadow: 0px 1px 5px 1px #ccc;
50
+ -moz-box-shadow: 0px 1px 5px 1px #ccc;
51
+ box-shadow: 0px 1px 5px 1px #ccc;
52
+ */
53
+
54
+
55
+ /* Buttons
56
+ –––––––––––––––––––––––––––––––––––––––––––––––––– */
57
+ /* .button,
58
+ button,
59
+ input[type="submit"],
60
+ input[type="reset"],
61
+ input[type="button"] {
62
+ display: inline-block;
63
+ height: 35px;
64
+ padding: 0 15px;
65
+ color: #555;
66
+ text-align: center;
67
+ font-size: 11px;
68
+ font-weight: 600;
69
+ line-height: 35px;
70
+ letter-spacing: .1rem;
71
+ text-transform: uppercase;
72
+ text-decoration: none;
73
+ white-space: nowrap;
74
+ background-color: transparent;
75
+ border-radius: 4px;
76
+ border: 1px solid #bbb;
77
+ cursor: pointer;
78
+ box-sizing: border-box; }
79
+ .button:hover,
80
+ button:hover,
81
+ input[type="submit"]:hover,
82
+ input[type="reset"]:hover,
83
+ input[type="button"]:hover,
84
+ .button:focus,
85
+ button:focus,
86
+ input[type="submit"]:focus,
87
+ input[type="reset"]:focus,
88
+ input[type="button"]:focus {
89
+ color: #333;
90
+ border-color: #888;
91
+ outline: 0; }
92
+ .button.button-primary,
93
+ button.button-primary,
94
+ input[type="submit"].button-primary,
95
+ input[type="reset"].button-primary,
96
+ input[type="button"].button-primary {
97
+ color: rgb(145, 9, 9);
98
+ background-color: #33C3F0;
99
+ border-color: #33C3F0; }
100
+ .button.button-primary:hover,
101
+ button.button-primary:hover,
102
+ input[type="submit"].button-primary:hover,
103
+ input[type="reset"].button-primary:hover,
104
+ input[type="button"].button-primary:hover,
105
+ .button.button-primary:focus,
106
+ button.button-primary:focus,
107
+ input[type="submit"].button-primary:focus,
108
+ input[type="reset"].button-primary:focus,
109
+ input[type="button"].button-primary:focus {
110
+ color: #FFF;
111
+ background-color: #1EAEDB;
112
+ border-color: #1EAEDB; } */
113
+
114
+
115
+ /* @import "bootstrap";
116
+
117
+ $mynewcolor:teal;
118
+
119
+ .btn-primary {
120
+ @include button-variant($mynewcolor, darken($mynewcolor, 7.5%), darken($mynewcolor, 10%), lighten($mynewcolor,5%), lighten($mynewcolor, 10%), darken($mynewcolor,30%));
121
+ }
122
+
123
+ .btn-outline-primary {
124
+ @include button-outline-variant($mynewcolor, #222222, lighten($mynewcolor,5%), $mynewcolor);
125
+ } */ */
126
+ /*
127
+ .btn-primary{color:#fff;background-color:teal;border-color:#005a5a}
128
+
129
+ .btn-primary:hover{color:#fff;background-color:#004d4d;border-color:#009a9a}
130
+ .btn-primary:focus,.btn-primary.focus{box-shadow:0 0 0 .2rem rgba(0,90,90,0.5)}
131
+ .btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:teal;border-color:#005a5a}
132
+ .btn-primary:not(:disabled):not(.disabled):active,.btn-primary:not(:disabled):not(.disabled).active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#00b3b3;border-color:#000}
133
+ .btn-primary:not(:disabled):not(.disabled):active:focus,.btn-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,90,90,0.5)}
134
+
135
+ .btn-outline-primary{color:teal;background-color:transparent;background-image:none;border-color:teal}.btn-outline-primary:hover{color:#222;background-color:#009a9a;border-color:teal}
136
+ .btn-outline-primary:focus,.btn-outline-primary.focus{box-shadow:0 0 0 .2rem rgba(0,128,128,0.5)}
137
+ .btn-outline-primary.disabled,.btn-outline-primary:disabled{color:teal;background-color:transparent}
138
+ .btn-outline-primary:not(:disabled):not(.disabled):active,.btn-outline-primary:not(:disabled):not(.disabled).active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#009a9a;border-color:teal}
139
+ .btn-outline-primary:not(:disabled):not(.disabled):active:focus,.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,128,128,0.5)} */
Binary file
@@ -0,0 +1,34 @@
1
+ .custom-tabs-container {
2
+ width: 85%;
3
+ }
4
+ .custom-tabs {
5
+ border-top-left-radius: 3px;
6
+ background-color: #f9f9f9;
7
+ padding: 0px 24px;
8
+ border-bottom: 1px solid #d6d6d6;
9
+ }
10
+
11
+ .custom-tab {
12
+ width: 20px;
13
+ color:#586069;
14
+ border-top-left-radius: 15px;
15
+ border-top-right-radius: 5px;
16
+ border-top: 3px solid transparent !important;
17
+ border-left: 0px !important;
18
+ border-right: 0px !important;
19
+ border-bottom: 0px !important;
20
+ background-color: #fafbfc;
21
+ padding: 12px !important;
22
+ font-family: "system-ui";
23
+ display: flex !important;
24
+ align-items: center;
25
+ justify-content: center;
26
+ }
27
+ .custom-tab--selected {
28
+ color: black;
29
+ box-shadow: 1px 1px 0px white;
30
+ border-left: 1px solid lightgrey !important;
31
+ border-right: 1px solid lightgrey !important;
32
+ border-top: 1px solid lightgrey !important;
33
+
34
+ }
arg_dashboard/index.py ADDED
@@ -0,0 +1,6 @@
1
+
2
+
3
+ from app import app
4
+
5
+ if __name__ == '__main__':
6
+ app.run_server(debug=True)
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: arg-dashboard
3
+ Version: 0.1.19
4
+ Summary: Project description
5
+ Author-email: Your Name <your@email.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/munch-group/arg-dashboard
8
+ Project-URL: Repository, https://github.com/munch-group/arg-dashboard
9
+ Project-URL: Documentation, https://munch-group.org/arg-dashboard
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: <3.14,>=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: ipykernel
16
+ Requires-Dist: jupyter
17
+ Requires-Dist: dash
18
+ Requires-Dist: dash-bootstrap-components
19
+ Requires-Dist: pandas
20
+ Requires-Dist: networkx
21
+ Requires-Dist: jupyter-dash
22
+ Requires-Dist: jupyter-server-proxy<5,>=4.4
23
+ Requires-Dist: plotly
24
+ Requires-Dist: seaborn
25
+ Requires-Dist: quartodoc
26
+ Requires-Dist: quarto
27
+ Requires-Dist: pip
28
+ Requires-Dist: pytest
29
+ Dynamic: license-file
30
+
31
+
32
+ # Template repository for a library project
33
+
34
+ ## Initial set up
35
+
36
+ ```bash
37
+ pixi run init
38
+ ```
39
+
40
+ ## Get updates to upstream fork
41
+
42
+ Add upstream if not already added
43
+
44
+ ```bash
45
+ git remote add upstream https://github.com/munch-group/arg-dashboard.git
46
+ ```
47
+
48
+ Fetch upstream changes
49
+
50
+ ```bash
51
+ git fetch upstream
52
+ ```
53
+
54
+ Either rebase your changes on top of upstream (cleaner history)
55
+
56
+ ```bash
57
+ git rebase upstream/main
58
+ ```
59
+
60
+ Or, merge upstream into your fork (preserves history)
61
+
62
+ ```bash
63
+ git merge upstream/main
64
+ ```
65
+
66
+ If you want to see what's changed upstream before applying:
67
+
68
+ ```bash
69
+ git log HEAD..upstream/main
70
+ ```
71
+
72
+ See the actual diff
73
+
74
+ ```bash
75
+ git diff HEAD...upstream/main
76
+ ```
77
+
78
+ Then push your updated fork:
79
+
80
+ ```bash
81
+ git push origin main
82
+ ```
83
+
84
+ If you rebased and need to force push
85
+
86
+ ```bash
87
+ git push origin main --force-with-lease
88
+ ```