Parses a LCOV report to coverage data.
10887
async function parseReport() { // eslint-disable-line no-unused-vars
  try {
    const coverage = await promises.readFile('lcov.info', 'utf8');
    const report = Report.fromCoverage(coverage);
    console.log(`The coverage report contains ${report.records.length} records:`);
    console.log(report.toJSON());
  }

  catch (error) {
    console.log(`An error occurred: ${error.message}`);
  }
}
1190900
def to_lcov_info(cov, out: "", test_name: nil)
      out << "TN:#{ test_name }\n"
      cov.each do |path, runs|
        out << "SF:#{ path }\n"

        # function coverage
        if runs.is_a?(Hash) && runs[:methods]
          total = covered = 0
          runs[:methods].each do |(klass, name, lineno), run|
            out << "FN:#{ lineno },#{ klass }##{ name }\n"
            total += 1
            covered += 1 if run > 0
          end
          out << "FNF:#{ total }\n"
          out << "FNF:#{ covered }\n"
          runs[:methods].each do |(klass, name, _), run|
            out << "FNDA:#{ run },#{ klass }##{ name }\n"
          end
        end

        # line coverage
        if runs.is_a?(Array) || (runs.is_a?(Hash) && runs[:lines])
          total = covered = 0
          lines = runs.is_a?(Array) ? runs : runs[:lines]
          lines.each_with_index do |run, lineno|
            next unless run
            out << "DA:#{ lineno + 1 },#{ run }\n"
            total += 1
            covered += 1 if run > 0
          end
          out << "LF:#{ total }\n"
          out << "LH:#{ covered }\n"
        end

        # branch coverage
        if runs.is_a?(Hash) && runs[:branches]
          total = covered = 0
          id = 0
          runs[:branches].each do |(_base_type, _, base_lineno), targets|
            i = 0
            targets.each do |(_target_type, _target_lineno), run|
              out << "BRDA:#{ base_lineno },#{ id },#{ i },#{ run }\n"
              total += 1
              covered += 1 if run > 0
              i += 1
            end
            id += 1
          end
          out << "BRF:#{ total }\n"
          out << "BRH:#{ covered }\n"
        end

        out << "end_of_record\n"
      end

      out
    end
728043
def parse_gcov_file(args, fobj, filename):
    
    coverage = []
    ignoring = False
    for line in fobj:
        report_fields = line.decode('utf-8', 'replace').split(':', 2)
        if len(report_fields) == 1:
            continue
        line_num = report_fields[1].strip()
        if line_num == '':
            continue

        cov_num = report_fields[0].strip()
        line_num = int(line_num)
        text = report_fields[2]
        if line_num == 0:
            continue
        if re.search(r'\bLCOV_EXCL_START\b', text):
            if ignoring:
                sys.stderr.write("Warning: %s:%d: nested LCOV_EXCL_START, "
                                 "please fix\n" % (filename, line_num))
            ignoring = True
        elif re.search(r'\bLCOV_EXCL_(STOP|END)\b', text):
            if not ignoring:
                sys.stderr.write("Warning: %s:%d: LCOV_EXCL_STOP outside of "
                                 "exclusion zone, please fix\n" % (filename,
                                                                   line_num))
            if 'LCOV_EXCL_END' in text:
                sys.stderr.write("Warning: %s:%d: LCOV_EXCL_STOP is the "
                                 "correct keyword\n" % (filename, line_num))
            ignoring = False
        if cov_num == '-':
            coverage.append(None)
        elif cov_num == '#####':
            # Avoid false positives.
            if (
                ignoring or
                any([re.search(pattern, text) for pattern in args.exclude_lines_pattern])
            ):
                coverage.append(None)
            else:
                coverage.append(0)
        elif cov_num == '=====':
            # This is indicitive of a gcov output parse
            # error.
            coverage.append(0)
        else:
            coverage.append(int(cov_num.rstrip('*')))
    return coverage
