scout-browser 4.85__py3-none-any.whl → 4.86__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. scout/__version__.py +1 -1
  2. scout/adapter/mongo/base.py +17 -14
  3. scout/adapter/mongo/case.py +20 -1
  4. scout/adapter/mongo/filter.py +36 -1
  5. scout/adapter/mongo/omics_variant.py +145 -0
  6. scout/adapter/mongo/query.py +13 -3
  7. scout/adapter/mongo/variant.py +10 -4
  8. scout/build/case.py +5 -0
  9. scout/build/variant/variant.py +1 -0
  10. scout/constants/__init__.py +3 -1
  11. scout/constants/case_tags.py +1 -0
  12. scout/constants/clinvar.py +1 -1
  13. scout/constants/file_types.py +31 -0
  14. scout/constants/filters.py +4 -0
  15. scout/constants/indexes.py +30 -13
  16. scout/constants/variant_tags.py +3 -0
  17. scout/demo/643594.clinical.mei.vcf.gz +0 -0
  18. scout/demo/643594.clinical.mei.vcf.gz.tbi +0 -0
  19. scout/demo/643594.config.yaml +4 -0
  20. scout/demo/drop/fraser_top_hits_clinical.tsv +5 -0
  21. scout/demo/drop/outrider_top_hits_clinical.tsv +10 -0
  22. scout/load/setup.py +4 -4
  23. scout/models/case/case_loading_models.py +25 -2
  24. scout/models/omics_variant.py +227 -0
  25. scout/parse/omics_variant/__init__.py +11 -0
  26. scout/parse/omics_variant/drop.py +19 -0
  27. scout/parse/variant/callers.py +6 -3
  28. scout/parse/variant/frequency.py +10 -2
  29. scout/server/app.py +4 -1
  30. scout/server/blueprints/alignviewers/controllers.py +35 -24
  31. scout/server/blueprints/alignviewers/templates/alignviewers/igv_sashimi_viewer.html +19 -15
  32. scout/server/blueprints/alignviewers/templates/alignviewers/igv_viewer.html +45 -5
  33. scout/server/blueprints/alignviewers/templates/alignviewers/utils.html +1 -1
  34. scout/server/blueprints/alignviewers/views.py +10 -2
  35. scout/server/blueprints/cases/controllers.py +3 -0
  36. scout/server/blueprints/cases/templates/cases/case.html +27 -9
  37. scout/server/blueprints/cases/templates/cases/case_report.html +2 -17
  38. scout/server/blueprints/cases/templates/cases/phenotype.html +8 -5
  39. scout/server/blueprints/cases/templates/cases/utils.html +26 -3
  40. scout/server/blueprints/clinvar/controllers.py +9 -3
  41. scout/server/blueprints/dashboard/controllers.py +44 -13
  42. scout/server/blueprints/dashboard/static/charts.js +46 -36
  43. scout/server/blueprints/dashboard/templates/dashboard/dashboard_general.html +2 -2
  44. scout/server/blueprints/institutes/forms.py +2 -0
  45. scout/server/blueprints/institutes/templates/overview/cases.html +6 -4
  46. scout/server/blueprints/institutes/templates/overview/gene_variants.html +40 -27
  47. scout/server/blueprints/institutes/templates/overview/institute_sidebar.html +1 -1
  48. scout/server/blueprints/institutes/views.py +5 -12
  49. scout/server/blueprints/omics_variants/__init__.py +1 -0
  50. scout/server/blueprints/omics_variants/controllers.py +122 -0
  51. scout/server/blueprints/omics_variants/templates/omics_variants/outliers.html +262 -0
  52. scout/server/blueprints/omics_variants/views.py +106 -0
  53. scout/server/blueprints/panels/controllers.py +1 -7
  54. scout/server/blueprints/panels/templates/panels/panels.html +12 -4
  55. scout/server/blueprints/panels/views.py +9 -11
  56. scout/server/blueprints/variant/templates/variant/buttons.html +7 -2
  57. scout/server/blueprints/variant/templates/variant/str-variant-reviewer.html +1 -1
  58. scout/server/blueprints/variant/templates/variant/utils.html +1 -1
  59. scout/server/blueprints/variant/utils.py +54 -103
  60. scout/server/blueprints/variant/views.py +1 -0
  61. scout/server/blueprints/variants/controllers.py +1 -4
  62. scout/server/blueprints/variants/forms.py +42 -0
  63. scout/server/blueprints/variants/templates/variants/utils.html +8 -4
  64. scout/server/blueprints/variants/views.py +28 -7
  65. scout/server/config.py +4 -0
  66. scout/server/extensions/clinvar_extension.py +7 -7
  67. scout/server/links.py +2 -2
  68. scout/server/templates/bootstrap_global.html +1 -4
  69. scout/server/templates/utils.html +3 -3
  70. scout/server/utils.py +4 -1
  71. {scout_browser-4.85.dist-info → scout_browser-4.86.dist-info}/METADATA +10 -10
  72. {scout_browser-4.85.dist-info → scout_browser-4.86.dist-info}/RECORD +76 -66
  73. {scout_browser-4.85.dist-info → scout_browser-4.86.dist-info}/WHEEL +1 -1
  74. {scout_browser-4.85.dist-info → scout_browser-4.86.dist-info}/LICENSE +0 -0
  75. {scout_browser-4.85.dist-info → scout_browser-4.86.dist-info}/entry_points.txt +0 -0
  76. {scout_browser-4.85.dist-info → scout_browser-4.86.dist-info}/top_level.txt +0 -0
