BammetricsReport.scala 15 KB
Newer Older
Peter van 't Hof's avatar
Peter van 't Hof committed
1 2 3 4 5 6 7 8 9 10
/**
 * Biopet is built on top of GATK Queue for building bioinformatic
 * pipelines. It is mainly intended to support LUMC SHARK cluster which is running
 * SGE. But other types of HPC that are supported by GATK Queue (such as PBS)
 * should also be able to execute Biopet tools and pipelines.
 *
 * Copyright 2014 Sequencing Analysis Support Core - Leiden University Medical Center
 *
 * Contact us at: sasc@lumc.nl
 *
11
 * A dual licensing mode is applied. The source code within this project is freely available for non-commercial use under an AGPL
Peter van 't Hof's avatar
Peter van 't Hof committed
12 13 14
 * license; For commercial users or users who do not want to follow the AGPL
 * license, please contact us to obtain a separate license.
 */
15 16
package nl.lumc.sasc.biopet.pipelines.bammetrics

Peter van 't Hof's avatar
Peter van 't Hof committed
17
import java.io.{ File, PrintWriter }
18

Peter van 't Hof's avatar
Peter van 't Hof committed
19
import nl.lumc.sasc.biopet.utils.config.Configurable
Peter van 't Hof's avatar
Peter van 't Hof committed
20
import nl.lumc.sasc.biopet.core.report.{ ReportBuilder, ReportBuilderExtension, ReportPage, ReportSection }
Peter van 't Hof's avatar
Peter van 't Hof committed
21
import nl.lumc.sasc.biopet.utils.ConfigUtils
Peter van 't Hof's avatar
Peter van 't Hof committed
22
import nl.lumc.sasc.biopet.utils.rscript.{ LinePlot, StackedBarPlot }
Peter van 't Hof's avatar
Peter van 't Hof committed
23
import nl.lumc.sasc.biopet.utils.summary.db.SummaryDb
Peter van 't Hof's avatar
Peter van 't Hof committed
24 25
import nl.lumc.sasc.biopet.utils.summary.db.SummaryDb.Implicts._
import nl.lumc.sasc.biopet.utils.summary.db.SummaryDb._
Peter van 't Hof's avatar
Peter van 't Hof committed
26

Peter van 't Hof's avatar
Peter van 't Hof committed
27
import scala.concurrent.{ Await, Future }
Peter van 't Hof's avatar
Peter van 't Hof committed
28
import scala.concurrent.duration.Duration
29

Peter van 't Hof's avatar
Peter van 't Hof committed
30
class BammetricsReport(val parent: Configurable) extends ReportBuilderExtension {
31
  def builder = BammetricsReport
32
}
33

34
/**
Peter van 't Hof's avatar
Peter van 't Hof committed
35 36
 * Object to create a report for [[BamMetrics]]
 *
37 38 39
 * Created by pjvan_thof on 3/30/15.
 */