701388
def parse_reports(self):
    

    # Set up vars
    self.picard_insertSize_data = dict()
    self.picard_insertSize_histogram = dict()
    self.picard_insertSize_samplestats = dict()

    # Go through logs and find Metrics
    for f in self.find_log_files('picard/insertsize', filehandles=True):
        s_name = None
        in_hist = False
        for l in f['f']:

            # Catch the histogram values
            if s_name is not None and in_hist is True:
                try:
                    sections = l.split("\t")
                    ins = int(sections[0])
                    tot_count = sum( [int(x) for x in sections[1:]] )
                    self.picard_insertSize_histogram[s_name][ins] = tot_count
                    self.picard_insertSize_samplestats[s_name]['total_count'] += tot_count
                except ValueError:
                    # Reset in case we have more in this log file
                    s_name = None
                    in_hist = False

            # New log starting
            if 'InsertSizeMetrics' in l and 'INPUT' in l:
                s_name = None
                # Pull sample name from input
                fn_search = re.search(r"INPUT(?:=|\s+)(\[?[^\s]+\]?)", l, flags=re.IGNORECASE)
                if fn_search:
                    s_name = os.path.basename(fn_search.group(1).strip('[]'))
                    s_name = self.clean_s_name(s_name, f['root'])

            if s_name is not None:
                if 'InsertSizeMetrics' in l and '## METRICS CLASS' in l:
                    if s_name in self.picard_insertSize_data:
                        log.debug("Duplicate sample name found in {}! Overwriting: {}".format(f['fn'], s_name))
                    self.add_data_source(f, s_name, section='InsertSizeMetrics')
                    keys = f['f'].readline().strip("\n").split("\t")
                    vals = f['f'].readline().strip("\n").split("\t")
                    self.picard_insertSize_samplestats[s_name] = {'total_count': 0, 'meansum':0, 'total_pairs':0 }
                    orientation_idx = keys.index('PAIR_ORIENTATION')
                    while len(vals) == len(keys):
                        pair_orientation = vals[orientation_idx]
                        rowkey = '{}_{}'.format(s_name, pair_orientation)
                        self.picard_insertSize_data[rowkey] = OrderedDict()
                        self.picard_insertSize_data[rowkey]['SAMPLE_NAME'] = s_name
                        for i, k in enumerate(keys):
                            try:
                                self.picard_insertSize_data[rowkey][k] = float(vals[i])
                            except ValueError:
                                try:
                                    self.picard_insertSize_data[rowkey][k] = float(vals[i].replace(',','.'))
                                    log.debug("Switching commas for points in '{}': {} - {}".format(f['fn'], vals[i], vals[i].replace(',','.')))
                                except ValueError:
                                    self.picard_insertSize_data[rowkey][k] = vals[i]
                            except IndexError:
                                pass # missing data
                        # Add to mean sums
                        rp = self.picard_insertSize_data[rowkey]['READ_PAIRS']
                        mis = self.picard_insertSize_data[rowkey]['MEAN_INSERT_SIZE']
                        self.picard_insertSize_samplestats[s_name]['meansum'] += (rp * mis)
                        self.picard_insertSize_samplestats[s_name]['total_pairs'] += rp

                        vals = f['f'].readline().strip("\n").split("\t")

                    # Skip lines on to histogram
                    l = f['f'].readline().strip("\n")
                    l = f['f'].readline().strip("\n")

                    self.picard_insertSize_histogram[s_name] = OrderedDict()
                    in_hist = True

        for key in list(self.picard_insertSize_data.keys()):
            if len(self.picard_insertSize_data[key]) == 0:
                self.picard_insertSize_data.pop(key, None)
        for s_name in list(self.picard_insertSize_histogram.keys()):
            if len(self.picard_insertSize_histogram[s_name]) == 0:
                self.picard_insertSize_histogram.pop(s_name, None)
                log.debug("Ignoring '{}' histogram as no data parsed".format(s_name))

    # Calculate summed mean values for all read orientations
    for s_name, v in self.picard_insertSize_samplestats.items():
        self.picard_insertSize_samplestats[s_name]['summed_mean'] = v['meansum'] / v['total_pairs']

    # Calculate summed median values for all read orientations
    for s_name in self.picard_insertSize_histogram:
        j = 0
        for idx, c in self.picard_insertSize_histogram[s_name].items():
            j += c
            if j > (self.picard_insertSize_samplestats[s_name]['total_count'] / 2):
                self.picard_insertSize_samplestats[s_name]['summed_median'] = idx
                break


    # Filter to strip out ignored sample names
    self.picard_insertSize_data = self.ignore_samples(self.picard_insertSize_data)

    if len(self.picard_insertSize_data) > 0:

        # Write parsed data to a file
        self.write_data_file(self.picard_insertSize_data, 'multiqc_picard_insertSize')

        # Do we have median insert sizes?
        missing_medians = False
        for v in self.picard_insertSize_samplestats.values():
            if 'summed_median' not in v:
                missing_medians = True

        # Add to general stats table
        self.general_stats_headers['summed_median'] = {
            'title': 'Insert Size',
            'description': 'Median Insert Size, all read orientations (bp)',
            'min': 0,
            'suffix': ' bp',
            'format': '{:,.0f}',
            'scale': 'GnBu',
        }
        self.general_stats_headers['summed_mean'] = {
            'title': 'Mean Insert Size',
            'description': 'Mean Insert Size, all read orientations (bp)',
            'min': 0,
            'suffix': ' bp',
            'format': '{:,.0f}',
            'scale': 'GnBu',
            'hidden': False if missing_medians else True
        }
        for s_name in self.picard_insertSize_samplestats:
            if s_name not in self.general_stats_data:
                self.general_stats_data[s_name] = dict()
            self.general_stats_data[s_name].update( self.picard_insertSize_samplestats[s_name] )

        # Section with histogram plot
        if len(self.picard_insertSize_histogram) > 0:
            # Make a normalised percentage version of the data
            data_percent = {}
            for s_name, data in self.picard_insertSize_histogram.items():
                data_percent[s_name] = OrderedDict()
                total = float( sum( data.values() ) )
                for k, v in data.items():
                    data_percent[s_name][k] = (v/total)*100

            # Allow customisation of how smooth the the plot is
            try:
                insertsize_smooth_points = int(config.picard_config['insertsize_smooth_points'])
                log.debug("Custom Picard insert size smoothing: {}".format(insertsize_smooth_points))
            except (AttributeError, KeyError, ValueError):
                insertsize_smooth_points = 500

            # Plot the data and add section
            pconfig = {
                'smooth_points': insertsize_smooth_points,
                'smooth_points_sumcounts': [True, False],
                'id': 'picard_insert_size',
                'title': 'Picard: Insert Size',
                'ylab': 'Count',
                'xlab': 'Insert Size (bp)',
                'xDecimals': False,
                'tt_label': '<b>{point.x} bp</b>: {point.y:.0f}',
                'ymin': 0,
                'data_labels': [
                    {'name': 'Counts', 'ylab': 'Coverage'},
                    {'name': 'Percentages', 'ylab': 'Percentage of Counts'}
                ]
            }
            self.add_section (
                name = 'Insert Size',
                anchor = 'picard-insertsize',
                description = 'Plot shows the number of reads at a given insert size. Reads with different orientations are summed.',
                plot = linegraph.plot([self.picard_insertSize_histogram, data_percent], pconfig)
            )


    # Return the number of detected samples to the parent module
    return len(self.picard_insertSize_data)
1059720
def parse_coverage (self, f):
        
        data = dict()
        s_names = None
        j = 1
        for l in f['f'].splitlines():
            s = l.strip().split("\t")
            if s_names is None:
                s_names = s
                for s_name in s_names:
                    data[s_name] = dict()
            else:
                for i, v in enumerate(s):
                    data[s_names[i]][j] = float(v)
                j += 1
        if f['fn'] == 'meanCoverageNorm_high.txt':
            self.rna_seqc_norm_high_cov.update(data)
        elif f['fn'] == 'meanCoverageNorm_medium.txt':
            self.rna_seqc_norm_medium_cov.update(data)
        elif f['fn'] == 'meanCoverageNorm_low.txt':
            self.rna_seqc_norm_low_cov.update(data)