@@ -14,14 +14,11 @@
14
14
  <link rel="stylesheet" type="text/css"
15
15
  href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/themes/smoothness/jquery-ui.css"/>
16
16
 
17
- <!-- Font Awesome CSS -->
18
- <link rel="stylesheet" type="text/css"
19
- href="https://maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css"/>
20
-
21
17
  <!-- jQuery JS -->
22
18
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
23
19
  <script type="text/javascript"
24
20
  src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.js"></script>
21
+
25
22
  {{ igv_script() }}
26
23
  </head>
27
24
 
@@ -30,6 +27,11 @@
30
27
  </body>
31
28
 
32
29
  <script type="text/javascript">
30
+
31
+ var outLinks = {
32
+ COSV: "https://cancer.sanger.ac.uk/cosmic/search?q=", // COSMIC variant search
33
+ };
34
+
33
35
  $(document).ready(function () {
34
36
  var div = $("#igvDiv")[0],
35
37
  options = {
@@ -146,7 +148,45 @@
146
148
  {% endfor %}
147
149
  ]
148
150
  };
149
- igv.createBrowser(div, options);
151
+ igv.createBrowser(div, options)
152
+ .then(function (browser) {
153
+ browser.on('trackclick', function (track, popoverData) {
154
+
155
+ var markup = "<table class=\"igv-popover-table\">";
156
+
157
+ // Don't show a pop-over when there's no data.
158
+ if (!popoverData || !popoverData.length) {
159
+ return false;
160
+ }
161
+
162
+ popoverData.forEach(function (nameValue) {
163
+
164
+ if (nameValue.name) {
165
+
166
+ var value = nameValue.value;
167
+ for (var key in outLinks) {
168
+ if (nameValue.value.toString().startsWith(key) && nameValue.name.toLowerCase() === 'name') {
169
+ value = '<a href="' + outLinks[key] + value + '" target="_blank" rel="noopener noreferrer">' + nameValue.value + '</a>';
170
+ }
171
+ }
172
+
173
+ markup += "<tr><td class=\"igv-popover-td\">"
174
+ + "<span class=\"igv-popover-name\">" + nameValue.name + "</span>&nbsp;"
175
+ + "<span class=\"igv-popover-value\">" + value + "</span>"
176
+ + "</td></tr>";
177
+ }
178
+ else {
179
+ // not a name/value pair
180
+ markup += "<tr><td>" + nameValue.toString() + "</td></tr>";
181
+ }
182
+ });
183
+
184
+ markup += "</table>";
185
+
186
+ // By returning a string from the trackclick handler we're asking IGV to use our custom HTML in its pop-over.
187
+ return markup;
188
+ });
189
+ });
150
190
  });
151
191
  </script>
152
192
 
@@ -1,5 +1,5 @@
1
1
  {% macro igv_script() %}
2
2
  <link rel="shortcut icon" href="//igv.org/web/img/favicon.ico">
3
3
  <!-- IGV JS-->
4
- <script src="https://cdn.jsdelivr.net/npm/igv@2.15.11/dist/igv.min.js"></script>
4
+ <script src="https://cdn.jsdelivr.net/npm/igv@3.0.1/dist/igv.min.js"></script>
5
5
  {% endmacro %}
@@ -91,7 +91,15 @@ def remote_static():
91
91
  @alignviewers_bp.route(
92
92
  "/<institute_id>/<case_name>/<variant_id>/igv-splice-junctions", methods=["GET"]
93
93
  )
