viral_seq 1.3.0 → 1.6.1

Sign up to get free protection for your applications and to get access to all the features.
data/bin/tcs_log CHANGED
@@ -16,6 +16,7 @@ require 'viral_seq'
16
16
  require 'pathname'
17
17
  require 'json'
18
18
  require 'fileutils'
19
+ require 'csv'
19
20
 
20
21
  indir = ARGV[0].chomp
21
22
  indir_basename = File.basename(indir)
@@ -26,6 +27,7 @@ Dir.mkdir(tcs_dir) unless File.directory?(tcs_dir)
26
27
 
27
28
  libs = []
28
29
  Dir.chdir(indir) {libs = Dir.glob("*")}
30
+ libs.sort_by! {|lib| lib.split("-")[1].to_i}
29
31
 
30
32
  outdir2 = File.join(tcs_dir, "combined_TCS_per_lib")
31
33
  outdir3 = File.join(tcs_dir, "TCS_per_region")
@@ -53,18 +55,26 @@ header = %w{
53
55
  Resampling_index
54
56
  Combined_TCS
55
57
  Combined_TCS_after_QC
58
+ Detection_Sensitivity
56
59
  WARNINGS
57
60
  }
58
61
 
62
+ pid_dist_data = {}
63
+
59
64
  log.puts header.join(',')
60
65
  libs.each do |lib|
61
66
  Dir.mkdir(File.join(outdir2, lib)) unless File.directory?(File.join(outdir2, lib))
62
67
  fasta_files = []
63
68
  json_files = []
69
+ pid_json_files = []
70
+ pid_dist_data[lib] = {}
71
+
64
72
  Dir.chdir(File.join(indir, lib)) do
65
73
  fasta_files = Dir.glob("**/*.fasta")
66
74
  json_files = Dir.glob("**/log.json")
75
+ pid_json_files = Dir.glob("**/primer_id.json")
67
76
  end
77
+
68
78
  fasta_files.each do |f|
69
79
  path_array = Pathname(f).each_filename.to_a
70
80
  region = path_array[0]
@@ -81,6 +91,14 @@ libs.each do |lib|
81
91
 
82
92
  json_files.each do |f|
83
93
  json_log = JSON.parse(File.read(File.join(indir, lib, f)), symbolize_names: true)
94
+ tcs_number = json_log[:total_tcs]
95
+ if json_log[:combined_tcs]
96
+ tcs_number = json_log[:combined_tcs]
97
+ if json_log[:combined_tcs_after_qc]
98
+ tcs_number = json_log[:combined_tcs_after_qc]
99
+ end
100
+ end
101
+
84
102
  log.print [lib,
85
103
  json_log[:primer_set_name],
86
104
  json_log[:total_raw_sequence],
@@ -95,8 +113,604 @@ libs.each do |lib|
95
113
  json_log[:resampling_param],
96
114
  json_log[:combined_tcs],
97
115
  json_log[:combined_tcs_after_qc],
116
+ ViralSeq::TcsCore::detection_limit(tcs_number.to_i),
98
117
  json_log[:warnings],
99
118
  ].join(',') + "\n"
100
119
  end
120
+
121
+ pid_json_files.each do |f|
122
+ pid_json = JSON.parse(File.read(File.join(indir, lib, f)), symbolize_names: true)
123
+ region = Pathname(f).each_filename.to_a[-2]
124
+ pid_dist = {}
125
+ pid_json[:primer_id_distribution].each {|k,v| pid_dist[k.to_s.to_i] = v}
126
+ pid_dist_data[lib][region] = pid_dist
127
+ end
101
128
  end
102
129
  log.close