351497
function configureCodeCoverage (config) {
  if (process.argv.indexOf("--coverage") === -1) {
    console.warn("Code-coverage is not enabled");
    return;
  }

  config.reporters.push("coverage");
  config.coverageReporter = {
    reporters: [
      { type: "text-summary" },
      { type: "lcov" }
    ]
  };

  config.files = config.files.map(function (file) {
    if (typeof file === "string") {
      file = file.replace(/^dist\/(.*)\.min\.js$/, "dist/$1.coverage.js");
    }
    return file;
  });
}
531990
def parse_reports(self):
    

    # Set up vars
    self.read_dist = dict()
    first_regexes = {
        'total_reads': r"Total Reads\s+(\d+)\s*",
        'total_tags': r"Total Tags\s+(\d+)\s*",
        'total_assigned_tags': r"Total Assigned Tags\s+(\d+)\s*",
    }
    second_regexes = {
        'cds_exons': r"CDS_Exons\s+(\d+)\s+(\d+)\s+([\d\.]+)\s*",
        '5_utr_exons': r"5'UTR_Exons\s+(\d+)\s+(\d+)\s+([\d\.]+)\s*",
        '3_utr_exons': r"3'UTR_Exons\s+(\d+)\s+(\d+)\s+([\d\.]+)\s*",
        'introns': r"Introns\s+(\d+)\s+(\d+)\s+([\d\.]+)\s*",
        'tss_up_1kb': r"TSS_up_1kb\s+(\d+)\s+(\d+)\s+([\d\.]+)\s*",
        'tss_up_5kb': r"TSS_up_5kb\s+(\d+)\s+(\d+)\s+([\d\.]+)\s*",
        'tss_up_10kb': r"TSS_up_10kb\s+(\d+)\s+(\d+)\s+([\d\.]+)\s*",
        'tes_down_1kb': r"TES_down_1kb\s+(\d+)\s+(\d+)\s+([\d\.]+)\s*",
        'tes_down_5kb': r"TES_down_5kb\s+(\d+)\s+(\d+)\s+([\d\.]+)\s*",
        'tes_down_10kb': r"TES_down_10kb\s+(\d+)\s+(\d+)\s+([\d\.]+)\s*",
    }

    # Go through files and parse data using regexes
    for f in self.find_log_files('rseqc/read_distribution'):
        d = dict()
        for k, r in first_regexes.items():
            r_search = re.search(r, f['f'], re.MULTILINE)
            if r_search:
                d[k] = int(r_search.group(1))
        for k, r in second_regexes.items():
            r_search = re.search(r, f['f'], re.MULTILINE)
            if r_search:
                d['{}_total_bases'.format(k)] = int(r_search.group(1))
                d['{}_tag_count'.format(k)] = int(r_search.group(2))
                d['{}_tags_kb'.format(k)] = float(r_search.group(2))

        d['other_intergenic_tag_count'] = d['total_tags']-d['total_assigned_tags']

        # Calculate some percentages for parsed file
        if 'total_tags' in d:
            t = float(d['total_tags'])
            pcts = dict()
            for k in d:
                if k.endswith('_tag_count'):
                    pk = '{}_tag_pct'.format(k[:-10])
                    pcts[pk] = (float(d[k]) / t)*100.0
            d.update(pcts)

        if len(d) > 0:
            if f['s_name'] in self.read_dist:
                log.debug("Duplicate sample name found! Overwriting: {}".format(f['s_name']))
            self.add_data_source(f, section='read_distribution')
            self.read_dist[f['s_name']] = d

    # Filter to strip out ignored sample names
    self.read_dist = self.ignore_samples(self.read_dist)

    if len(self.read_dist) > 0:

        # Write to file
        self.write_data_file(self.read_dist, 'multiqc_rseqc_read_distribution')

        # Plot bar graph of groups
        keys = OrderedDict()
        keys['cds_exons_tag_count'] = {'name': "CDS_Exons"}
        keys['5_utr_exons_tag_count'] = {'name': "5'UTR_Exons"}
        keys['3_utr_exons_tag_count'] = {'name': "3'UTR_Exons"}
        keys['introns_tag_count'] = {'name': "Introns"}
        keys['tss_up_1kb_tag_count'] = {'name': "TSS_up_1kb"}
        keys['tss_up_5kb_tag_count'] = {'name': "TSS_up_5kb"}
        keys['tss_up_10kb_tag_count'] = {'name': "TSS_up_10kb"}
        keys['tes_down_1kb_tag_count'] = {'name': "TES_down_1kb"}
        keys['tes_down_5kb_tag_count'] = {'name': "TES_down_5kb"}
        keys['tes_down_10kb_tag_count'] = {'name': "TES_down_10kb"}
        keys['other_intergenic_tag_count'] = {'name': "Other_intergenic"}

        # Config for the plot
        pconfig = {
            'id': 'rseqc_read_distribution_plot',
            'title': 'RSeQC: Read Distribution',
            'ylab': '# Tags',
            'cpswitch_counts_label': 'Number of Tags',
            'cpswitch_c_active': False
        }

        self.add_section (
            name = 'Read Distribution',
            anchor = 'rseqc-read_distribution',
            description = '<a href="http://rseqc.sourceforge.net/#read-distribution-py" target="_blank">Read Distribution</a>' \
                " calculates how mapped reads are distributed over genome features.",
            plot = bargraph.plot(self.read_dist, keys, pconfig)
        )

    # Return number of samples found
    return len(self.read_dist)
529817
def parse_single_report(f):
    
    # Fixme: Separate GATKReport parsing and data subsetting. A GATKReport parser now available from the GATK MultiqcModel.

    data = dict()
    in_CompOverlap = False
    in_CountVariants = False
    in_TiTv = False
    for l in f:
        # Detect section headers
        if '#:GATKTable:CompOverlap' in l:
            in_CompOverlap = True
        elif '#:GATKTable:CountVariants' in l:
            in_CountVariants = True
        elif '#:GATKTable:TiTvVariantEvaluator' in l:
            in_TiTv = True
        else:
            # Parse contents using nested loops
            if in_CompOverlap:
                headers = l.split()
                while in_CompOverlap:
                    l = f.readline().strip("\n")
                    d = dict()
                    try:
                        for i, s in enumerate(l.split()):
                            d[headers[i]] = s
                        if d['Novelty'] == 'all':
                            data['reference'] = d['CompRod']
                            data['comp_rate'] = float(d['compRate'])
                            data['concordant_rate'] = float(d['concordantRate'])
                            data['eval_variants'] = int(d['nEvalVariants'])
                            data['novel_sites'] = int(d['novelSites'])
                        elif d['Novelty'] == 'known':
                            data['known_sites'] = int(d['nEvalVariants'])
                    except KeyError:
                        in_CompOverlap = False
            elif in_CountVariants:
                headers = l.split()
                while in_CountVariants:
                    l = f.readline().strip("\n")
                    d = dict()
                    try:
                        for i, s in enumerate(l.split()):
                            d[headers[i]] = s
                        if d['Novelty'] == 'all':
                            data['snps'] = int(d['nSNPs'])
                            data['mnps'] = int(d['nMNPs'])
                            data['insertions'] = int(d['nInsertions'])
                            data['deletions'] = int(d['nDeletions'])
                            data['complex'] = int(d['nComplex'])
                            data['symbolic'] = int(d['nSymbolic'])
                            data['mixed'] = int(d['nMixed'])
                            data['nocalls'] = int(d['nNoCalls'])
                    except KeyError:
                        in_CountVariants = False
            elif in_TiTv:
                headers = l.split()
                data['titv_reference'] = 'unknown'
                while in_TiTv:
                    l = f.readline().strip("\n")
                    d = dict()
                    try:
                        for i, s in enumerate(l.split()):
                            d[headers[i]] = s
                        if d['Novelty'] == 'known':
                            data['titv_reference'] = d['CompRod']
                            data['known_titv'] = float(d['tiTvRatio'])
                        elif d['Novelty'] == 'novel':
                            data['novel_titv'] = float(d['tiTvRatio'])
                    except KeyError:
                        in_TiTv = False

    return data