94
- def sashimi_igv(institute_id, case_name, variant_id=None):
94
+ @alignviewers_bp.route(
95
+ "/<institute_id>/<case_name>/outliers/<omics_variant_id>/igv-splice-junctions", methods=["GET"]
96
+ )
97
+ def sashimi_igv(
98
+ institute_id: str,
99
+ case_name: str,
100
+ variant_id: Optional[str] = None,
101
+ omics_variant_id: Optional[str] = None,
102
+ ):
95
103
  """Visualize splice junctions on igv.js sashimi-like viewer for one or more individuals of a case.
96
104
  wiki: https://github.com/igvteam/igv.js/wiki/Splice-Junctions
97
105
  """
@@ -99,7 +107,7 @@ def sashimi_igv(institute_id, case_name, variant_id=None):
99
107
  store, institute_id, case_name
100
108
  ) # This function takes care of checking if user is authorized to see resource
101
109
 
102
- display_obj = controllers.make_sashimi_tracks(case_obj, variant_id)
110
+ display_obj = controllers.make_sashimi_tracks(case_obj, variant_id, omics_variant_id)
103
111
  controllers.set_session_tracks(display_obj)
104
112
 
105
113
  response = Response(render_template("alignviewers/igv_sashimi_viewer.html", **display_obj))
@@ -452,6 +452,9 @@ def case(
452
452
  "gens_info": gens.connection_settings(case_obj.get("genome_build")),
453
453
  "display_rerunner": rerunner.connection_settings.get("display", False),
454
454
  "hide_matching": hide_matching,
455
+ "audits": store.case_events_by_verb(
456
+ category="case", institute=institute_obj, case=case_obj, verb="filter_audit"
457
+ ),
455
458
  }
456
459
 
457
460
  return data
@@ -1,7 +1,7 @@
1
1
  {% extends "layout.html" %}
2
2
  {% from "cases/collapsible_actionbar.html" import action_bar, research_modal, reanalysis_modal %}
3
3
  {% from "utils.html" import comments_panel, activity_panel, pedigree_panel %}
4
- {% from "cases/utils.html" import causatives_list, suspects_list, remove_form, matching_causatives, matching_managed_variants, beacon_modal, matchmaker_modal %}
4
+ {% from "cases/utils.html" import causatives_list, suspects_list, remove_form, matching_causatives, matching_managed_variants, beacon_modal, matchmaker_modal, filter_audits %}
5
5
  {% from "cases/individuals_table.html" import cancer_individuals_table, individuals_table %}
6
6
  {% from "cases/phenotype.html" import hpo_item, cohort_panel, diagnosis_phenotypes, phenotype_groups_panel, add_phenotype_terms_panel, hpo_panel %}
7
7
  {% from "cases/gene_panel.html" import genepanels_table, hpo_genelist_panel %}
@@ -119,8 +119,16 @@
119
119
  </div>
120
120
  </div>
121
121
 
122
+ <div class="row ms-1">
122
123
  {{ matching_variants() }}
123
124
 
125
+ {% if audits | count_cursor > 0 %}
126
+ <div class="card panel-default col-4 ms-5 me-1">
127
+ {{ filter_audits(audits, true) }}
128
+ </div>
129
+ {% endif %}
130
+ </div>
131
+
124
132
  <div class="card panel-default" >
125
133
  <div class="row">
126
134
  <div class="col">{{ causatives_list(causatives, partial_causatives, evaluated_variants, institute, case, manual_rank_options, cancer_tier_options) }}</div>
@@ -258,6 +266,9 @@
258
266
  {% if case.vcf_files.vcf_fusion %}
259
267
  <a class="btn btn-dark btn-sm text-white" href="{{ url_for('variants.fusion_variants', institute_id=institute._id, case_name=case.display_name, variant_type='clinical') }}">Clinical fusion variants</a>
260
268
  {% endif %}
269
+ {% if case.has_outliers %}
270
+ <a class="btn btn-dark btn-sm text-white" href="{{ url_for('omics_variants.outliers', institute_id=institute._id, case_name=case.display_name, variant_type='clinical') }}">Clinical WTS outliers</a>
271
+ {% endif %}
261
272
  </div>
262
273
  </div>
263
274
  </div>
@@ -537,9 +548,9 @@
537
548
  {% endmacro %}
538
549
 
539
550
  {% macro matching_variants() %}
540
- <div class="card mt-3">
541
- <div class="row mt-0 ms-3">
542
- <div class="col-sm-12 col-md-12">
551
+ <div class="card mt-3 col-7">
552
+ <div class="mt-0">
553
+ <div class="col-md-7">
543
554
  <div data-bs-toggle='tooltip' class="panel-heading" title="Check if there are any variants in this case