object BammetricsReport extends ReportBuilder {
40

41
  /** Name of report */
42 43
  val reportName = "Bam Metrics"

44 45
  def pipelineName = "bammetrics"

46
  /** Root page for single BamMetrcis report */
Peter van 't Hof's avatar
Peter van 't Hof committed
47 48 49 50 51 52 53 54 55 56 57 58
  def indexPage: Future[ReportPage] =
    bamMetricsPage(summary, sampleId, libId).map { bamMetricsPage =>
      ReportPage(bamMetricsPage.subPages ::: List(
        "Versions" -> Future(ReportPage(List(), List("Executables" -> ReportSection("/nl/lumc/sasc/biopet/core/report/executables.ssp"
        )), Map())),
        "Files" -> filesPage(sampleId, libId)
      ), List(
        "Report" -> ReportSection("/nl/lumc/sasc/biopet/pipelines/bammetrics/bamMetricsFront.ssp")
      ) ::: bamMetricsPage.sections,
        Map()
      )
    }
59

60
  /** Generates a page with alignment stats */
Peter van 't Hof's avatar
Peter van 't Hof committed
61 62 63
  def bamMetricsPage(summary: SummaryDb,
                     sampleId: Option[Int],
                     libId: Option[Int],
Peter van 't Hof's avatar
Peter van 't Hof committed
64
                     metricsTag: String = "bammetrics"): Future[ReportPage] = Future {
Peter van 't Hof's avatar
Peter van 't Hof committed
65 66
    val wgsExecuted = summary.getStatsSize(runId, metricsTag, "wgs", sample = sampleId.map(SampleId), library = libId.map(LibraryId)) >= 1
    val rnaExecuted = summary.getStatsSize(runId, metricsTag, "rna", sample = sampleId.map(SampleId), library = libId.map(LibraryId)) >= 1
Peter van 't Hof's avatar
Peter van 't Hof committed
67

Peter van 't Hof's avatar
Peter van 't Hof committed
68 69
    val insertsizeMetrics = summary.getStatKeys(runId, metricsTag, "CollectInsertSizeMetrics",
      sampleId.map(SampleId).getOrElse(NoSample), libId.map(LibraryId).getOrElse(NoLibrary), Map("metrics" -> List("metrics")))
Peter van 't Hof's avatar
Peter van 't Hof committed
70
      .exists(_._2.isDefined)
71

Peter van 't Hof's avatar
Peter van 't Hof committed
72 73
    val targetSettings = summary.getSettingKeys(runId, metricsTag, NoModule,
      sample = sampleId.map(SampleId).getOrElse(NoSample), library = libId.map(LibraryId).getOrElse(NoLibrary),
Peter van 't Hof's avatar
Peter van 't Hof committed
74
      Map("amplicon_name" -> List("amplicon_name"), "roi_name" -> List("roi_name")))
75
    val targets = (
Peter van 't Hof's avatar
Peter van 't Hof committed
76 77
      targetSettings("amplicon_name"),
      targetSettings("roi_name")
Peter van 't Hof's avatar
Peter van 't Hof committed
78 79 80 81 82
    ) match {
        case (Some(amplicon: String), Some(roi: List[_])) => amplicon :: roi.map(_.toString)
        case (_, Some(roi: List[_])) => roi.map(_.toString)
        case _ => Nil
      }
83 84

    ReportPage(
Peter van 't Hof's avatar
Peter van 't Hof committed
85
      if (targets.isEmpty) List()
Peter van 't Hof's avatar
Peter van 't Hof committed
86
      else List("Targets" -> Future.successful(ReportPage(
87
        List(),
88
        targets.map(t => t -> ReportSection("/nl/lumc/sasc/biopet/pipelines/bammetrics/covstatsPlot.ssp", Map("target" -> Some(t)))),
Peter van 't Hof's avatar
Peter van 't Hof committed
89
        Map()))),
90
      List(
91 92 93
        "Summary" -> ReportSection("/nl/lumc/sasc/biopet/pipelines/bammetrics/alignmentSummary.ssp"),
        "Mapping Quality" -> ReportSection("/nl/lumc/sasc/biopet/pipelines/bammetrics/mappingQuality.ssp", Map("showPlot" -> true)),
        "Clipping" -> ReportSection("/nl/lumc/sasc/biopet/pipelines/bammetrics/clipping.ssp", Map("showPlot" -> true))) ++
94
        (if (insertsizeMetrics) List("Insert Size" -> ReportSection("/nl/lumc/sasc/biopet/pipelines/bammetrics/insertSize.ssp", Map("showPlot" -> true))
Peter van 't Hof's avatar
Peter van 't Hof committed
95 96
        )
        else Nil) ++ (if (wgsExecuted) List("Whole genome coverage" -> ReportSection("/nl/lumc/sasc/biopet/pipelines/bammetrics/wgsHistogram.ssp",
Peter van 't Hof's avatar
Peter van 't Hof committed
97 98 99 100 101
          Map("showPlot" -> true)))
        else Nil) ++
        (if (rnaExecuted) List("Rna coverage" -> ReportSection("/nl/lumc/sasc/biopet/pipelines/bammetrics/rnaHistogram.ssp",
          Map("showPlot" -> true)))
        else Nil),
102
      Map("metricsTag" -> metricsTag)
103 104 105
    )
  }

106 107
  /**
   * Generate a stackbar plot for alignment stats
Peter van 't Hof's avatar
Peter van 't Hof committed
108
   *
109 110 111 112 113 114
   * @param outputDir OutputDir for the tsv and png file
   * @param prefix Prefix of the tsv and png file
   * @param summary Summary class
   * @param libraryLevel Default false, when set true plot will be based on library stats instead of sample stats
   * @param sampleId Default it selects all sampples, when sample is giving it limits to selected sample
   */
115 116
  def alignmentSummaryPlot(outputDir: File,
                           prefix: String,
Peter van 't Hof's avatar
Peter van 't Hof committed
117
                           summary: SummaryDb,
118
                           libraryLevel: Boolean = false,
Peter van 't Hof's avatar
Peter van 't Hof committed
119
                           sampleId: Option[Int] = None): Unit = {
120 121 122 123 124 125
    val tsvFile = new File(outputDir, prefix + ".tsv")
    val pngFile = new File(outputDir, prefix + ".png")
    val tsvWriter = new PrintWriter(tsvFile)
    if (libraryLevel) tsvWriter.print("Library") else tsvWriter.print("Sample")
    tsvWriter.println("\tMapped\tDuplicates\tUnmapped\tSecondary")

Peter van 't Hof's avatar
Peter van 't Hof committed
126 127 128 129 130 131 132 133
    val statsPaths = Map(
      "Mapped" -> List("flagstats", "Mapped"),
      "Duplicates" -> List("flagstats", "Duplicates"),
      "All" -> List("flagstats", "All"),
      "NotPrimaryAlignment" -> List("flagstats", "NotPrimaryAlignment")
    )

    val results: Map[(Int, Option[Int]), Map[String, Option[Any]]] = if (libraryLevel) {
Peter van 't Hof's avatar
Peter van 't Hof committed
134
      summary.getStatsForLibraries(runId, "bammetrics", "bamstats",
Peter van 't Hof's avatar
Peter van 't Hof committed
135
        sampleId = sampleId, keyValues = statsPaths).map(x => (x._1._1, Some(x._1._2)) -> x._2)
Peter van 't Hof's avatar
Peter van 't Hof committed
136 137
    } else summary.getStatsForSamples(runId, "bammetrics", "bamstats",
      sample = sampleId.map(SampleId), keyValues = statsPaths).map(x => (x._1, None) -> x._2)
Peter van 't Hof's avatar
Peter van 't Hof committed
138

Peter van 't Hof's avatar
Peter van 't Hof committed
139
    for (((s, l), result) <- results) {
Peter van 't Hof's avatar
Peter van 't Hof committed
140 141
      val sampleName: String = summary.getSampleName(s).map(_.get)
      val libName: Option[String] = l.flatMap(x => Await.result(summary.getLibraryName(x), Duration.Inf))
142
      val sb = new StringBuffer()
Peter van 't Hof's avatar
Peter van 't Hof committed
143 144 145 146 147
      if (libName.isDefined) sb.append(sampleName + "-" + libName.get + "\t") else sb.append(sampleName + "\t")
      val mapped = ConfigUtils.any2long(result("Mapped"))
      val duplicates = ConfigUtils.any2long(result("Duplicates"))
      val total = ConfigUtils.any2long(result("All"))
      val secondary = ConfigUtils.any2long(result("NotPrimaryAlignment"))
Peter van 't Hof's avatar
Peter van 't Hof committed
148
      sb.append((mapped - duplicates - secondary) + "\t")
149
      sb.append(duplicates + "\t")
Peter van 't Hof's avatar
Peter van 't Hof committed
150
      sb.append((total - mapped) + "\t")
151
      sb.append(secondary)
Peter van 't Hof's avatar
Peter van 't Hof committed
152
      tsvWriter.println(sb.toString)
153 154 155 156 157 158 159 160
    }

    tsvWriter.close()

    val plot = new StackedBarPlot(null)
    plot.input = tsvFile
    plot.output = pngFile
    plot.ylabel = Some("Reads")
161
    plot.width = Some(200 + (results.size * 10))
Peter van 't Hof's avatar
WIP  
Peter van 't Hof committed
162
    plot.title = Some("Aligned_reads")
163 164 165
    plot.runLocal()
  }

Peter van 't Hof's avatar
Peter van 't Hof committed
166
  def writePlotFromSummary(outputDir: File,
Peter van 't Hof's avatar
Peter van 't Hof committed
167 168 169 170 171 172 173
                           prefix: String,
                           summary: SummaryDb,
                           libraryLevel: Boolean = false,
                           sampleId: Option[Int] = None,
                           libraryId: Option[Int] = None,
                           statsPaths: Map[String, List[String]],
                           yKey: String,
Peter van 't Hof's avatar
Peter van 't Hof committed
174
                           xKey: String,
Peter van 't Hof's avatar
Peter van 't Hof committed
175 176
                           pipeline: PipelineQuery,
                           module: ModuleQuery,
Peter van 't Hof's avatar
Peter van 't Hof committed
177 178 179 180
                           xlabel: Option[String] = None,
                           ylabel: Option[String] = None,
                           title: Option[String] = None,
                           removeZero: Boolean = true): Unit = {
Peter van 't Hof's avatar
Peter van 't Hof committed
181 182 183 184 185 186
    val tsvFile = new File(outputDir, prefix + ".tsv")
    val pngFile = new File(outputDir, prefix + ".png")

    val results: Map[(Int, Option[Int]), Map[String, Option[Array[Any]]]] = if (libraryLevel) {
      summary.getStatsForLibraries(runId, pipeline, module, sampleId = sampleId, keyValues = statsPaths)
        .map(x => (x._1._1, Some(x._1._2)) -> x._2.map(x => x._1 -> x._2.map(ConfigUtils.any2list(_).toArray)))
Peter van 't Hof's avatar
Peter van 't Hof committed
187
    } else summary.getStatsForSamples(runId, pipeline, module, sample = sampleId.map(SampleId), keyValues = statsPaths)
Peter van 't Hof's avatar
Peter van 't Hof committed
188 189
      .map(x => (x._1, None) -> x._2.map(x => x._1 -> x._2.map(ConfigUtils.any2list(_).toArray)))

Peter van 't Hof's avatar
Peter van 't Hof committed
190 191
    val tables: Array[Map[String, Array[Any]]] = results.map {
      case ((sample, library), map) =>
Peter van 't Hof's avatar
Peter van 't Hof committed
192 193 194
        val sampleName = Await.result(summary.getSampleName(sample), Duration.Inf)
          .getOrElse(throw new IllegalStateException("Sample must be there"))
        val libraryName = library.flatMap(l => Await.result(summary.getLibraryName(l), Duration.Inf))
Peter van 't Hof's avatar
Peter van 't Hof committed
195 196
        Map(
          yKey -> map(yKey).getOrElse(Array()),
Peter van 't Hof's avatar
Peter van 't Hof committed
197
          (sampleName + libraryName.map("-" + _).getOrElse("")) -> map(xKey).getOrElse(Array())
Peter van 't Hof's avatar
Peter van 't Hof committed
198
        )
Peter van 't Hof's avatar
Peter van 't Hof committed
199 200 201 202 203 204 205 206
    }.toArray

    writeTableToTsv(tsvFile, mergeTables(tables, yKey), yKey)

    LinePlot(tsvFile, pngFile,
      xlabel = xlabel,
      ylabel = ylabel,
      title = title,
207
      hideLegend = results.size > 40,
Peter van 't Hof's avatar
Peter van 't Hof committed
208 209 210
      removeZero = removeZero).runLocal()
  }

211 212
  /**
   * Generate a line plot for insertsize
Peter van 't Hof's avatar
Peter van 't Hof committed
213
   *
214 215 216 217 218 219
   * @param outputDir OutputDir for the tsv and png file
   * @param prefix Prefix of the tsv and png file
   * @param summary Summary class
   * @param libraryLevel Default false, when set true plot will be based on library stats instead of sample stats
   * @param sampleId Default it selects all sampples, when sample is giving it limits to selected sample
   */
Peter van 't Hof's avatar
Peter van 't Hof committed
220
  def insertSizePlot(outputDir: File,
Peter van 't Hof's avatar
Peter van 't Hof committed
221
                     prefix: String,
Peter van 't Hof's avatar
Peter van 't Hof committed
222
                     summary: SummaryDb,
Peter van 't Hof's avatar
Peter van 't Hof committed
223
                     libraryLevel: Boolean = false,
Peter van 't Hof's avatar
Peter van 't Hof committed
224
                     sampleId: Option[Int] = None,
Peter van 't Hof's avatar
Peter van 't Hof committed
225
                     libraryId: Option[Int] = None): Unit = {
Peter van 't Hof's avatar
Peter van 't Hof committed
226 227
    val statsPaths = Map(
      "insert_size" -> List("histogram", "insert_size"),
Peter van 't Hof's avatar
Peter van 't Hof committed
228
      "count" -> List("histogram", "All_Reads.fr_count")
Peter van 't Hof's avatar
Peter van 't Hof committed
229
    )
Peter van 't Hof's avatar
Peter van 't Hof committed
230

Peter van 't Hof's avatar
Peter van 't Hof committed
231
    writePlotFromSummary(outputDir, prefix, summary, libraryLevel, sampleId, libraryId, statsPaths,
Peter van 't Hof's avatar
Peter van 't Hof committed
232
      "insert_size", "count", "bammetrics", "CollectInsertSizeMetrics",
Peter van 't Hof's avatar
Peter van 't Hof committed
233
      "Insert size", "Reads", "Insert size")
Peter van 't Hof's avatar
Peter van 't Hof committed
234
  }
Peter van 't Hof's avatar
Peter van 't Hof committed
235

236
  def mappingQualityPlot(outputDir: File,
Peter van 't Hof's avatar
Peter van 't Hof committed
237
                         prefix: String,
Peter van 't Hof's avatar
Peter van 't Hof committed
238
                         summary: SummaryDb,
Peter van 't Hof's avatar
Peter van 't Hof committed
239
                         libraryLevel: Boolean = false,
Peter van 't Hof's avatar
Peter van 't Hof committed
240
                         sampleId: Option[Int] = None,
Peter van 't Hof's avatar
Peter van 't Hof committed
241 242 243 244
                         libraryId: Option[Int] = None): Unit = {
    val statsPaths = Map(
      "mapping_quality" -> List("mapping_quality", "histogram", "values"),
      "count" -> List("mapping_quality", "histogram", "counts")
245 246
    )

Peter van 't Hof's avatar
Peter van 't Hof committed
247
    writePlotFromSummary(outputDir, prefix, summary, libraryLevel, sampleId, libraryId, statsPaths,
Peter van 't Hof's avatar
Peter van 't Hof committed
248
      "mapping_quality", "count", "bammetrics", "bamstats",
Peter van 't Hof's avatar
Peter van 't Hof committed
249
      "Mapping quality", "Reads", "Mapping quality")
250 251 252
  }