1324288
def parse_report(self, buf):
        """"""
        dpad = buf[5] % 16

        return DS4Report(
            # Left analog stick
            buf[1], buf[2],

            # Right analog stick
            buf[3], buf[4],

            # L2 and R2 analog
            buf[8], buf[9],

            # DPad up, down, left, right
            (dpad in (0, 1, 7)), (dpad in (3, 4, 5)),
            (dpad in (5, 6, 7)), (dpad in (1, 2, 3)),

            # Buttons cross, circle, square, triangle
            (buf[5] & 32) != 0, (buf[5] & 64) != 0,
            (buf[5] & 16) != 0, (buf[5] & 128) != 0,

            # L1, L2 and L3 buttons
            (buf[6] & 1) != 0, (buf[6] & 4) != 0, (buf[6] & 64) != 0,

            # R1, R2,and R3 buttons
            (buf[6] & 2) != 0, (buf[6] & 8) != 0, (buf[6] & 128) != 0,

            # Share and option buttons
            (buf[6] & 16) != 0, (buf[6] & 32) != 0,

            # Trackpad and PS buttons
            (buf[7] & 2) != 0, (buf[7] & 1) != 0,

            # Acceleration
            S16LE.unpack_from(buf, 13)[0],
            S16LE.unpack_from(buf, 15)[0],
            S16LE.unpack_from(buf, 17)[0],

            # Orientation
            -(S16LE.unpack_from(buf, 19)[0]),
            S16LE.unpack_from(buf, 21)[0],
            S16LE.unpack_from(buf, 23)[0],

            # Trackpad touch 1: id, active, x, y
            buf[35] & 0x7f, (buf[35] >> 7) == 0,
            ((buf[37] & 0x0f) << 8) | buf[36],
            buf[38] << 4 | ((buf[37] & 0xf0) >> 4),

            # Trackpad touch 2: id, active, x, y
            buf[39] & 0x7f, (buf[39] >> 7) == 0,
            ((buf[41] & 0x0f) << 8) | buf[40],
            buf[42] << 4 | ((buf[41] & 0xf0) >> 4),

            # Timestamp and battery
            buf[7] >> 2,
            buf[30] % 16,

            # External inputs (usb, audio, mic)
            (buf[30] & 16) != 0, (buf[30] & 32) != 0,
            (buf[30] & 64) != 0
        )
758750
def summary(self, stream):
        """"""

        # Output coverage section header.
        if len(self.node_descs) == 1:
            self.sep(stream, '-', 'coverage: %s' % ''.join(self.node_descs))
        else:
            self.sep(stream, '-', 'coverage')
            for node_desc in sorted(self.node_descs):
                self.sep(stream, ' ', '%s' % node_desc)

        # Produce terminal report if wanted.
        if 'term' in self.cov_report or 'term-missing' in self.cov_report:
            show_missing = 'term-missing' in self.cov_report
            self.cov.report(show_missing=show_missing, ignore_errors=True, file=stream)

        # Produce annotated source code report if wanted.
        if 'annotate' in self.cov_report:
            self.cov.annotate(ignore_errors=True)
            stream.write('Coverage annotated source written next to source\n')

        # Produce html report if wanted.
        if 'html' in self.cov_report:
            self.cov.html_report(ignore_errors=True)
            stream.write('Coverage HTML written to dir %s\n' % self.cov.config.html_dir)

        # Produce xml report if wanted.
        if 'xml' in self.cov_report:
            self.cov.xml_report(ignore_errors=True)
            stream.write('Coverage XML written to file %s\n' % self.cov.config.xml_output)

        # Report on any failed slaves.
        if self.failed_slaves:
            self.sep(stream, '-', 'coverage: failed slaves')
            stream.write('The following slaves failed to return coverage data, '
                         'ensure that pytest-cov is installed on these slaves.\n')
            for node in self.failed_slaves:
                stream.write('%s\n' % node.gateway.id)
303571
def parse_reports(self):
    

    # Set up vars
    self.read_dups = dict()

    # Go through files and parse data
    for f in self.find_log_files('rseqc/read_duplication_pos'):
        if f['f'].startswith('Occurrence	UniqReadNumber'):
            if f['s_name'] in self.read_dups:
                log.debug("Duplicate sample name found! Overwriting: {}".format(f['s_name']))
            self.add_data_source(f, section='read_duplication')
            self.read_dups[f['s_name']] = OrderedDict()
            for l in f['f'].splitlines():
                s = l.split()
                try:
                    if int(s[0]) <= 500:
                        self.read_dups[f['s_name']][int(s[0])] = int(s[1])
                except:
                    pass

    # Filter to strip out ignored sample names
    self.read_dups = self.ignore_samples(self.read_dups)

    if len(self.read_dups) > 0:

        # Add line graph to section
        pconfig = {
            'smooth_points': 200,
            'id': 'rseqc_read_dups_plot',
            'title': 'RSeQC: Read Duplication',
            'ylab': 'Number of Reads (log10)',
            'xlab': "Occurrence of read",
            'yLog': True,
            'tt_label': "<strong>{point.x} occurrences</strong>: {point.y} reads",
        }

        self.add_section (
            name = 'Read Duplication',
            anchor = 'rseqc-read_dups',
            description = '<a href="http://rseqc.sourceforge.net/#read-duplication-py" target="_blank">read_duplication.py</a>' \
                " calculates how many alignment positions have a certain number of exact duplicates."\
                " Note - plot truncated at 500 occurrences and binned.</p>",
            plot = linegraph.plot(self.read_dups, pconfig)
        )

    # Return number of samples found
    return len(self.read_dups)