544
555
  marked as causative in another case for this institute, or are on the managed variants list.">
545
556
  <strong>
@@ -554,27 +565,27 @@
554
565
  {% if hide_matching == false %}
555
566
  {% if other_causatives|length > 0 %}
556
567
  <div class="row mt-0 ms-3">
557
- <div class="col-xs-12 col-md-12">{{ matching_causatives(other_causatives, institute, case) }}</div>
568
+ <div class="col-md-7">{{ matching_causatives(other_causatives, institute, case) }}</div>
558
569
  </div>
559
570
  {% endif %}
560
571
  {% if default_other_causatives|length > 0%}
561
572
  <div class="row mt-0 ms-3">
562
- <div class="col-xs-12 col-md-12">{{ matching_causatives(default_other_causatives, institute, case, default=True) }}</div>
573
+ <div class="col-md-7">{{ matching_causatives(default_other_causatives, institute, case, default=True) }}</div>
563
574
  </div>
564
575
  {% endif %}
565
576
  {% if managed_variants|length > 0%}
566
577
  <div class="row mt-0 ms-3">
567
- <div class="col-sm-12 col-md-12">{{ matching_managed_variants(managed_variants, institute, case) }}</div>
578
+ <div class="col-md-7">{{ matching_managed_variants(managed_variants, institute, case) }}</div>
568
579
  </div>
569
580
  {% endif %}
570
581
  {% if default_managed_variants|length > 0%}
571
582
  <div class="row mt-0 ms-3">
572
- <div class="col-sm-12 col-md-12">{{ matching_managed_variants(default_managed_variants, institute, case, default=True) }}</div>
583
+ <div class="col-md-7">{{ matching_managed_variants(default_managed_variants, institute, case, default=True) }}</div>
573
584
  </div>
574
585
  {% endif %}
575
586
  {% if other_causatives|length == 0 and default_other_causatives|length == 0 and managed_variants|length == 0 and default_managed_variants|length == 0%}
576
587
  <div class="row mt-0 ms-3">
577
- <div class="col-sm-12 col-md-12">No matching causatives or managed variants found</div>
588
+ <div class="col-md-7">No matching causatives or managed variants found</div>
578
589
  </div>
579
590
  {% endif %}
580
591
  {% endif %}
@@ -772,5 +783,12 @@ $('#collapse-icon').addClass('fa-angle-double-left');
772
783
  $('[data-bs-toggle=sidebar-collapse]').click(function() {
773
784
  SidebarCollapse();
774
785
  });
786
+
787
+ function dynamicPhenotypeCheck(source){
788
+ var checkboxes = document.getElementsByName('hpo_id');
789
+ for(var i=0, n=checkboxes.length;i<n;i++) {
790
+ checkboxes[i].checked = source.checked;
791
+ }
792
+ }
775
793
  </script>
776
794
  {% endblock %}
@@ -1,4 +1,4 @@
1
- {% from "cases/utils.html" import variant_transcripts %}
1
+ {% from "cases/utils.html" import variant_transcripts, filter_audits %}
2
2
  {% from "utils.html" import comments_table, variant_related_comments_table %}
3
3
  {% from "variants/components.html" import fusion_variants_header, default_fusion_variant_cells %}
4
4
 
@@ -266,22 +266,7 @@
266
266
  {% if audits | count_cursor > 0 %}
267
267
  <tr>
268
268
  <td colspan=2>
269
- <table id="audit-table" class="table table-sm" style="background-color: transparent">
270
- <thead>
271
- <tr>
272
- <th>Filters marked audited for case</th>
273
- </tr>
274
- </thead>
275
- <tbody>
276
- {% set audit_query = namespace() %}
277
- {% for audit in audits %}
278
- {% set audit_query = audit.link|url_args %}
279
- <tr>
280
- <td><strong>{{ audit.subject }} ({{audit_query.variant_type if audit_query.variant_type else "type unavailable -"}} {{ audit_query.category if audit_query.category else "category unavailable"}})</strong> was marked checked by {{ audit.user_name }} on {{audit.created_at.strftime('%Y-%m-%d')}}.</td>
281
- </tr>
282
- {% endfor %}
283
- </tbody>
284
- </table>
269
+ {{filter_audits(audits)}}
285
270
  </td>
286
271
  </tr>
287
272
  {% endif %}