  def clippingPlot(outputDir: File,
Peter van 't Hof's avatar
Peter van 't Hof committed
253
                   prefix: String,
Peter van 't Hof's avatar
Peter van 't Hof committed
254
                   summary: SummaryDb,
Peter van 't Hof's avatar
Peter van 't Hof committed
255
                   libraryLevel: Boolean = false,
Peter van 't Hof's avatar
Peter van 't Hof committed
256
                   sampleId: Option[Int] = None,
Peter van 't Hof's avatar
Peter van 't Hof committed
257 258 259 260
                   libraryId: Option[Int] = None): Unit = {
    val statsPaths = Map(
      "clipping" -> List("clipping", "histogram", "values"),
      "count" -> List("clipping", "histogram", "counts")
261 262
    )

Peter van 't Hof's avatar
Peter van 't Hof committed
263
    writePlotFromSummary(outputDir, prefix, summary, libraryLevel, sampleId, libraryId, statsPaths,
Peter van 't Hof's avatar
Peter van 't Hof committed
264
      "clipping", "count", "bammetrics", "bamstats",
Peter van 't Hof's avatar
Peter van 't Hof committed
265
      "Clipping", "Reads", "Clipping")
266 267
  }

268 269
  /**
   * Generate a line plot for wgs coverage
Peter van 't Hof's avatar
Peter van 't Hof committed
270
   *
271 272 273 274 275 276
   * @param outputDir OutputDir for the tsv and png file
   * @param prefix Prefix of the tsv and png file
   * @param summary Summary class
   * @param libraryLevel Default false, when set true plot will be based on library stats instead of sample stats
   * @param sampleId Default it selects all sampples, when sample is giving it limits to selected sample
   */
Peter van 't Hof's avatar
Peter van 't Hof committed
277
  def wgsHistogramPlot(outputDir: File,
Peter van 't Hof's avatar
Peter van 't Hof committed
278
                       prefix: String,
Peter van 't Hof's avatar
Peter van 't Hof committed
279
                       summary: SummaryDb,
Peter van 't Hof's avatar
Peter van 't Hof committed
280
                       libraryLevel: Boolean = false,
Peter van 't Hof's avatar
Peter van 't Hof committed
281
                       sampleId: Option[Int] = None,
Peter van 't Hof's avatar
Peter van 't Hof committed
282 283
                       libraryId: Option[Int] = None): Unit = {
    val statsPaths = Map(
Peter van 't Hof's avatar
Peter van 't Hof committed
284 285
      "coverage" -> List("histogram", "coverage"),
      "count" -> List("histogram", "count")
Peter van 't Hof's avatar
Peter van 't Hof committed
286
    )
Peter van 't Hof's avatar
Peter van 't Hof committed
287

Peter van 't Hof's avatar
Peter van 't Hof committed
288
    writePlotFromSummary(outputDir, prefix, summary, libraryLevel, sampleId, libraryId, statsPaths,
Peter van 't Hof's avatar
Peter van 't Hof committed
289
      "coverage", "count", "bammetrics", "wgs",
Peter van 't Hof's avatar
Peter van 't Hof committed
290
      "Coverage", "Bases", "Whole genome coverage")
Peter van 't Hof's avatar
Peter van 't Hof committed
291
  }
Peter van 't Hof's avatar
Peter van 't Hof committed
292 293

