#!/usr/bin/env python

""" MultiQC module to parse output from Slamdunk """

from __future__ import print_function
import logging
import re
from collections import OrderedDict

from multiqc import config
from multiqc.modules.base_module import BaseMultiqcModule
from multiqc.plots import table, bargraph, linegraph, scatter

# Initialise the logger
log = logging.getLogger(__name__)

class MultiqcModule(BaseMultiqcModule):
    """
    Slamdunk module class, parses slamdunk logs.
    """

    def __init__(self):

        # Initialise the parent object
        super(MultiqcModule, self).__init__(name='Slamdunk', anchor='slamdunk',
        href='http://t-neumann.github.io/slamdunk/',
        info="is a tool to analyze SLAMSeq data.")

        num_reports = 0

        self.plot_cols = [
            '#fdbf6f','#2171B5','#6BAED6','#C6DBEF','#74C476','#C7E9C0',
            '#D9D9D9','#969696','#525252','#DADAEB','#9E9AC8','#6A51A3'
        ]

        # Summary Reports
        self.slamdunk_data = dict()
        for f in self.find_log_files('slamdunk/summary', filehandles = True):
            self.parseSummary(f)
        self.slamdunk_data = self.ignore_samples(self.slamdunk_data)
        if len(self.slamdunk_data) > 0:
            self.slamdunkGeneralStatsTable()
            self.slamdunkFilterStatsTable()
            log.debug("Found {} summary reports".format(len(self.slamdunk_data)))
            num_reports = max(num_reports, len(self.slamdunk_data))

        # PCA Plots
        self.PCA_data = dict()
        for f in self.find_log_files('slamdunk/PCA', filehandles = True):
            self.parsePCA(f)
        self.PCA_data = self.ignore_samples(self.PCA_data)
        if len(self.PCA_data) > 0:
            self.slamdunkPCAPlot()
            log.debug("Found {} PCA plots".format(len(self.PCA_data)))
            num_reports = max(num_reports, len(self.PCA_data))

        # UTR Rate reports
        self.utrates_data = dict()
        for f in self.find_log_files('slamdunk/utrrates', filehandles = True):
            self.parseUtrRates(f)
        self.utrates_data = self.ignore_samples(self.utrates_data)
        if len(self.utrates_data) > 0:
            self.write_data_file(self.utrates_data, 'multiqc_slamdunk_utrrates')
            self.slamdunkUtrRatesPlot()
            log.debug("Found {} UTR rate reports".format(len(self.utrates_data)))
            num_reports = max(num_reports, len(self.utrates_data))

        # Read rate reports
        self.rates_data_plus = dict()
        self.rates_data_minus = dict()
        for f in self.find_log_files('slamdunk/rates', filehandles = True):
            self.parseSlamdunkRates(f)
        self.rates_data_plus = self.ignore_samples(self.rates_data_plus)
        self.rates_data_minus = self.ignore_samples(self.rates_data_minus)
        if len(self.rates_data_plus) > 0:
            self.write_data_file(self.rates_data_plus, 'multiqc_slamdunk_readrates_plus')
            self.write_data_file(self.rates_data_minus, 'multiqc_slamdunk_readrates_minus')
            self.slamdunkOverallRatesPlot()
            log.debug("Found {} read rate reports".format(len(self.rates_data_plus)))
            num_reports = max(num_reports, len(self.rates_data_plus))

        # TCP error read rate
        self.nontc_per_readpos_plus = dict()
        self.nontc_per_readpos_minus = dict()
        self.tc_per_readpos_plus = dict()
        self.tc_per_readpos_minus = dict()
        for f in self.find_log_files('slamdunk/tcperreadpos', filehandles = True):
            self.parseSlamdunkTCPerReadpos(f)
        self.nontc_per_readpos_plus = self.ignore_samples(self.nontc_per_readpos_plus)
        self.nontc_per_readpos_minus = self.ignore_samples(self.nontc_per_readpos_minus)
        self.tc_per_readpos_plus = self.ignore_samples(self.tc_per_readpos_plus)
        self.tc_per_readpos_minus = self.ignore_samples(self.tc_per_readpos_minus)
        if len(self.tc_per_readpos_plus) > 0:
            self.write_data_file(self.tc_per_readpos_plus, 'multiqc_slamdunk_tcperreadpos_plus')
            self.write_data_file(self.nontc_per_readpos_plus, 'multiqc_slamdunk_nontcperreadpos_plus')
            self.write_data_file(self.tc_per_readpos_minus, 'multiqc_slamdunk_tcperreadpos_minus')
            self.write_data_file(self.nontc_per_readpos_minus, 'multiqc_slamdunk_nontcperreadpos_minus')
            self.slamdunkTcPerReadPosPlot()
            log.debug("Found {} TCP error read rate reports".format(len(self.tc_per_readpos_plus)))
            num_reports = max(num_reports, len(self.tc_per_readpos_plus))

        # Non-TCP error read rate
        self.nontc_per_utrpos_plus = dict()
        self.nontc_per_utrpos_minus = dict()
        self.tc_per_utrpos_plus = dict()
        self.tc_per_utrpos_minus = dict()
        for f in self.find_log_files('slamdunk/tcperutrpos', filehandles = True):
            self.parseSlamdunkTCPerUtrpos(f)
        self.nontc_per_utrpos_plus = self.ignore_samples(self.nontc_per_utrpos_plus)
        self.nontc_per_utrpos_minus = self.ignore_samples(self.nontc_per_utrpos_minus)
        self.tc_per_utrpos_plus = self.ignore_samples(self.tc_per_utrpos_plus)
        self.tc_per_utrpos_minus = self.ignore_samples(self.tc_per_utrpos_minus)
        if len(self.nontc_per_utrpos_plus) > 0:
            self.write_data_file(self.tc_per_utrpos_plus, 'multiqc_slamdunk_tcperutrpos_plus')
            self.write_data_file(self.nontc_per_utrpos_plus, 'multiqc_slamdunk_nontcperutrpos_plus')
            self.write_data_file(self.tc_per_utrpos_minus, 'multiqc_slamdunk_tcperutrpos_minus')
            self.write_data_file(self.nontc_per_utrpos_minus, 'multiqc_slamdunk_nontcperutrpos_minus')
            self.slamdunkTcPerUTRPosPlot()
            log.debug("Found {} non TCP error read rate reports".format(len(self.nontc_per_utrpos_plus)))
            num_reports = max(num_reports, len(self.nontc_per_utrpos_plus))

        if num_reports == 0:
            raise UserWarning
        else:
            log.info("Found {} reports".format(num_reports))


    def parsePCA(self, f):

        # Skip header
        next(f['f'])

        for line in f['f']:
            fields = line.rstrip().split('\t')

            sample = self.clean_s_name(fields[0],f['root'])
            PC1 = fields[1]
            PC2 = fields[2]

            self.PCA_data[sample] = [{'x': float(PC1), 'y': float(PC2)}]


    def parseUtrRates(self, f):

        # Skip comment line #
        next(f['f'])

        # Read median header
        line = next(f['f'])

        if "Conversions=" in line:

            sample = f['s_name']
            self.utrates_data[sample] = OrderedDict()

            conversions = re.sub(".*Conversions=","",line.rstrip()).split(",")

            for conversion in conversions:
                type, value = conversion.split(":")
                self.utrates_data[sample][type] = float(value)

        else:
            log.warning("Malformed UTR rates header. Conversion rates per UTR plot will be affected.")

    def parseSlamdunkRates(self, f):

        sample = f['s_name']

        # Skip comment line #
        next(f['f'])

        bases = next(f['f']).rstrip().split('\t')

        baseDict = {}
        order = {}

        for i in range(1, len(bases)):
            order[i] = bases[i]

        for line in f['f']:
            values = line.rstrip().split('\t')
            base = values[0]
            baseDict[base]= {}

            for i in range(1, len(values)):
                baseDict[base][order[i]] = int(values[i])

        divisor = {}

        for fromBase in baseDict:
            for toBase in baseDict[fromBase]:
                if(toBase.islower()):
                    if not fromBase.lower() in divisor:
                        divisor[fromBase.lower()] = 0
                    divisor[fromBase.lower()] += baseDict[fromBase][toBase]
                else:
                    if not fromBase in divisor:
                        divisor[fromBase] = 0
                    divisor[fromBase] += baseDict[fromBase][toBase]

        for fromBase in baseDict:
            for toBase in baseDict[fromBase]:
                if(toBase.islower()):
                    if divisor[fromBase.lower()] > 0:
                        baseDict[fromBase][toBase] = baseDict[fromBase][toBase] / float(divisor[fromBase.lower()]) * 100
                    else :
                        baseDict[fromBase][toBase] = 0.0
                else:
                    if divisor[fromBase] > 0:
                        baseDict[fromBase][toBase] = baseDict[fromBase][toBase] / float(divisor[fromBase]) * 100
                    else :
                        baseDict[fromBase][toBase] = 0.0

        self.rates_data_plus[sample] = {}
        self.rates_data_minus[sample] = {}

        for fromBase in baseDict:
            for toBase in baseDict[fromBase]:
                if fromBase != "N" and toBase.upper() != "N" and fromBase != toBase.upper():
                    if(toBase.islower()):
                        self.rates_data_minus[sample][fromBase + ">" + toBase.upper()] = baseDict[fromBase][toBase]
                    else:
                        self.rates_data_plus[sample][fromBase + ">" + toBase] = baseDict[fromBase][toBase]

    def parseSlamdunkTCPerReadpos(self, f):

        sample = f['s_name']

        # Skip comment line #
        next(f['f'])

        self.nontc_per_readpos_plus[sample] = {}
        self.nontc_per_readpos_minus[sample] = {}

        self.tc_per_readpos_plus[sample] = {}
        self.tc_per_readpos_minus[sample] = {}

        pos = 1

        for line in f['f']:
            values = line.rstrip().split('\t')
            if int(values[4]) > 0:
                self.nontc_per_readpos_plus[sample][pos] = float(int(values[0])) / int(values[4]) * 100
                self.tc_per_readpos_plus[sample][pos] = float(int(values[2])) / int(values[4]) * 100
            else:
                self.nontc_per_readpos_plus[sample][pos] = 0
                self.tc_per_readpos_plus[sample][pos] = 0

            if int(values[5]) > 0:
                self.nontc_per_readpos_minus[sample][pos] = float(int(values[1])) / int(values[5]) * 100
                self.tc_per_readpos_minus[sample][pos] = float(int(values[3])) / int(values[5]) * 100
            else:
                self.nontc_per_readpos_minus[sample][pos] = 0
                self.tc_per_readpos_minus[sample][pos] = 0

            pos += 1

    def parseSlamdunkTCPerUtrpos(self, f):

        sample = f['s_name']

        # Skip comment line #
        next(f['f'])

        self.nontc_per_utrpos_plus[sample] = {}
        self.nontc_per_utrpos_minus[sample] = {}

        self.tc_per_utrpos_plus[sample] = {}
        self.tc_per_utrpos_minus[sample] = {}

        pos = 1

        for line in f['f']:
            values = line.rstrip().split('\t')
            if int(values[4]) > 0:
                self.nontc_per_utrpos_plus[sample][pos] = float(int(values[0])) / int(values[4]) * 100
                self.tc_per_utrpos_plus[sample][pos] = float(int(values[2])) / int(values[4]) * 100
            else:
                self.nontc_per_utrpos_plus[sample][pos] = 0
                self.tc_per_utrpos_plus[sample][pos] = 0

            if int(values[5]) > 0:
                self.nontc_per_utrpos_minus[sample][pos] = float(int(values[1])) / int(values[5]) * 100
                self.tc_per_utrpos_minus[sample][pos] = float(int(values[3])) / int(values[5]) * 100
            else:
                self.nontc_per_utrpos_minus[sample][pos] = 0
                self.tc_per_utrpos_minus[sample][pos] = 0

            pos += 1

    def parseSummary(self, f):

        # Skip comment line #
        next(f['f'])

        # Skip header line "FileName..."
        columnCount = next(f['f']).count("\t") + 1

        for line in f['f']:

            fields = line.rstrip().split("\t")
            s_name = self.clean_s_name(fields[0],f['root'])
            self.slamdunk_data[s_name] = dict()
            self.slamdunk_data[s_name]['sequenced'] = int(fields[4])
            self.slamdunk_data[s_name]['mapped'] = int(fields[5])
            #self.slamdunk_data[s_name]['deduplicated'] = int(fields[6])
            self.slamdunk_data[s_name]['mqfiltered'] = int(fields[7])
            self.slamdunk_data[s_name]['idfiltered'] = int(fields[8])
            self.slamdunk_data[s_name]['nmfiltered'] = int(fields[9])
            self.slamdunk_data[s_name]['multimapper'] = int(fields[10])
            self.slamdunk_data[s_name]['retained'] = int(fields[11])

            # Additional Count Column found in Table
            if columnCount == 14:
                self.slamdunk_data[s_name]['counted'] = int(fields[12])

        self.add_data_source(f)

    def slamdunkGeneralStatsTable(self):
        """ Take the parsed summary stats from Slamdunk and add it to the
        basic stats table at the top of the report """


        headers = OrderedDict()
        headers['counted'] = {
            'title': '{} Counted'.format(config.read_count_prefix),
            'description': '# reads counted within 3\'UTRs ({})'.format(config.read_count_desc),
            'shared_key': 'read_count',
            'min': 0,
            'format': '{:,.2f}',
            'scale': 'YlGn',
            'modify': lambda x: float(x) * config.read_count_multiplier,
        }
        headers['retained'] = {
            'title': '{} Retained'.format(config.read_count_prefix),
            'description': '# retained reads after filtering ({})'.format(config.read_count_desc),
            'shared_key': 'read_count',
            'min': 0,
            'format': '{:,.2f}',
            'scale': 'YlGn',
            'modify': lambda x: float(x) * config.read_count_multiplier,
        }
        headers['mapped'] = {
            'title': '{} Mapped'.format(config.read_count_prefix),
            'description': '# mapped reads ({})'.format(config.read_count_desc),
            'shared_key': 'read_count',
            'min': 0,
            'format': '{:,.2f}',
            'scale': 'YlGn',
            'modify': lambda x: float(x) * config.read_count_multiplier,
        }
        headers['sequenced'] = {
            'title': '{} Sequenced'.format(config.read_count_prefix),
            'description': '# sequenced reads ({})'.format(config.read_count_desc),
            'shared_key': 'read_count',
            'min': 0,
            'format': '{:,.2f}',
            'scale': 'YlGn',
            'modify': lambda x: float(x) * config.read_count_multiplier,
        }

        self.general_stats_addcols(self.slamdunk_data, headers)

    def slamdunkFilterStatsTable(self):
        """ Take the parsed filter stats from Slamdunk and add it to a separate table """

        headers = OrderedDict()
        headers['mapped'] = {
            'namespace': 'Slamdunk',
            'title': '{} Mapped'.format(config.read_count_prefix),
            'description': '# mapped reads ({})'.format(config.read_count_desc),
            'shared_key': 'read_count',
            'min': 0,
            'format': '{:,.2f}',
            'suffix': config.read_count_prefix,
            'scale': 'YlGn',
            'modify': lambda x: float(x) * config.read_count_multiplier,
        }
        headers['multimapper'] = {
            'namespace': 'Slamdunk',
            'title': '{} Multimap-Filtered'.format(config.read_count_prefix),
            'description': '# multimap-filtered reads ({})'.format(config.read_count_desc),
            'shared_key': 'read_count',
            'min': 0,
            'format': '{:,.2f}',
            'suffix': config.read_count_prefix,
            'scale': 'OrRd',
            'modify': lambda x: float(x) * config.read_count_multiplier,
        }
        headers['nmfiltered'] = {
            'namespace': 'Slamdunk',
            'title': '{} NM-Filtered'.format(config.read_count_prefix),
            'description': '# NM-filtered reads ({})'.format(config.read_count_desc),
            'shared_key': 'read_count',
            'min': 0,
            'format': '{:,.2f}',
            'suffix': config.read_count_prefix,
            'scale': 'OrRd',
            'modify': lambda x: float(x) * config.read_count_multiplier,
        }
        headers['idfiltered'] = {
            'namespace': 'Slamdunk',
            'title': '{} Identity-Filtered'.format(config.read_count_prefix),
            'description': '# identity-filtered reads ({})'.format(config.read_count_desc),
            'shared_key': 'read_count',
            'min': 0,
            'format': '{:,.2f}',
            'suffix': config.read_count_prefix,
            'scale': 'OrRd',
            'modify': lambda x: float(x) * config.read_count_multiplier,
        }
        headers['mqfiltered'] = {
            'namespace': 'Slamdunk',
            'title': '{} MQ-Filtered'.format(config.read_count_prefix),
            'description': '# MQ-filtered reads ({})'.format(config.read_count_desc),
            'shared_key': 'read_count',
            'min': 0,
            'format': '{:,.2f}',
            'suffix': config.read_count_prefix,
            'scale': 'OrRd',
            'modify': lambda x: float(x) * config.read_count_multiplier,
        }
        pconfig = {
            'id': 'slamdunk_filtering_table',
            'min': 0,
        }

        self.add_section (
            name = 'Filter statistics',
            anchor = 'slamdunk_filtering',
            description = 'This table shows the number of reads filtered with each filter criterion during filtering phase of slamdunk.',
            plot = table.plot(self.slamdunk_data, headers, pconfig)
        )

    def slamdunkOverallRatesPlot (self):
        """ Generate the overall rates plot """

        pconfig = {
            'id': 'overallratesplot',
            'title': 'Slamdunk: Overall conversion rates in reads',
            'cpswitch': False,
            'cpswitch_c_active': False,
            'ylab': 'Number of reads',
            'stacking': 'normal',
            'tt_decimals': 2,
            'tt_suffix': '%',
            'tt_percentages': False,
            'hide_zero_cats': False,
            'data_labels': [
                "Plus Strand +",
                "Minus Strand -",
            ]
        }

        cats = [OrderedDict(), OrderedDict()]
        keys = [
            ['T>C', 'A>T', 'A>G', 'A>C', 'T>A', 'T>G', 'G>A', 'G>T', 'G>C', 'C>A', 'C>T', 'C>G'],
            ['A>G','A>T','A>C','T>A','T>G','T>C','G>A','G>T','G>C','C>A','C>T','C>G']
        ]
        for i, k in enumerate(keys):
            for j, v in enumerate(k):
                cats[i][v] = { 'color': self.plot_cols[j] }

        self.add_section (
            name = 'Conversion rates per read',
            anchor = 'slamdunk_overall_rates',
            description = """This plot shows the individual conversion rates over all reads.
                        It shows these conversion rates strand-specific: This means for a properly labeled
                        sample you would see a T&gt;C excess on the plus-strand and an A&gt;G excess on the minus strand
                        (see the <a href="http://t-neumann.github.io/slamdunk/docs.html#rates" target="_blank">slamdunk docs</a>).""",
            plot = bargraph.plot([self.rates_data_plus,self.rates_data_minus], cats, pconfig)
        )


    def slamdunkUtrRatesPlot (self):
        """ Generate the UTR rates plot """

        cats = OrderedDict()
        keys = ['T>C', 'A>T', 'A>G', 'A>C', 'T>A', 'T>G', 'G>A', 'G>T', 'G>C', 'C>A', 'C>T', 'C>G']
        for i, v in enumerate(keys):
            cats[v] = { 'color': self.plot_cols[i] }

        pconfig = {
            'id': 'slamdunk_utrratesplot',
            'title': 'Slamdunk: Overall conversion rates per UTR',
            'cpswitch': False,
            'cpswitch_c_active': False,
            'ylab': 'Number of conversions',
            'stacking': 'normal',
            'tt_decimals': 2,
            'tt_suffix': '%',
            'tt_percentages': False,
            'hide_zero_cats': False
        }

        self.add_section (
            name = 'Conversion rates per UTR',
            anchor = 'slamdunk_utr_rates',
            description = """This plot shows the individual conversion rates for all UTRs
                        (see the <a href="http://t-neumann.github.io/slamdunk/docs.html#utrrates" target="_blank">slamdunk docs</a>).""",
            plot = bargraph.plot(self.utrates_data, cats, pconfig)
        )

    def slamdunkPCAPlot (self):
        """ Generate the PCA plots """

        pconfig = {
            'id': 'slamdunk_pca',
            'title': 'Slamdunk: PCA',
            'xlab': 'PC1',
            'ylab': 'PC2',
            'tt_label': 'PC1 {point.x:.2f}: PC2 {point.y:.2f}'
        }

        self.add_section (
            name = 'PCA (T&gt;C based)',
            anchor = 'slamdunk_PCA',
            description = """This plot shows the principal components of samples based
                        on the distribution of reads with T&gt;C conversions within UTRs
                        (see the <a href="http://t-neumann.github.io/slamdunk/docs.html#summary" target="_blank">slamdunk docs</a>).""",
            plot = scatter.plot(self.PCA_data, pconfig)
        )

    def slamdunkTcPerReadPosPlot (self):
        """ Generate the tc per read pos plots """

        pconfig_nontc = {
            'id': 'slamdunk_nontcperreadpos_plot',
            'title': 'Slamdunk: Non-T>C mismatches over reads',
            'ylab': 'Percent mismatches %',
            'xlab': 'Position in read',
            'xDecimals': False,
            'ymin': 0,
            'tt_label': '<b>Pos {point.x}</b>: {point.y:.2f} %',
            'data_labels': [{'name': 'Forward reads +', 'ylab': 'Percent mismatches %'},
                            {'name': 'Reverse reads -', 'ylab': 'Percent mismatches %'}]
        }

        pconfig_tc = {
            'id': 'slamdunk_tcperreadpos_plot',
            'title': 'Slamdunk: T>C conversions over reads',
            'ylab': 'Percent converted %',
            'xlab': 'Position in read',
            'xDecimals': False,
            'ymin': 0,
            'tt_label': '<b>Pos {point.x}</b>: {point.y:.2f} %',
            'data_labels': [{'name': 'Forward reads +', 'ylab': 'Percent converted %'},
                            {'name': 'Reverse reads -', 'ylab': 'Percent converted %'}]
        }

        self.add_section (
            name = 'Non T&gt;C mismatches over read positions',
            anchor = 'slamdunk_nontcperreadpos',
            description = """This plot shows the distribution of non T&gt;C mismatches across read positions
                        (see the <a href="http://t-neumann.github.io/slamdunk/docs.html#tcperreadpos" target="_blank">slamdunk docs</a>).""",
            plot = linegraph.plot([self.nontc_per_readpos_plus, self.nontc_per_readpos_minus], pconfig_nontc)
        )

        self.add_section (
            name = 'T&gt;C conversions over read positions',
            anchor = 'slamdunk_tcperreadpos',
            description = """This plot shows the distribution of T&gt;C conversions across read positions
                        (see the <a href="http://t-neumann.github.io/slamdunk/docs.html#tcperreadpos" target="_blank">slamdunk docs</a>).""",
            plot = linegraph.plot([self.tc_per_readpos_plus, self.tc_per_readpos_minus], pconfig_tc)
        )

    def slamdunkTcPerUTRPosPlot (self):
        """ Generate the tc per UTR pos plots """

        pconfig_nontc = {
            'id': 'slamdunk_slamdunk_nontcperutrpos_plot',
            'title': 'Slamdunk: Non-T>C mutations over 3\' UTR ends',
            'ylab': 'Percent mismatches %',
            'xlab': 'Position in the static last 250bp window of 3\' UTR',
            'xDecimals': False,
            'ymin': 0,
            'tt_label': '<b>Pos {point.x}</b>: {point.y:.2f} %',
            'data_labels': [{'name': 'UTRs on plus strand', 'ylab': 'Percent mismatches %'},
                            {'name': 'UTRs on minus strand', 'ylab': 'Percent mismatches %'}]
        }

        pconfig_tc = {
            'id': 'slamdunk_slamdunk_tcperutrpos_plot',
            'title': 'Slamdunk: T>C conversions over 3\' UTR ends',
            'ylab': 'Percent converted %',
            'xlab': 'Position in the static last 250bp window of 3\' UTR',
            'xDecimals': False,
            'ymin': 0,
            'tt_label': '<b>Pos {point.x}</b>: {point.y:.2f} %',
            'data_labels': [{'name': 'UTRs on plus strand', 'ylab': 'Percent converted %'},
                            {'name': 'UTRs on minus strand', 'ylab': 'Percent converted %'}]
        }

        self.add_section (
            name = 'Non T&gt;C mismatches over UTR positions',
            anchor = 'slamdunk_nontcperutrpos',
            description = """This plot shows the distribution of non T&gt;C mismatches across UTR positions for the last 250 bp from the 3\' UTR end
                        (see the <a href="http://t-neumann.github.io/slamdunk/docs.html#tcperutrpos" target="_blank">slamdunk docs</a>).""",
            plot = linegraph.plot([self.nontc_per_utrpos_plus, self.nontc_per_utrpos_minus], pconfig_nontc)
        )

        self.add_section (
            name = 'T&gt;C conversions over UTR positions',
            anchor = 'tcperutrpos',
            description = """This plot shows the distribution of T&gt;C conversions across UTR positions for the last 250 bp from the 3\' UTR end
                        (see the <a href="http://t-neumann.github.io/slamdunk/docs.html#tcperutrpos" target="_blank">slamdunk docs</a>).""",
            plot = linegraph.plot([self.tc_per_utrpos_plus, self.tc_per_utrpos_minus], pconfig_tc)
        )