@@ -234,16 +234,19 @@
234
234
  <form action="{{ url_for('cases.phenotypes_actions', institute_id=institute._id, case_name=case.display_name)+'#phenotypes_panel' }}" method="POST">
235
235
 
236
236
  <!-- Display and remove added HPO terms -->
237
- <div class="row mt-3">
238
- <div class="col-12 ms-3">
237
+ <div class="mt-3">
239
238
  {% if "phenotype_terms" in case and case.phenotype_terms|length > 0 %}
240
239
  {% for hpo_term in case.phenotype_terms %}
241
- {{ hpo_item(hpo_term, case) }}
240
+ <div class="col-12"> {{ hpo_item(hpo_term, case) }} </div>
242
241
  {% endfor %}
242
+
243
+ <div class="col-12 mt-3 form-check form-switch">
244
+ <input type="checkbox" class="form-check-input" id="checkAllPhenotypes" onChange="dynamicPhenotypeCheck(this)" name="checkAllPhenotypes">
245
+ <label for="checkAllPhenotypes" class="form-check-label">Select all phenotypes</label>
246
+ </div>
243
247
  {% else %}
244
248
  <span class="text-mute">No phenotypes added yet</span>
245
249
  {% endif %}
246
- </div>
247
250
  </div>
248
251
 
249
252
  <div id="phenotypes_panel" class="mt-3">
@@ -284,7 +287,7 @@
284
287
 
285
288
  {% macro hpo_item(hpo_term, case) %}
286
289
  {% if hpo_term %}
287
- <input type="checkbox" name="hpo_id" value="{{ hpo_term.phenotype_id }}"
290
+ <input type="checkbox" name="hpo_id" value="{{ hpo_term.phenotype_id }}"
288
291
  {% if case.dynamic_panel_phenotypes and hpo_term.phenotype_id in case.dynamic_panel_phenotypes %} checked {% endif %}>
289
292
  {{ hpo_term.feature }}
290
293
  <span class="badge bg-info">
@@ -324,18 +324,18 @@
324
324
  <a href="{{ url_for('variant.cancer_variant',
325
325
  institute_id=variant.institute,
326
326
  case_name=case.display_name,
327
- variant_id=variant._id) }}">
327
+ variant_id=variant._id) }}" target="_blank">
328
328
  {% else %}
329
329
  <a href="{{ url_for('variant.variant',
330
330
  institute_id=variant.institute,
331
331
  case_name=case.display_name,
332
- variant_id=variant._id) }}">
332
+ variant_id=variant._id) }}" target="_blank">
333
333
  {% endif %}
334
334
  {% else %} <!-- structural variants -->
335
335
  <a href="{{ url_for('variant.sv_variant',
336
336
  institute_id=variant.institute,
337
337
  case_name=case.display_name,
338
- variant_id=variant._id) }}">
338
+ variant_id=variant._id) }}" target="_blank">
339
339
  {% endif %}
340
340
  {{ pretty_variant(variant) }}
341
341
  </a>
@@ -698,3 +698,26 @@
698
698
  <span class="fa fa-exclamation-circle text-danger" data-bs-toggle='tooltip' title="Sex is not confirmed."></span>
699
699
  {% endif %}
700
700
  {% endmacro %}
701
+
702
+ {% macro filter_audits(audits, edit=none) %}
703
+ <table id="audit-table" class="table table-sm" style="background-color: transparent">
704
+ <thead>
705
+ <tr>
706
+ <th>Filters marked audited for case</th>
707
+ </tr>
708
+ </thead>
709
+ <tbody>
710
+ {% set audit_query = namespace() %}
711
+ {% for audit in audits %}
712
+ {% set audit_query = audit.link|url_args %}
713
+ <tr>
714
+ <td class="d-flex align-items-center"><strong>{{ audit.subject }} ({{audit_query.variant_type if audit_query.variant_type else "type unavailable -"}} {{ audit_query.category if audit_query.category else "category unavailable"}})</strong>&nbspwas marked checked by {{ audit.user_name }} on {{audit.created_at.strftime('%Y-%m-%d')}}.
715
+ {% if edit is true %}
716
+ <a href="{{ url_for('variants.unaudit_filter', audit_id=audit._id) }}" class="btn btn-link btn-sm" type="submit" data-bs-toggle='tooltip' title="Un-audit filter"><i class="fa fa-times text-danger"></i></a>
717
+ {% endif %}
718
+ </td>
719
+ </tr>
720
+ {% endfor %}
721
+ </tbody>
722
+ </table>
723
+ {% endmacro %}
@@ -442,11 +442,14 @@ def send_api_submission(institute_id, submission_id, key):
442
442
  if clinvar_id: # Check if submission object has already an associated ClinVar ID