530989
def parse_report(self):
        
        logging.info('Parsing Plasmid Extractor outputs')
        # A dictionary to store the parsed excel file in a more readable format
        nesteddictionary = dict()
        # Use pandas to read in the CSV file, and convert the pandas data frame to a dictionary (.to_dict())
        dictionary = pandas.read_csv(self.plasmid_report).to_dict()
        # Iterate through the dictionary - each header from the CSV file
        for header in dictionary:
            # Sample is the primary key, and value is the value of the cell for that primary key + header combination
            for sample, value in dictionary[header].items():
                # Update the dictionary with the new data
                try:
                    nesteddictionary[sample].update({header: value})
                # Create the nested dictionary if it hasn't been created yet
                except KeyError:
                    nesteddictionary[sample] = dict()
                    nesteddictionary[sample].update({header: value})
        # Get the results into the metadata object
        for sample in self.metadata:
            # Initialise the plasmid extractor genobject
            setattr(sample, self.analysistype, GenObject())
            # Initialise the list of all plasmids
            sample[self.analysistype].plasmids = list()
            # Iterate through the dictionary of results
            for line in nesteddictionary:
                # Extract the sample name from the dictionary in a manner consistent with the rest of the COWBAT
                # pipeline e.g. 2014-SEQ-0276_S2_L001 becomes 2014-SEQ-0276
                sample_name = nesteddictionary[line]['Sample']
                # Use the filer method to extract the name
                name = list(filer([sample_name]))[0]
                # Ensure that the names match
                if name == sample.name:
                    # Append the plasmid name extracted from the dictionary to the list of plasmids
                    sample[self.analysistype].plasmids.append(nesteddictionary[line]['Plasmid'])
        # Copy the report to the folder containing all reports for the pipeline
        try:
            shutil.copyfile(self.plasmid_report, os.path.join(self.reportpath, 'plasmidReport.csv'))
        except IOError:
            pass
916854
def _parse_reports_by_type(self):
    

    data = dict()

    for file_meta in self.find_log_files('picard/sam_file_validation', filehandles=True):
        sample = file_meta['s_name']

        if sample in data:
            log.debug("Duplicate sample name found! Overwriting: {}".format(sample))

        filehandle = file_meta['f']
        first_line = filehandle.readline().rstrip()
        filehandle.seek(0)  # Rewind reading of the file

        if 'No errors found' in first_line:
            sample_data = _parse_no_error_report()
        elif first_line.startswith('ERROR') or first_line.startswith('WARNING'):
            sample_data = _parse_verbose_report(filehandle)
        else:
            sample_data = _parse_summary_report(filehandle)

        data[sample] = sample_data

    return data