  /**
Peter van 't Hof's avatar
Peter van 't Hof committed
294
   * Generate a line plot for rna coverage
Peter van 't Hof's avatar
Peter van 't Hof committed
295
   *
Peter van 't Hof's avatar
Peter van 't Hof committed
296 297 298 299 300 301
   * @param outputDir OutputDir for the tsv and png file
   * @param prefix Prefix of the tsv and png file
   * @param summary Summary class
   * @param libraryLevel Default false, when set true plot will be based on library stats instead of sample stats
   * @param sampleId Default it selects all sampples, when sample is giving it limits to selected sample
   */
Peter van 't Hof's avatar
Peter van 't Hof committed
302 303
  def rnaHistogramPlot(outputDir: File,
                       prefix: String,
Peter van 't Hof's avatar
Peter van 't Hof committed
304
                       summary: SummaryDb,
Peter van 't Hof's avatar
Peter van 't Hof committed
305
                       libraryLevel: Boolean = false,
Peter van 't Hof's avatar
Peter van 't Hof committed
306 307 308
                       sampleId: Option[Int] = None,
                       libraryId: Option[Int] = None): Unit = {
    val statsPaths = Map(
Peter van 't Hof's avatar
Peter van 't Hof committed
309 310
      "position" -> List("histogram", "normalized_position"),
      "count" -> List("histogram", "All_Reads.normalized_coverage")
Peter van 't Hof's avatar
Peter van 't Hof committed
311
    )
Peter van 't Hof's avatar
Peter van 't Hof committed
312

Peter van 't Hof's avatar
Peter van 't Hof committed
313
    writePlotFromSummary(outputDir, prefix, summary, libraryLevel, sampleId, libraryId, statsPaths,
Peter van 't Hof's avatar
Peter van 't Hof committed
314
      "position", "count", "bammetrics", "rna",
Peter van 't Hof's avatar
Peter van 't Hof committed
315
      "Relative position", "Coverage", "Rna coverage")
Peter van 't Hof's avatar
Peter van 't Hof committed
316 317
  }

Peter van 't Hof's avatar
Peter van 't Hof committed
318 319
  def mergeTables(tables: Array[Map[String, Array[Any]]],
                  mergeColumn: String, defaultValue: Any = 0): Map[String, Array[Any]] = {
Peter van 't Hof's avatar
Peter van 't Hof committed
320 321 322 323 324 325 326
    val keys = tables.flatMap(x => x(mergeColumn)).distinct
    (for (table <- tables; (columnKey, columnValues) <- table if columnKey != mergeColumn) yield {
      columnKey -> keys.map(x => table(mergeColumn).zip(columnValues).toMap.getOrElse(x, defaultValue))
    }).toMap + (mergeColumn -> keys)
  }

  def writeTableToTsv(tsvFile: File, table: Map[String, Array[Any]], firstColumn: String): Unit = {
Peter van 't Hof's avatar
Peter van 't Hof committed
327
    require(table.map(_._2.size).toList.distinct.size == 1, "Not all values has the same number or rows")
Peter van 't Hof's avatar
Peter van 't Hof committed
328 329 330
    val keys = table.keys.filterNot(_ == firstColumn).toList.sorted
    val writer = new PrintWriter(tsvFile)
    writer.println((firstColumn :: keys).mkString("\t"))
Peter van 't Hof's avatar
Peter van 't Hof committed
331 332 333
    table(firstColumn).zipWithIndex.foreach {
      case (c, i) =>
        writer.println((c :: keys.map(x => table(x)(i))).mkString("\t"))
Peter van 't Hof's avatar
Peter van 't Hof committed
334 335 336
    }
    writer.close()
  }
337
}