443
443
  conversion_res["submissionName"] = clinvar_id
444
444
 
445
- code, submit_res = clinvar_api.submit_json(conversion_res, key)
445
+ service_url, code, submit_res = clinvar_api.submit_json(conversion_res, key)
446
446
 
447
447
  if code in [200, 201]:
448
448
  clinvar_id = submit_res.json().get("id")
449
- flash(f"Submission saved successfully with ID: {clinvar_id}", "success")
449
+ flash(
450
+ f"Submission sent to API URL '{service_url}'. Saved successfully with ID: {clinvar_id}",
451
+ "success",
452
+ )
450
453
 
451
454
  # Update ClinVar submission ID with the ID returned from ClinVar
452
455
  store.update_clinvar_id(
@@ -459,7 +462,10 @@ def send_api_submission(institute_id, submission_id, key):
459
462
  )
460
463
 
461
464
  else:
462
- flash(str(submit_res.json()), "warning")
465
+ flash(
466
+ f"Submission sent to API URL '{service_url}'. Returned the following error: {str(submit_res.json())}",
467
+ "warning",
468
+ )
463
469
 
464
470
 
465
471
  def clinvar_submission_file(submission_id, csv_type, clinvar_subm_id):
@@ -3,7 +3,7 @@ import logging
3
3
  from flask import flash, redirect, request, url_for
4
4
  from flask_login import current_user
5
5
 
6
- from scout.constants import CASE_SEARCH_TERMS
6
+ from scout.constants import CASE_SEARCH_TERMS, CASE_TAGS
7
7
  from scout.server.extensions import store
8
8
  from scout.server.utils import user_institutes
9
9
 
@@ -139,8 +139,22 @@ def get_dashboard_info(adapter, data={}, institute_id=None, slice_query=None):
139
139
  "count": general_sliced_info["cohort_cases"],
140
140
  "percent": general_sliced_info["cohort_cases"] / total_sliced_cases,
141
141
  },
142
+ {
143
+ "title": "Case status tag",
144
+ "count": general_sliced_info["tagged_cases"],
145
+ "percent": general_sliced_info["tagged_cases"] / total_sliced_cases,
146
+ },
142
147
  ]
143
148
 
149
+ for stats_tag in CASE_TAGS.keys():
150
+ overview.append(
151
+ {
152
+ "title": CASE_TAGS[stats_tag]["label"] + " status tag",
153
+ "count": general_sliced_info[stats_tag + "_cases"],
154
+ "percent": general_sliced_info[stats_tag + "_cases"] / total_sliced_cases,
155
+ }
156
+ )
157
+
144
158
  data["overview"] = overview
145
159
 
146
160
  return data
@@ -149,6 +163,11 @@ def get_dashboard_info(adapter, data={}, institute_id=None, slice_query=None):
149
163
  def get_general_case_info(adapter, institute_id=None, slice_query=None):