70334
function LCov(jscoverage) {
  var fnames = Object.keys(jscoverage);

  this.output = '';

  for(var i = 0, len = fnames.length; i < len; i++) {
    var fname = fnames[i],
        data = jscoverage[fname];

    this.output += exports.reportFile(fname, data);
  }
}
620387
function coverage(filename, data) {
  var ret = {
    filename: filename,
    coverage: 0,
    hits: 0,
    misses: 0,
    sloc: 0,
    source: {}
  };

  data.source.forEach(function(line, num){
    num++;

    if (data[num] === 0) {
      ret.misses++;
      ret.sloc++;
    } else if (data[num] !== undefined) {
      ret.hits++;
      ret.sloc++;
    }

    ret.source[num] = {
        source: line
      , coverage: data[num] === undefined
        ? ''
        : data[num]
    };
  });

  ret.coverage = ret.hits / ret.sloc * 100;

  return ret;
}
776694
def parse_reports(self):
    

    self.qualimap_rnaseq_genome_results = dict()
    regexes = {
        'reads_aligned': r"read(?:s| pairs) aligned\s*=\s*([\d,]+)",
        'total_alignments': r"total alignments\s*=\s*([\d,]+)",
        'non_unique_alignments': r"non-unique alignments\s*=\s*([\d,]+)",
        'reads_aligned_genes': r"aligned to genes\s*=\s*([\d,]+)",
        'ambiguous_alignments': r"ambiguous alignments\s*=\s*([\d,]+)",
        'not_aligned': r"not aligned\s*=\s*([\d,]+)",
        '5_3_bias': r"5'-3' bias\s*=\s*([\d,\.]+)$",
        'reads_aligned_exonic': r"exonic\s*=\s*([\d,]+)",
        'reads_aligned_intronic': r"intronic\s*=\s*([\d,]+)",
        'reads_aligned_intergenic': r"intergenic\s*=\s*([\d,]+)",
        'reads_aligned_overlapping_exon': r"overlapping exon\s*=\s*([\d,]+)",
    }
    for f in self.find_log_files('qualimap/rnaseq/rnaseq_results'):
        d = dict()

        # Get the sample name
        s_name_regex = re.search(r"bam file\s*=\s*(.+)", f['f'], re.MULTILINE)
        if s_name_regex:
            d['bam_file'] = s_name_regex.group(1)
            s_name = self.clean_s_name(d['bam_file'], f['root'])
        else:
            log.warn("Couldn't find an input filename in genome_results file {}/{}".format(f['root'], f['fn']))
            return None

        # Check for and 'fix' European style decimal places / thousand separators
        comma_regex = re.search(r"exonic\s*=\s*[\d\.]+ \(\d{1,3},\d+%\)", f['f'], re.MULTILINE)
        if comma_regex:
            log.debug("Trying to fix European comma style syntax in Qualimap report {}/{}".format(f['root'], f['fn']))
            f['f'] = f['f'].replace('.','')
            f['f'] = f['f'].replace(',','.')

        # Go through all numeric regexes
        for k, r in regexes.items():
            r_search = re.search(r, f['f'], re.MULTILINE)
            if r_search:
                try:
                    d[k] = float(r_search.group(1).replace(',',''))
                except UnicodeEncodeError:
                    # Qualimap reports infinity (\u221e) when 3' bias denominator is zero
                    pass
                except ValueError:
                    d[k] = r_search.group(1)

        # Add to general stats table
        for k in ['5_3_bias', 'reads_aligned']:
            try:
                self.general_stats_data[s_name][k] = d[k]
            except KeyError:
                pass

        # Save results
        if s_name in self.qualimap_rnaseq_genome_results:
            log.debug("Duplicate genome results sample name found! Overwriting: {}".format(s_name))
        self.qualimap_rnaseq_genome_results[s_name] = d
        self.add_data_source(f, s_name=s_name, section='rna_genome_results')


    #### Coverage profile
    self.qualimap_rnaseq_cov_hist = dict()
    for f in self.find_log_files('qualimap/rnaseq/coverage', filehandles=True):
        s_name = self.get_s_name(f)
        d = dict()
        for l in f['f']:
            if l.startswith('#'):
                continue
            coverage, count = l.split(None, 1)
            coverage = int(round(float(coverage)))
            count = float(count)
            d[coverage] = count

        if len(d) == 0:
            log.debug("Couldn't parse contents of coverage histogram file {}".format(f['fn']))
            return None

        # Save results
        if s_name in self.qualimap_rnaseq_cov_hist:
            log.debug("Duplicate coverage histogram sample name found! Overwriting: {}".format(s_name))
        self.qualimap_rnaseq_cov_hist[s_name] = d
        self.add_data_source(f, s_name=s_name, section='rna_coverage_histogram')

    # Filter to strip out ignored sample names
    self.qualimap_rnaseq_genome_results = self.ignore_samples(self.qualimap_rnaseq_genome_results)
    self.qualimap_rnaseq_cov_hist = self.ignore_samples(self.qualimap_rnaseq_cov_hist)

    #### Plots

    # Genomic Origin Bar Graph
    # NB: Ignore 'Overlapping Exon' in report - these make the numbers add up to > 100%
    if len(self.qualimap_rnaseq_genome_results) > 0:
        gorigin_cats = OrderedDict()
        gorigin_cats['reads_aligned_exonic'] = {'name': 'Exonic'}
        gorigin_cats['reads_aligned_intronic'] = {'name': 'Intronic'}
        gorigin_cats['reads_aligned_intergenic'] = {'name': 'Intergenic'}
        gorigin_pconfig = {
            'id': 'qualimap_genomic_origin',
            'title': 'Qualimap RNAseq: Genomic Origin',
            'ylab': 'Number of reads',
            'cpswitch_c_active': False
        }
        genomic_origin_helptext = '''
        There are currently three main approaches to map reads to transcripts in an
        RNA-seq experiment: mapping reads to a reference genome to identify expressed
        transcripts that are annotated (and discover those that are unknown), mapping
        reads to a reference transcriptome, and <i>de novo</i> assembly of transcript
        sequences (<a href="https://doi.org/10.1186/s13059-016-0881-8"
        target="_blank">Conesa et al. 2016</a>).

        For RNA-seq QC analysis, QualiMap can be used to assess alignments produced by
        the first of these approaches. For input, it requires a GTF annotation file
        along with a reference genome, which can be used to reconstruct the exon
        structure of known transcripts. This allows mapped reads to be grouped by
        whether they originate in an exonic region (for QualiMap, this may include
        5&#8242; and 3&#8242; UTR regions as well as protein-coding exons), an intron,
        or an intergenic region (see the <a href="http://qualimap.bioinfo.cipf.es/doc_html/index.html"
        target="_blank">Qualimap 2 documentation</a>).

        The inferred genomic origins of RNA-seq reads are presented here as a bar graph
        showing either the number or percentage of mapped reads in each read dataset
        that have been assigned to each type of genomic region. This graph can be used
        to assess the proportion of useful reads in an RNA-seq experiment. That
        proportion can be reduced by the presence of intron sequences, especially if
        depletion of ribosomal RNA was used during sample preparation (<a href="https://doi.org/10.1038/nrg3642"
        target="_blank">Sims et al. 2014</a>). It can also be reduced by off-target
        transcripts, which are detected in greater numbers at the sequencing depths
        needed to detect poorly-expressed transcripts (<a href="https://doi.org/10.1101/gr.124321.111"
        target="_blank">Tarazona et al. 2011</a>).'''
        self.add_section (
            name = 'Genomic origin of reads',
            anchor = 'qualimap-reads-genomic-origin',
            description = 'Classification of mapped reads as originating in exonic, intronic or intergenic regions. These can be displayed as either the number or percentage of mapped reads.',
            helptext = genomic_origin_helptext,
            plot = bargraph.plot(self.qualimap_rnaseq_genome_results, gorigin_cats, gorigin_pconfig)
        )

    if len(self.qualimap_rnaseq_cov_hist) > 0:
        coverage_profile_helptext = '''
        There are currently three main approaches to map reads to transcripts in an
        RNA-seq experiment: mapping reads to a reference genome to identify expressed
        transcripts that are annotated (and discover those that are unknown), mapping
        reads to a reference transcriptome, and <i>de novo</i> assembly of transcript
        sequences (<a href="https://doi.org/10.1186/s13059-016-0881-8"
        target="_blank">Conesa et al. 2016</a>).

        For RNA-seq QC analysis, QualiMap can be used to assess alignments produced by
        the first of these approaches. For input, it requires a GTF annotation file
        along with a reference genome, which can be used to reconstruct the exon
        structure of known transcripts. QualiMap uses this information to calculate the
        depth of coverage along the length of each annotated transcript. For a set of
        reads mapped to a transcript, the depth of coverage at a given base position is
        the number of high-quality reads that map to the transcript at that position
        (<a href="https://doi.org/10.1038/nrg3642" target="_blank">Sims et al. 2014</a>).

        QualiMap calculates coverage depth at every base position of each annotated
        transcript. To enable meaningful comparison between transcripts, base positions
        are rescaled to relative positions expressed as percentage distance along each
        transcript (*0%, 1%, &#8230;, 99%*). For the set of transcripts with at least
        one mapped read, QualiMap plots the cumulative mapped-read depth (y-axis) at
        each relative transcript position (x-axis). This plot shows the gene coverage
        profile across all mapped transcripts for each read dataset. It provides a
        visual way to assess positional biases, such as an accumulation of mapped reads
        at the 3&#8242; end of transcripts, which may indicate poor RNA quality in the
        original sample (<a href="https://doi.org/10.1186/s13059-016-0881-8"
        target="_blank">Conesa et al. 2016</a>).'''
        self.add_section (
            name = 'Gene Coverage Profile',
            anchor = 'qualimap-genome-fraction-coverage',
            description = 'Mean distribution of coverage depth across the length of all mapped transcripts.',
            helptext = coverage_profile_helptext,
            plot = linegraph.plot(self.qualimap_rnaseq_cov_hist, {
                'id': 'qualimap_gene_coverage_profile',
                'title': 'Qualimap RNAseq: Coverage Profile Along Genes (total)',
                'ylab': 'Coverage',
                'xlab': 'Transcript Position (%)',
                'ymin': 0,
                'xmin': 0,
                'xmax': 100,
                'tt_label': '<b>{point.x} bp</b>: {point.y:.0f}%',
            })
        )


    #### General Stats
    self.general_stats_headers['5_3_bias'] = {
        'title': "5'-3' bias",
        'format': '{:,.2f}',
    }
    self.general_stats_headers['reads_aligned'] = {
        'title': '{} Aligned'.format(config.read_count_prefix),
        'description': 'Reads Aligned ({})'.format(config.read_count_desc),
        'min': 0,
        'scale': 'RdBu',
        'shared_key': 'read_count',
        'modify': lambda x: x * config.read_count_multiplier
    }

    # Return the number of reports we found
    return len(self.qualimap_rnaseq_genome_results.keys())