130
+
131
+ # Create HTML page with charts from log.csv above
132
+
133
+ class String
134
+ def var_safe
135
+ gsub '-',''
136
+ end
137
+ def shorten_html
138
+ gsub /\n/, ''
139
+ gsub /\t/, ''
140
+ end
141
+ end
142
+
143
+ colors = ["#332288", "#117733", "#44AA99", "#88CCEE", "#DDCC77", "#CC6677", "#AA4499", "#882255"]
144
+ bool_colors = { true => colors[3], false => colors[5] }
145
+
146
+ #hold vars for csv input
147
+ raw_sequence_data = ["['Library Name', 'Raw Sequences', { role: 'annotation' }]"]
148
+ lib_names = []
149
+ total_reads = 0
150
+ max_region_char_length = 5
151
+ lib_data = {}
152
+ batch_name = ""
153
+ region_colors = {"Other" => "#808080"}
154
+
155
+ CSV.foreach(log_file).each_with_index do |row, i|
156
+ next if i == 0 || row[0] == nil
157
+
158
+ lib_name = row[0]
159
+ region = row[1]
160
+ raw_sequences_per_barcode = row[2].to_i
161
+
162
+ if not region_colors.key?(region)
163
+ region_colors[region] = colors[region_colors.length % (colors.length - 1)]
164
+ end
165
+
166
+ if region.length > max_region_char_length + 2
167
+ max_region_char_length = region.length + 2
168
+ end
169
+
170
+ if batch_name == ""
171
+ batch_name = lib_name.split('-')[0]
172
+ end
173
+
174
+ if not lib_names.include? lib_name
175
+ lib_names.push(lib_name)
176
+ total_reads += raw_sequences_per_barcode
177
+ raw_sequence_data.push("['#{lib_name}', #{raw_sequences_per_barcode}, '#{raw_sequences_per_barcode}']")
178
+ lib_data[lib_name] = {}
179
+ end
180
+
181
+ lib_data[lib_name][region] = {
182
+ 'lib_name' => lib_name,
183
+ 'region' => region,
184
+ 'raw_sequences_per_barcode' => raw_sequences_per_barcode,
185
+ 'r1_raw' => row[3].to_i,
186
+ 'r2_raw' => row[4].to_i,
187
+ 'paired_raw' => row[5].to_i,
188
+ 'cutoff' => row[6].to_i,
189
+ 'consensus2' => row[9].to_i,
190
+ 'distinct_to_raw' => row[10].to_f,
191
+ 'resampling_index' => row[11].to_f,
192
+ 'combined_TCS' => row[12].to_i,
193
+ 'combined_TCS_after_QC' => row[13].to_i,
194
+ 'detection_sensitivity' => row[14].to_f,
195
+ 'warnings' => row[15].to_s || ""
196
+ }
197
+ end
198
+
199
+ #format output
200
+ total_reads = total_reads.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
201
+ raw_sequence_data = "[" + raw_sequence_data.join(',') + "]"
202
+ lib_names = lib_names.sort_by { |s| [s.split("-")[0], s.split("-")[1].to_i] }
203
+
204
+ #calculate 'other'
205
+ lib_data.each do |lib, data|
206
+ sum = data.values.reduce(0) { |sum, obj| sum + obj['paired_raw'] }
207
+ raw_sequences_per_barcode = data[data.keys.first]['raw_sequences_per_barcode']
208
+ other = raw_sequences_per_barcode - sum
209
+ data['other'] = {
210
+ "region" => "Other",
211
+ "paired_raw" => other,
212
+ "raw_sequences_per_barcode" => raw_sequences_per_barcode
213
+ }
214
+ end
215
+
216
+ #process each library's html
217
+ library_links = "
218
+ <ul>
219
+ <li id='link_basic-statistics' class='pointer current_page' onclick='showPage(\"basic-statistics\")'>Basic Statistics</li>
220
+ <li>Libraries
221
+ <ul style='margin-left: 16px;'>" + lib_names.map { |lib_name| "
222
+ <li id='link_#{lib_name}' class='pointer' onclick='showPage(\""+lib_name+"\")'>"+lib_name+"</li>"
223
+ }.join + "
224
+ </ul>
225
+ </li>
226
+ </ul>
227
+ "
228
+
229
+ library_pages = lib_names.map { |lib_name|
230
+ "<div id='"+lib_name+"' class='page hidden'>
231
+ <div class='error-message'></div>
232
+ <div style='display: flex; gap: 16px; min-height: 45vh;'>
233
+ <div class='card' style='flex: 1; display: flex; flex-direction: column;'>
234
+ <h2>Distribution of Raw Sequencing Reads</h2>
235
+ <div class='pie_chart' style='flex: 1;'></div>
236
+ </div>
237
+ <div class='card' style='display: flex; align-items: center; justify-content: center; flex-direction: column; flex: 1;'>
238
+ " + lib_data[lib_name].map{ |lib, data| data['region'] == "Other" ? "" : "
239
+ <div class='#{data['region']}' style='display: flex; align-items: center;'>
240
+ <div style='width: #{max_region_char_length.to_s}ch;'>#{data['region']}</div>
241
+ <div class='treemap' style='flex: 1; height: #{90 / lib_data[lib_name].length}vh;'></div>
242
+ </div>
243
+ " }.join + "
244
+ </div>
245
+ </div>
246
+ <div class='card'>
247
+ <h2 style='margin: 32px;'>Number of TCS at Regions</h2>
248
+ <div class='tcs_bar_chart'></div>
249
+ <div class='tcs_warnings' style='text-align: center;color: red;font-weight: bold;margin-left: 24px;'><strong></strong></div>
250
+ </div>
251
+ <div class='card'>
252
+ <h2>Detection Sensitivity</h2>
253
+ <p><i>The lowest abundance of minority mutations that can be detected with 95% confidence</i></p>
254
+ <div class='detection_sensitivity_chart'></div>
255
+ </div>
256
+ <div class='card'>
257
+ <h2 style='text-align: center;'>Distinct to Raw</h2>
258
+ <p><i>Distinct to Raw greater than 0.1 suggests more raw sequences are required to fully recover TCS</i></p>
259
+ <div class='raw_bar_chart'></div>
260
+ </div>
261
+ <div class='card'>
262
+ <h2>Resampling Index</h2>
263
+ <p><i>Resampling index less than 0.9 suggests Primer ID resampling</i></p>
264
+ <div class='resampling_bar_chart'></div>
265
+ </div>
266
+ <div class='card'>
267
+ <h2>Primer ID bin size distribution</h2>
268
+ <p><i>Red vertical line shows the consensus cut-off value</i></p>
269
+ " + lib_data[lib_name].map{ |lib, data| data['region'] == "Other" ? "" : "
270
+ <div class='#{data['region']} scatter' style='height: #{90 / 3}vh;'></div>
271
+ " }.join + "
272
+ </div>
273
+ </div>"
274
+ }.join
275
+
276
+ #format lib_data into charts
277
+ paired_raw = {}
278
+ tree_charts = {}
279
+ tcs_bar_chart = {}
280
+ distinct_bar_chart = {}
281
+ resampling_bar_chart = {}
282
+ detection_sensitivity_chart = {}
283
+
284
+ lib_data.each do |lib, data|
285
+ paired_raw[lib] = {}
286
+
287
+ paired_raw[lib]['data'] = "
288
+ [
289
+ ['Region', 'Paired Raw'], " +
290
+ data.map { |lib, d| "
291
+ ['#{d['region']},#{d['paired_raw']}', #{d['paired_raw']}],
292
+ " }.join + "
293
+ ]
294
+ "
295
+
296
+ paired_raw[lib]['slices'] = "[#{
297
+ data.map{ |lib, d| "{color: '#{region_colors[d['region']]}'}" }.join(',')
298
+ }]"
299
+
300
+ paired_raw[lib]['residuel_text'] = data.map { |lib, d|
301
+ (d['paired_raw'].to_f / d['raw_sequences_per_barcode'].to_f) < (0.5/360) ?
302
+ "#{d['region']},#{d['paired_raw']}" :
303
+ nil
304
+ }.compact.join(" - ")
305
+
306
+ tree_charts[lib] = {}
307
+ data.each do |region, d|
308
+ if region != "other"
309
+ tree_charts[lib][region] = "
310
+ [
311
+ ['Location', 'Parent', 'r'],
312
+ ['Global', null,0],
313
+ ['R1_Raw', 'Global', #{d['r1_raw'].to_s}],
314
+ ['R2_Raw', 'Global', #{d['r2_raw'].to_s}],
315
+ ['Paired_Raw', 'Global', #{d['paired_raw'].to_s}],
316
+ ]
317
+ "
318
+ end
319
+ end
320
+
321
+ has_c = data.values.inject(0) {|sum, h| sum + (h['combined_TCS'] == nil ? 0 : h['combined_TCS']) } > 0
322
+ has_qc = data.values.inject(0) {|sum, h| sum + (h['combined_TCS_after_QC'] == nil ? 0 : h['combined_TCS_after_QC']) } > 0
323
+
324
+ tcs_bar_chart[lib] = {
325
+ 'data' => "
326
+ [
327
+ [
328
+ 'Region',
329
+ 'TCS',
330
+ #{has_c ? "'Combined TCS'," : ''}
331
+ #{has_qc ? "'TCS After QC'," : ''}
332
+ ],
333
+ " + data.map{ |region, d| d['region'] === "Other" ? "" : "
334
+ [
335
+ '#{d['region']}#{d['warnings'].length > 0 ? '*' : ''}',
336
+ #{d['consensus2']} ,
337
+ #{has_c ? "#{d['combined_TCS']}," : ''}
338
+ #{has_qc ? "#{d['combined_TCS_after_QC']}," : ''}
339
+ ],
340
+ " }.join + "
341
+ ]
342
+ ",
343
+ 'warnings' => data.map{ |region, d|
344
+ d['region'] != "Other" && d['warnings'].length > 0 ? "#{region} - #{d["warnings"]}<br/>" : ''
345
+ }.join
346
+ }
347
+
348
+ detection_sensitivity_chart[lib] = "[
349
+ ['Region', 'Detection Sensitivity'],
350
+ #{ data.map{ |region, d|
351
+ d['region'] == 'Other' ? "" : "['#{d['region']}', #{d['detection_sensitivity']}]"
352
+ }.join(',') }
353
+ ]"
354
+
355
+ distinct_bar_chart[lib] = "
356
+ [
357
+ ['Region','Distinct to Raw', {role: 'style'}],
358
+ "+ data.map{ |region, d| d['region'] == 'Other' ? "" : "
359
+ [
360
+ '#{d['region']}', #{d['distinct_to_raw']}, '#{bool_colors[d['distinct_to_raw'].to_f < 0.1]}'
361
+ ],"}.join + "
362
+ ]
363
+ "
364
+
365
+ resampling_bar_chart[lib] = "
366
+ [
367
+ ['Region','Resampling Index', {role: 'style'}],
368
+ "+ data.map{ |region, d| d['region'] == 'Other' ? "" : "
369
+ [
370
+ '#{d['region']}', #{d['resampling_index']}, '#{bool_colors[d['resampling_index'].to_f > 0.9]}'
371
+ ]," }.join + "
372
+ ]
373
+ "
374
+ end
375
+
376
+ scatter_charts = {}
377
+
378
+ pid_dist_data.each do |lib, data|
379
+ scatter_charts[lib] = {}
380
+ data.each do |region, d|
381
+ max_x = d.max_by{|index, distribution| distribution }[1]
382
+ max_x = "1#{ max_x.to_s.chars.map{ |c| "0"}.join }"
383
+ scatter_charts[lib][region] = "[
384
+ ['Index', 'Distribution', 'Cutoff'],
385
+ [#{lib_data[lib][region]['cutoff'].to_s}, null, 0],
386
+ [#{lib_data[lib][region]['cutoff'].to_s}, null, #{max_x}],
387
+ #{d.map{ |index, distribution| "[#{index}, #{distribution}, null]" }.join(',') },
388
+ ]"
389
+ end
390
+ end
391
+
392
+ #create JS that initializes charts
393
+ paired_raw_js = paired_raw.map { |lib, data| '
394
+ var element_pie_'+lib.var_safe+' = document.querySelector("#'+lib+' .pie_chart")
395
+ if(isVisible(element_pie_'+lib.var_safe+')){
396
+ var chart_pie_'+lib.var_safe+' = new google.visualization.PieChart(element_pie_'+lib.var_safe+');
397
+ chart_pie_'+lib.var_safe+'.draw(
398
+ google.visualization.arrayToDataTable('+data['data']+'),
399
+ {
400
+ title: "Paired Raw",
401
+ titleTextStyle: {
402
+ fontSize: 18
403
+ },
404
+ pieSliceText: "label",
405
+ slices: ' + data['slices'] + ',
406
+ legend: {
407
+ position: "left",
408
+ alignment: "center"
409
+ },
410
+ chartArea: {
411
+ width: "100%",
412
+ height: "100%"
413
+ },
414
+ ' + (data['residuel_text'].length > 0 ? "pieResidueSliceLabel: '#{data['residuel_text']}'" : "") + '
415
+ }
416
+ );
417
+ }
418
+ ' }.join
419
+
420
+ tree_charts_js = tree_charts.map { |lib, d|
421
+ d.map { |region, data| '
422
+ var element_chart_tree_'+lib.var_safe+'_'+region.var_safe+' = document.querySelector("#'+lib+' .'+region+' .treemap")
423
+ if(isVisible(element_chart_tree_'+lib.var_safe+'_'+region.var_safe+')){
424
+ var chart_tree_'+lib.var_safe+'_'+region.var_safe+' = new google.visualization.TreeMap(element_chart_tree_'+lib.var_safe+'_'+region.var_safe+');
425
+ chart_tree_'+lib.var_safe+'_'+region.var_safe+'.draw(
426
+ google.visualization.arrayToDataTable('+data+'),
427
+ {
428
+ headerHeight: 0,
429
+ minColor: "' + colors[1] + '",
430
+ maxColor: "' + colors[5] + '",
431
+ fontColor: "#fff",
432
+ }
433
+ );
434
+ google.visualization.events.addListener(chart_tree_'+lib.var_safe+'_'+region.var_safe+', "select", function () {
435
+ chart_tree_'+lib.var_safe+'_'+region.var_safe+'.setSelection([]);
436
+ });
437
+ }
438
+ '}
439
+ }.join
440
+
441
+ tcs_bar_chart_js = tcs_bar_chart.map { |lib, data| '
442
+ var element_tcs_bar_chart_'+lib.var_safe+' = document.querySelector("#'+lib+' .tcs_bar_chart")
443
+ if(isVisible(element_tcs_bar_chart_'+lib.var_safe+')){
444
+ var tcs_bar_chart_'+lib.var_safe+' = new google.charts.Bar(element_tcs_bar_chart_'+lib.var_safe+');
445
+ tcs_bar_chart_'+lib.var_safe+'.draw(
446
+ google.visualization.arrayToDataTable('+data["data"]+'),
447
+ google.charts.Bar.convertOptions({
448
+ colors: ["' + colors[0] + '", "' + colors[1] + '", "' + colors[2] + '"],
449
+ legend: { position: "bottom" },
450
+ height: Math.round(window.innerHeight * .5),
451
+ chartArea: {
452
+ width: "100%",
453
+ height: "100%"
454
+ }
455
+ })
456
+ );
457
+ document.querySelector("#'+lib+' .tcs_warnings").innerHTML = "'+data['warnings']+'";
458
+ }
459
+ ' }.join
460
+
461
+ detection_sensitivity_js = detection_sensitivity_chart.map { |lib, data| '
462
+ var element_detection_'+lib.var_safe+' = document.querySelector("#'+lib+' .detection_sensitivity_chart")
463
+ if(isVisible(element_detection_'+lib.var_safe+')){
464
+ var detection_'+lib.var_safe+' = new google.visualization.ColumnChart(element_detection_'+lib.var_safe+');
465
+ detection_'+lib.var_safe+'.draw(
466
+ google.visualization.arrayToDataTable('+data+'),
467
+ {
468
+ legend: {
469
+ position: "none"
470
+ },
471
+ height: Math.round(window.innerHeight * .5),
472
+ colors: ["' + colors[3] + '"],
473
+ vAxis: {
474
+ scaleType: "log",
475
+ viewWindow: {
476
+ min: 0.00001
477
+ },
478
+ ticks: [0.00001, 0.0001, 0.001, 0.01, 0.1, 1]
479
+ }
480
+ }
481
+ );
482
+ }
483
+ '}.join
484
+
485
+ distinct_bar_chart_js = distinct_bar_chart.map { |lib, data| '
486
+ var element_distinct_'+lib.var_safe+' = document.querySelector("#'+lib+' .raw_bar_chart")
487
+ if(isVisible(element_distinct_'+lib.var_safe+')){
488
+ var distinct_'+lib.var_safe+' = new google.visualization.ColumnChart(element_distinct_'+lib.var_safe+');
489
+ distinct_'+lib.var_safe+'.draw(
490
+ google.visualization.arrayToDataTable('+data+'),
491
+ {
492
+ legend: {
493
+ position: "none"
494
+ },
495
+ height: Math.round(window.innerHeight * .5),
496
+ }
497
+ );
498
+ }
499
+ ' }.join
500
+
501
+ resampling_bar_chart_js = resampling_bar_chart.map { |lib, data| '
502
+ var element_resampling_'+lib.var_safe+' = document.querySelector("#'+lib+' .resampling_bar_chart")
503
+ if(isVisible(element_resampling_'+lib.var_safe+')){
504
+ var resampling_'+lib.var_safe+' = new google.visualization.ColumnChart(element_resampling_'+lib.var_safe+');
505
+ resampling_'+lib.var_safe+'.draw(
506
+ google.visualization.arrayToDataTable('+data+'),
507
+ {
508
+ legend: {
509
+ position: "none"
510
+ },
511
+ height: Math.round(window.innerHeight * .5),
512
+ }
513
+ );
514
+ }
515
+ ' }.join
516
+
517
+ scatter_charts_js = scatter_charts.map { |lib, d|
518
+ d.map { |region, data| '
519
+ var element_chart_scatter_'+lib.var_safe+'_'+region.var_safe+' = document.querySelector("#'+lib+' .'+region+'.scatter")
520
+ if(isVisible(element_chart_scatter_'+lib.var_safe+'_'+region.var_safe+')){
521
+ var chart_scatter_'+lib.var_safe+'_'+region.var_safe+' = new google.visualization.ComboChart(element_chart_scatter_'+lib.var_safe+'_'+region.var_safe+');
522
+
523
+ var view_'+lib.var_safe+'_'+region.var_safe+' = new google.visualization.DataView(
524
+ google.visualization.arrayToDataTable(' + data + ')
525
+ );
526
+
527
+ chart_scatter_'+lib.var_safe+'_'+region.var_safe+'.draw(
528
+ view_'+lib.var_safe+'_'+region.var_safe+',
529
+ {
530
+ pointSize: 5,
531
+ title: "' + region + '",
532
+ hAxis: {title: "Raw sequencing reads per unique PID"},
533
+ vAxis: {
534
+ title: "# of PIDs ",
535
+ logScale: true
536
+ },
537
+ colors: ["' + bool_colors[true] + '"],
538
+ legend: "none",
539
+ seriesType: "scatter",
540
+ series: {
541
+ 1: {
542
+ type: "line",
543
+ color: "' + bool_colors[false] + '",
544
+ pointsVisible: false,
545
+ }
546
+ }
547
+ }
548
+ );
549
+ }
550
+ '}
551
+ }.join
552
+
553
+ html = '
554
+ <!DOCTYPE html>
555
+ <html lang="en">
556
+ <head>
557
+ <meta charset="UTF-8">
558
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
559
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
560
+ <title>TCS Log</title>
561
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
562
+ <script src="https://www.gstatic.com/charts/loader.js"></script>
563
+ <script>
564
+ google.charts.load("current", {packages: ["corechart", "treemap", "bar"]});
565
+ google.charts.setOnLoadCallback(drawChart);
566
+
567
+ var isInit = false
568
+
569
+ function isVisible(el) {
570
+ return (el.offsetParent !== null)
571
+ }
572
+
573
+ function drawChart() {
574
+
575
+ document.querySelectorAll(".error-message").forEach(element => element.innerHTML = "")
576
+
577
+ if(! isInit){
578
+ isInit = true;
579
+ window.onresize = drawChart;
580
+ }
581
+
582
+ try{
583
+ var element_home_chart = document.getElementById("raw-sequence-chart")
584
+ if(isVisible(element_home_chart)){
585
+ var home_chart = new google.visualization.ColumnChart(element_home_chart);
586
+ home_chart.draw(
587
+ google.visualization.arrayToDataTable('+raw_sequence_data+'),
588
+ {
589
+ annotations: {alwaysOutside: true},
590
+ colors: ["' + bool_colors[true] + '"]
591
+ }
592
+ );
593
+ }'+ paired_raw_js + tree_charts_js + tcs_bar_chart_js + distinct_bar_chart_js + resampling_bar_chart_js + detection_sensitivity_js + scatter_charts_js + '
594
+ }catch(e){
595
+ console.log(e);
596
+ document.querySelectorAll(".error-message").forEach(element =>
597
+ element.innerHTML = "There was an error displaying your data.<br>To help us improve, please forward this html file to<br>shuntaiz@email.unc.edu"
598
+ )
599
+ }
600
+ }
601
+
602
+ function showPage(pageID){
603
+ document.querySelectorAll(".page").forEach(element => element.classList.add("hidden"))
604
+ document.getElementById(pageID).classList.remove("hidden")
605
+
606
+ document.querySelectorAll(".current_page").forEach(element => element.classList.remove("current_page"));
607
+ document.getElementById("link_"+pageID).classList.add("current_page");
608
+
609
+ drawChart();
610
+ }
611
+ </script>
612
+ </head>
613
+ <body>
614
+ <div style="display: flex; flex-direction: column; height: 100vh; width: 100vw; position: fixed; overflow: hidden;">
615
+ <div style="display: flex; gap: 4px;">
616
+ <div id="nav" class="card" style="overflow: auto; height: 100vh; text-align: left; margin-top: 0;">
617
+ <a href="https://primer-id.org" target="_BLANK">
618
+ <h3 style="margin: 24px; font-weight: 600; color: #333 !important">TCS Log</h3>
619
+ </a>
620
+ '+library_links+'
621
+ </div>
622
+ <div id="pages" style="flex: 1; overflow: auto; height: 100vh;">
623
+ <div style="display: flex; flex-direction: column; align-items: center; height: 100vh;" id="basic-statistics" class="page">
624
+ <div class="error-message"></div>
625
+ <div class="card" style="margin-top: 16px; width: 100%;">
626
+ <table id="home-table">
627
+ <tr>
628
+ <td>Batch Name</td>
629
+ <td>'+batch_name+'</td>
630
+ </tr>
631
+ <tr>
632
+ <td>Processed Time</td>
633
+ <td>'+Time.now.strftime("%m/%d/%Y")+'</td>
634
+ </tr>
635
+ <tr>
636
+ <td>TCS Version</td>
637
+ <td>'+ViralSeq::TCS_VERSION+'</td>
638
+ </tr>
639
+ <tr>
640
+ <td>viral_seq Version</td>
641
+ <td>'+ViralSeq::VERSION+'</td>
642
+ </tr>
643
+ <tr>
644
+ <td>Number of Libraries</td>
645
+ <td>'+lib_names.length.to_s+'</td>
646
+ </tr>
647
+ <tr>
648
+ <td>Total Reads</td>
649
+ <td>'+total_reads+'</td>
650
+ </tr>
651
+ </table>
652
+ </div>
653
+ <div class="card" style="margin-top: 16px; width: 100%;">
654
+ <h2>Raw Sequences Distribution</h2>
655
+ <div id="raw-sequence-chart"></div>
656
+ </div>
657
+ </div>
658
+ '+library_pages+'
659
+ </div>
660
+ </div>
661
+ </div>
662
+ </body>
663
+ <style>
664
+ body {
665
+ font-size: 1.2rem;
666
+ color: #333;
667
+ }
668
+ #nav ul {
669
+ list-style-type: none;
670
+ }
671
+ .page {
672
+ margin: 16px;
673
+ }
674
+ .card {
675
+ padding: 16px;
676
+ text-align: center;
677
+ }
678
+ .hidden {
679
+ display: none !important;
680
+ visibility: hidden !important;
681
+ }
682
+ .pointer {
683
+ cursor: pointer;
684
+ }
685
+ #home-table {
686
+ margin: 0 auto;
687
+ width: 80%;
688
+ border-collapse: collapse;
689
+ }
690
+ #home-table, #home-table th, #home-table td {
691
+ border: 1px solid black;
692
+ }
693
+ #home-table td {
694
+ padding: 8px;
695
+ }
696
+ .current_page {
697
+ border-bottom: 1px solid blue;
698
+ }
699
+ .error-message {
700
+ background: red !important;
701
+ color: white;
702
+ border-radius: 15px;
703
+ font-weight: bold;
704
+ font-size: 1.8rem;
705
+ text-align: center;
706
+ padding: 1%;
707
+ margin: 16px auto;
708
+ }
709
+ .error-message:empty {
710
+ display: none;
711
+ }
712
+ </style>
713
+ </html>
714
+ '.shorten_html
715
+
716
+ File.open(File.join(tcs_dir,"log.html"), 'w') { |file| file.write(html) }