150
164
  """Return general information about cases
151
165
 
166
+ Count e.g. cases with each kind of case tag, as well as cases that have phenotype, causatives or pinned variants
167
+ marked or are part of cohorts.
168
+
169
+ Gather pedigree information - single, duo, trio or many individuals in case.
170
+
152
171
  Args:
153
172
  adapter(adapter.MongoAdapter)
154
173
  institute_id(str)
@@ -168,15 +187,24 @@ def get_general_case_info(adapter, institute_id=None, slice_query=None):
168
187
  "suspects": 1,
169
188
  "cohorts": 1,
170
189
  "individuals": 1,
190
+ "tags": 1,
171
191
  }
172
192
  cases = adapter.cases(
173
193
  owner=institute_id, name_query=name_query, projection=CASE_GENERAL_INFO_PROJECTION
174
194
  )
175
195
 
176
- phenotype_cases = 0
177
- causative_cases = 0
178
- pinned_cases = 0
179
- cohort_cases = 0
196
+ case_counter_keys = [
197
+ "phenotype",
198
+ "causative",
199
+ "pinned",
200
+ "cohort",
201
+ "tagged",
202
+ ]
203
+ case_counter_keys.extend(CASE_TAGS.keys())
204
+
205
+ case_counter = {}
206
+ for counter in case_counter_keys:
207
+ case_counter[counter] = 0
180
208
 
181
209
  pedigree = {
182
210
  1: {"title": "Single", "count": 0},
@@ -191,13 +219,18 @@ def get_general_case_info(adapter, institute_id=None, slice_query=None):
191
219
  for total_cases, case in enumerate(cases, 1):
192
220
  case_ids.add(case["_id"])
193
221
  if case.get("phenotype_terms"):
194
- phenotype_cases += 1
222
+ case_counter["phenotype"] += 1
195
223
  if case.get("causatives"):
196
- causative_cases += 1
224
+ case_counter["causative"] += 1
197
225
  if case.get("suspects"):
198
- pinned_cases += 1
226
+ case_counter["pinned"] += 1
199
227
  if case.get("cohorts"):
200
- cohort_cases += 1
228
+ case_counter["cohort"] += 1
229
+ if case.get("tags"):
230
+ case_counter["tagged"] += 1
231
+ case_tags = case.get("tags")
232
+ for tag in case_tags:
233
+ case_counter[tag] += 1
201
234
 
202
235
  nr_individuals = len(case.get("individuals", []))
203
236
  if nr_individuals == 0:
@@ -208,12 +241,10 @@ def get_general_case_info(adapter, institute_id=None, slice_query=None):
208
241
  pedigree[nr_individuals]["count"] += 1
209
242
 
210
243
  general["total_cases"] = total_cases
211
- general["phenotype_cases"] = phenotype_cases
212
- general["causative_cases"] = causative_cases
213
- general["pinned_cases"] = pinned_cases
214
- general["cohort_cases"] = cohort_cases
215
244
  general["pedigree"] = pedigree
216
245
  general["case_ids"] = case_ids
246
+ for counter in case_counter_keys:
247
+ general[counter + "_cases"] = case_counter[counter]
217
248
 
218
249
  return general
219
250
 
@@ -15,12 +15,16 @@ function analysisTypeData(analysis_types) {
15
15
  "bkgColors": {
16
16
  "WES": "#46bfbd",
17
17
  "WGS": "#fdb45c",
18
- "PANEL": "#44449B"
18
+ "PANEL": "#44449B",
19
+ "PANEL-UMI": "#949fb1",
20
+ "WTS": "#f7464A"
19
21
  },
20
22
  "hoverColors": {
21
23
  "WES": "#5ad3d1",
22
24
  "WGS": "#ffc870",
23
- "PANEL": "#353578"
25
+ "PANEL": "#353578",
26
+ "PANEL-UMI": "#a8b3c5",
27
+ "WTS": "#ff5a5e"
24
28
  }
25
29
  };
26
30
  var bkg = [];
@@ -77,7 +81,7 @@ function casesType(cases) {
77
81
  var bkg = [];
78
82
  var hover = [];
79
83
  labels.forEach(function (item, index) {
80
- bkg.push(bkgColors[index]);
84
+ bkg.push(bkgColors[index]);
81
85
  hover.push(hoverColors[index]);
82
86
  });
83
87
  var chart_data = {
@@ -129,52 +133,58 @@ function casesDetailed(overview, all_cases) {
129
133
  var bkg = [];
130
134
  var hover = [];
131
135
  labels.forEach(function (item, index) {
132
- bkg.push(bkgColors[index]);
136
+ ncol = bkgColors.length;
137
+ bkg.push(bkgColors[index % ncol]);
133
138
  });
134
139
  var chart_data = {
135
- type: "horizontalBar",
140
+ type: "bar",
136
141
  data: {
137
142
  labels: labels,
138
143
  datasets: [{
139
144
  data: overview.map(function (overview) {
140
145
  return overview.count * 100 / all_cases;
141
146
  }),
147
+ label: "Case percentage",
142
148
  backgroundColor: bkg,
143
149
  hoverBackgroundColor: hover
144
150
  }]
145
151
  },
146
152
  options: {
147
- tooltips: {
148
- callbacks: {
149
- label: function label(tooltipItems) {
150
- return Math.round(Number(tooltipItems.value) * all_cases / 100);
151
- }
152
- }
153
- },
154
- scales: {
155
- xAxes: [{
156
- ticks: {
157
- min: 0,
158
- max: 100,
159
- callback: function callback(value) {
160
- return value + "%";
161
- }
162
- },
163
- scaleLabel: {
164
- display: true,
165
- labelString: "Case percentage",
166
- fontSize: 20
167
- }
168
- }],
169
- yAxes: [{
170
- ticks: {
171
- fontSize: 20
172
- }
173
- }]
174
- },
175
- legend: {
176
- display: false
177
- }
153
+ indexAxis: 'y',
154
+ tooltips: {
155
+ callbacks: {
156
+ label: function label(tooltipItems) {
157
+ return Math.round(Number(tooltipItems.value) * all_cases / 100);
158
+ }
159
+ }
160
+ },
161
+ scales: {
162
+ x: {
163
+ ticks: {
164
+ min: 0,
165
+ max: 100,
166
+ callback: function callback(value) {
167
+ return value + "%";
168
+ }
169
+ },
170
+ title: {
171
+ display: true,
172
+ text: "Case percentage",
173
+ font: 20
174
+ }
175
+ },
176
+ y: {
177
+ ticks: {
178
+ fontSize: 20
179
+ }
180
+ }
181
+ },
182
+ plugins:
183
+ {
184
+ legend: {
185
+ display: false
186
+ }
187
+ }
178
188
  }
179
189
  };
180
190
  return chart_data;
@@ -108,7 +108,7 @@
108
108
  {% endmacro %}
109
109
 
110
110
  {% macro cases_stats_panels() %}
111
- <div class="mt-3" id="cases">
111
+ <div class="mt-3 mb-1" id="cases">
112
112
  <div class="row">
113
113
  <div class="col-md-8">
114
114
  <canvas id="cases-bar-horiz" height="80"></canvas>
@@ -129,7 +129,7 @@
129
129
 
130
130
  {% block scripts %}
131
131
  {{ super() }}
132
- <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.bundle.min.js" integrity="sha512-60KwWtZOhzgr840mc57MV8JqDZHAws3w61mhK45KsYHmhyNFJKmfg4M7/s2Jsn4PgtQ4Uhr9xItS+HCbGTIRYQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
132
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js" integrity="sha512-NqRhTU0DQNHNUO0pTx6zPLJ11YhOqj4MRcvv0+amxJk+re07ykGhFuhMmrQpfTRAUx8nQ4EcMuX/m8cz2K8vIQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
133
133
  <script src="{{ url_for('dashboard.static', filename='charts.js') }}"></script>
134
134
  <script type="text/javascript">
135
135
 
@@ -17,6 +17,7 @@ from scout.constants import CASE_SEARCH_TERMS, PHENOTYPE_GROUPS
17
17
  from scout.models.case import STATUS
18
18
 
19
19
  CASE_SEARCH_KEY = [(value["prefix"], value["label"]) for key, value in CASE_SEARCH_TERMS.items()]
20
+ CATEGORY_CHOICES = [("snv", "SNV"), ("sv", "SV")]
20
21
 
21
22
 
22
23
  class NonValidatingSelectField(SelectField):
@@ -142,6 +143,7 @@ class GeneVariantFiltersForm(FlaskForm):
142
143
  """Base FiltersForm for SNVs"""
143
144
 
144
145
  variant_type = SelectMultipleField(choices=[("clinical", "clinical"), ("research", "research")])
146
+ category = SelectMultipleField(choices=CATEGORY_CHOICES)
145
147
  hgnc_symbols = TagListField(
146
148
  "HGNC Symbols (comma-separated, case sensitive)", validators=[validators.InputRequired()]
147
149
  )
@@ -113,19 +113,21 @@
113
113
  {% macro case_row(case) %}
114
114
  <tr class="{% if case.status == 'solved' %}causative{% endif %}">
115
115
  <td>
116
- <a href="{{ url_for('cases.case', institute_id=case.owner, case_name=case.display_name) }}">
116
+ <a class="me-2"
117
+ {% if case.individuals|length == 1 %} data-bs-toggle="tooltip" title="{{case.individuals[0].display_name}}" {% endif %}
118
+ href="{{ url_for('cases.case', institute_id=case.owner, case_name=case.display_name) }}">
117
119
  {{ case.display_name }}
118
120
  </a>
119
121
  {% if case.individuals|length > 1 %}
120
- <span class="badge bg-primary">
121
- {% for item in case.individuals %}
122
+ {% for sample in case.individuals %}
123
+ <span class="badge {{'bg-danger' if sample.phenotype == 2 else 'bg-primary'}}" style="margin:-3px !important;" data-bs-toggle="tooltip" title="{{sample.display_name}}">
122
124
  {% if case.track == "cancer" %}
123
125
  <span class="fa fa-vial"></span>
124
126
  {% else %} <!-- rare disease case -->
125
127
  <span class="fa fa-user"></span>
126
128
  {% endif %}
129
+ </span>
127
130
  {% endfor %}
128
- </span>
129
131
  {% endif %}
130
132
  {% for user in case.assignees %}
131
133
  <span class="badge bg-secondary">{{ user.name }}</span>