741118
def report_sections(self):
    
    # Append to self.sections list

    if len(self.qualimap_bamqc_coverage_hist) > 0:
        # Chew back on histogram to prevent long flat tail
        # (find a sensible max x - lose 1% of longest tail)
        max_x = 0
        total_bases_by_sample = dict()
        for s_name, d in self.qualimap_bamqc_coverage_hist.items():
            total_bases_by_sample[s_name] = sum(d.values())
            cumulative = 0
            for count in sorted(d.keys(), reverse=True):
                cumulative += d[count]
                if cumulative / total_bases_by_sample[s_name] > 0.01:
                    max_x = max(max_x, count)
                    break

        rates_within_threshs = dict()
        for s_name, hist in self.qualimap_bamqc_coverage_hist.items():
            total = total_bases_by_sample[s_name]
            # Make a range of depths that isn't stupidly huge for high coverage expts
            depth_range = list(range(0, max_x + 1, math.ceil(float(max_x)/400.0) if max_x > 0 else 1))
            # Check that we have our specified coverages in the list
            for c in self.covs:
                if int(c) not in depth_range:
                    depth_range.append(int(c))
            # Calculate the coverage rates for this range of coverages
            rates_within_threshs[s_name] = _calculate_bases_within_thresholds(hist, total, depth_range)
            # Add requested coverage levels to the General Statistics table
            for c in self.covs:
                if int(c) in rates_within_threshs[s_name]:
                    self.general_stats_data[s_name]['{}_x_pc'.format(c)] = rates_within_threshs[s_name][int(c)]
                else:
                    self.general_stats_data[s_name]['{}_x_pc'.format(c)] = 0

        # Section 1 - BamQC Coverage Histogram
        coverage_histogram_helptext = '''
        For a set of DNA or RNA reads mapped to a reference sequence, such as a genome
        or transcriptome, the depth of coverage at a given base position is the number
        of high-quality reads that map to the reference at that position
        (<a href="https://doi.org/10.1038/nrg3642" target="_blank">Sims et al. 2014</a>).

        QualiMap groups the bases of a reference sequence by their depth of coverage
        (*0&#215;, 1&#215;, &#8230;, N&#215;*), then plots the number of bases of the
        reference (y-axis) at each level of coverage depth (x-axis). This plot shows
        the frequency of coverage depths relative to the reference sequence for each
        read dataset, which provides an indirect measure of the level and variation of
        coverage depth in the corresponding sequenced sample.

        If reads are randomly distributed across the reference sequence, this plot
        should resemble a Poisson distribution (<a href="https://doi.org/10.1016/0888-7543(88)90007-9"
        target="_blank">Lander & Waterman 1988</a>), with a peak indicating approximate
        depth of coverage, and more uniform coverage depth being reflected in a narrower
        spread. The optimal level of coverage depth depends on the aims of the
        experiment, though it should at minimum be sufficiently high to adequately
        address the biological question; greater uniformity of coverage is generally
        desirable, because it increases breadth of coverage for a given depth of
        coverage, allowing equivalent results to be achieved at a lower sequencing depth
        (<a href="https://doi.org/10.1002/gepi.20575" target="_blank">Sampson
        et al. 2011</a>; <a href="https://doi.org/10.1038/nrg3642" target="_blank">Sims
        et al. 2014</a>). However, it is difficult to achieve uniform coverage
        depth in practice, due to biases introduced during sample preparation
        (<a href="https://doi.org/10.1016/j.yexcr.2014.01.008" target="_blank">van
        Dijk et al. 2014</a>), sequencing (<a href="https://doi.org/10.1186/gb-2013-14-5-r51"
        target="_blank">Ross et al. 2013</a>) and read mapping
        (<a href="https://doi.org/10.1038/nrg3642" target="_blank">Sims et al. 2014</a>).

        This plot may include a small peak for regions of the reference sequence with
        zero depth of coverage. Such regions may be absent from the given sample (due
        to a deletion or structural rearrangement), present in the sample but not
        successfully sequenced (due to bias in sequencing or preparation), or sequenced
        but not successfully mapped to the reference (due to the choice of mapping
        algorithm, the presence of repeat sequences, or mismatches caused by variants
        or sequencing errors). Related factors cause most datasets to contain some
        unmapped reads (<a href="https://doi.org/10.1038/nrg3642" target="_blank">Sims
        et al. 2014</a>).'''
        self.add_section (
            name = 'Coverage histogram',
            anchor = 'qualimap-coverage-histogram',
            description = 'Distribution of the number of locations in the reference genome with a given depth of coverage.',
            helptext = coverage_histogram_helptext,
            plot = linegraph.plot(self.qualimap_bamqc_coverage_hist, {
                'id': 'qualimap_coverage_histogram',
                'title': 'Qualimap BamQC: Coverage histogram',
                'ylab': 'Genome bin counts',
                'xlab': 'Coverage (X)',
                'ymin': 0,
                'xmin': 0,
                'xmax': max_x,
                'xDecimals': False,
                'tt_label': '<b>{point.x}X</b>: {point.y}',
            })
        )
        # Section 2 - BamQC cumulative coverage genome fraction
        genome_fraction_helptext = '''
        For a set of DNA or RNA reads mapped to a reference sequence, such as a genome
        or transcriptome, the depth of coverage at a given base position is the number
        of high-quality reads that map to the reference at that position, while the
        breadth of coverage is the fraction of the reference sequence to which reads
        have been mapped with at least a given depth of coverage
        (<a href="https://doi.org/10.1038/nrg3642" target="_blank">Sims et al. 2014</a>).

        Defining coverage breadth in terms of coverage depth is useful, because
        sequencing experiments typically require a specific minimum depth of coverage
        over the region of interest (<a href="https://doi.org/10.1038/nrg3642"
        target="_blank">Sims et al. 2014</a>), so the extent of the reference sequence
        that is amenable to analysis is constrained to lie within regions that have
        sufficient depth. With inadequate sequencing breadth, it can be difficult to
        distinguish the absence of a biological feature (such as a gene) from a lack
        of data (<a href="https://doi.org/10.1101/gr.7050807" target="_blank">Green 2007</a>).

        For increasing coverage depths (*1&#215;, 2&#215;, &#8230;, N&#215;*),
        QualiMap calculates coverage breadth as the percentage of the reference
        sequence that is covered by at least that number of reads, then plots
        coverage breadth (y-axis) against coverage depth (x-axis). This plot
        shows the relationship between sequencing depth and breadth for each read
        dataset, which can be used to gauge, for example, the likely effect of a
        minimum depth filter on the fraction of a genome available for analysis.'''
        self.add_section (
            name = 'Cumulative genome coverage',
            anchor = 'qualimap-cumulative-genome-fraction-coverage',
            description = 'Percentage of the reference genome with at least the given depth of coverage.',
            helptext = genome_fraction_helptext,
            plot = linegraph.plot(rates_within_threshs, {
                'id': 'qualimap_genome_fraction',
                'title': 'Qualimap BamQC: Genome fraction covered by at least X reads',
                'ylab': 'Fraction of reference (%)',
                'xlab': 'Coverage (X)',
                'ymax': 100,
                'ymin': 0,
                'xmin': 0,
                'xmax': max_x,
                'xDecimals': False,
                'tt_label': '<b>{point.x}X</b>: {point.y:.2f}%',
            })
        )

    # Section 3 - Insert size histogram
    if len(self.qualimap_bamqc_insert_size_hist) > 0:
        insert_size_helptext = '''
        To overcome limitations in the length of DNA or RNA sequencing reads,
        many sequencing instruments can produce two or more shorter reads from
        one longer fragment in which the relative position of reads is
        approximately known, such as paired-end or mate-pair reads
        (<a href="https://doi.org/10.1146/annurev-anchem-062012-092628"
        target="_blank">Mardis 2013</a>). Such techniques can extend the reach
        of sequencing technology, allowing for more accurate placement of reads
        (<a href="https://doi.org/10.1146/annurev-genom-090413-025358"
        target="_blank">Reinert et al. 2015</a>) and better resolution of repeat
        regions (<a href="https://doi.org/10.1146/annurev-genom-090413-025358"
        target="_blank">Reinert et al. 2015</a>), as well as detection of
        structural variation (<a href="https://doi.org/10.1038/nrg2958"
        target="_blank">Alkan et al. 2011</a>) and chimeric transcripts
        (<a href="https://doi.org/10.1073/pnas.0904720106"
        target="_blank">Maher et al. 2009</a>).

        All these methods assume that the approximate size of an insert is known.
        (Insert size can be defined as the length in bases of a sequenced DNA or
        RNA fragment, excluding technical sequences such as adapters, which are
        typically removed before alignment.) This plot allows for that assumption
        to be assessed. With the set of mapped fragments for a given sample, QualiMap
        groups the fragments by insert size, then plots the frequency of mapped
        fragments (y-axis) over a range of insert sizes (x-axis). In an ideal case,
        the distribution of fragment sizes for a sequencing library would culminate
        in a single peak indicating average insert size, with a narrow spread
        indicating highly consistent fragment lengths.

        QualiMap calculates insert sizes as follows: for each fragment in which
        every read mapped successfully to the same reference sequence, it
        extracts the insert size from the `TLEN` field of the leftmost read
        (see the <a href="http://qualimap.bioinfo.cipf.es/doc_html/index.html"
        target="_blank">Qualimap 2 documentation</a>), where the `TLEN` (or
        'observed Template LENgth') field contains 'the number of bases from the
        leftmost mapped base to the rightmost mapped base'
        (<a href="https://samtools.github.io/hts-specs/" target="_blank">SAM
        format specification</a>). Note that because it is defined in terms of
        alignment to a reference sequence, the value of the `TLEN` field may
        differ from the insert size due to factors such as alignment clipping,
        alignment errors, or structural variation or splicing in a gap between
        reads from the same fragment.'''
        self.add_section (
            name = 'Insert size histogram',
            anchor = 'qualimap-insert-size-histogram',
            description = 'Distribution of estimated insert sizes of mapped reads.',
            helptext = insert_size_helptext,
            plot = linegraph.plot(self.qualimap_bamqc_insert_size_hist, {
                'id': 'qualimap_insert_size',
                'title': 'Qualimap BamQC: Insert size histogram',
                'ylab': 'Fraction of reads',
                'xlab': 'Insert Size (bp)',
                'ymin': 0,
                'xmin': 0,
                'tt_label': '<b>{point.x} bp</b>: {point.y}',
            })
        )

    # Section 4 - GC-content distribution
    if len(self.qualimap_bamqc_gc_content_dist) > 0:
        gc_content_helptext = '''
        GC bias is the difference between the guanine-cytosine content
        (GC-content) of a set of sequencing reads and the GC-content of the DNA
        or RNA in the original sample. It is a well-known issue with sequencing
        systems, and may be introduced by PCR amplification, among other factors
        (<a href="https://doi.org/10.1093/nar/gks001" target="_blank">Benjamini
        & Speed 2012</a>; <a href="https://doi.org/10.1186/gb-2013-14-5-r51"
        target="_blank">Ross et al. 2013</a>).

        QualiMap calculates the GC-content of individual mapped reads, then
        groups those reads by their GC-content (*1%, 2%, &#8230;, 100%*), and
        plots the frequency of mapped reads (y-axis) at each level of GC-content
        (x-axis). This plot shows the GC-content distribution of mapped reads
        for each read dataset, which should ideally resemble that of the
        original sample. It can be useful to display the GC-content distribution
        of an appropriate reference sequence for comparison, and QualiMap has an
        option to do this (see the <a href="http://qualimap.bioinfo.cipf.es/doc_html/index.html"
        target="_blank">Qualimap 2 documentation</a>).'''
        extra_series = []
        for i, (species_name, species_data) in enumerate(sorted(self.qualimap_bamqc_gc_by_species.items())):
            extra_series.append({
                'name': species_name,
                'data': list(species_data.items()),
                'dashStyle': 'Dash',
                'lineWidth': 1,
                'color': ['#000000', '#E89191'][i % 2],
            })
        if len(self.qualimap_bamqc_gc_content_dist) == 1:
            desc = 'The solid line represents the distribution of GC content of mapped reads for the sample.'
        else:
            desc = 'Each solid line represents the distribution of GC content of mapped reads for a given sample.'
        lg_config = {
            'id': 'qualimap_gc_content',
            'title': 'Qualimap BamQC: GC content distribution',
            'ylab': 'Fraction of reads',
            'xlab': 'GC content (%)',
            'ymin': 0,
            'xmin': 0,
            'xmax': 100,
            'tt_label': '<b>{point.x}%</b>: {point.y:.3f}'
        }
        if len(extra_series) == 1:
            desc += ' The dotted line represents a pre-calculated GC distribution for the reference genome.'
            lg_config['extra_series'] = extra_series
        elif len(extra_series) > 1:
            desc += ' Each dotted line represents a pre-calculated GC distribution for a specific reference genome.'
            lg_config['extra_series'] = extra_series

        self.add_section (
            name = 'GC content distribution',
            anchor = 'qualimap-gc-distribution',
            description = desc,
            helptext = gc_content_helptext,
            plot = linegraph.plot(self.qualimap_bamqc_gc_content_dist, lg_config)
        )