Exemple #1
0
    def somalier_relatedness_plot(self):
        data = dict()
        alpha = 0.6
        relatedness_colours = {
            0: ['Unrelated', 'rgba(74, 124, 182, {})'.format(alpha)],
            0.49: ['Sib-sib', 'rgba(243, 123, 40, {})'.format(alpha)],
            0.5: ['Parent-child', 'rgba(159, 84, 47, {})'.format(alpha)]
        }

        # Get index colour scale
        cscale = mqc_colour.mqc_colour_scale()
        extra_colours = cscale.get_colours("Dark2")
        extra_colours = _make_col_alpha(extra_colours, alpha)

        extra_colour_idx = 0
        for s_name, d in self.somalier_data.items():
            if 'ibs0' in d and 'ibs2' in d:
                data[s_name] = {'x': d['ibs0'], 'y': d['ibs2']}
            if 'relatedness' in d:
                relatedness = d['expected_relatedness']
                # -1 is not the same family, 0 is same family but unreleaed
                # @brentp says he usually bundles them together
                if relatedness == -1:
                    relatedness = 0

                # New unique value that we've not seen before
                if relatedness not in relatedness_colours:
                    relatedness_colours[relatedness] = [
                        str(relatedness), extra_colours[extra_colour_idx]
                    ]
                    extra_colour_idx += 0
                    if extra_colour_idx > len(extra_colours):
                        extra_colour_idx = 0

                # Assign colour
                data[s_name]['color'] = relatedness_colours[relatedness][1]

        if len(data) > 0:
            pconfig = {
                'id': 'somalier_relatedness_plot',
                'title': 'Somalier: Sample Shared Allele Rates (IBS)',
                'xlab': 'IBS0 (no alleles shared)',
                'ylab': 'IBS2 (both alleles shared)',
                'marker_line_width': 0
            }

            colours_legend = ''
            for val in sorted(relatedness_colours.keys()):
                name, col_rgb = relatedness_colours[val]
                colours_legend += "<span style=\"color:{}\">{}</span>, ".format(
                    col_rgb.replace(str(alpha), "1.0"), name, val)

            self.add_section(name='Relatedness',
                             anchor='somalier-relatedness',
                             description="""
                Shared allele rates between sample pairs.
                Points are coloured by degree of expected-relatedness: {}""".
                             format(colours_legend),
                             plot=scatter.plot(data, pconfig))
Exemple #2
0
    def somalier_ancestry_pca_plot(self):
        data = OrderedDict()

        # cycle over samples and add PC coordinates to data dict
        for s_name, d in self.somalier_data.items():
            if "PC1" in d and "PC2" in d:
                data[s_name] = {
                    "x": d["PC1"],
                    "y": d["PC2"],
                    "color": "rgba(0, 0, 0, 0.6)",
                }

        # add background
        # N.B. this must be done after samples to have samples on top
        d = self.somalier_background_pcs.pop("background_pcs", {})
        if d:
            # generate color scale to match the number of categories
            c_scale = mqc_colour.mqc_colour_scale(name="Paired").colours
            cats = self.somalier_ancestry_cats
            ancestry_colors = dict(zip(cats, c_scale[: len(cats)]))
            default_background_color = "rgb(255,192,203,0.3)"

            # Make colours semi-transparent
            ancestry_colors = dict(zip(ancestry_colors.keys(), _make_col_alpha(ancestry_colors.values(), 0.3)))

            background = [
                {"x": pc1, "y": pc2, "color": ancestry_colors.get(ancestry, default_background_color), "name": ancestry}
                for pc1, pc2, ancestry in zip(d["PC1"], d["PC2"], d["ancestry"])
            ]
            data["background"] = background

        # generate section and plot
        if len(data) > 0:
            pconfig = {
                "id": "somalier_ancestry_pca_plot",
                "title": "Somalier: Sample Predicted Ancestry",
                "xlab": "PC1",
                "ylab": "PC2",
                "marker_size": 5,
                "marker_line_width": 0,
            }

            self.add_section(
                name="Ancestry PCA",
                description="Principal components of samples against background PCs.",
                helptext="""
                Sample PCs are plotted against background PCs from the
                background data supplied to somalier.
                Color indicates predicted ancestry of sample. Data points in close
                proximity are predicted to be of similar ancestry. Consider whether
                the samples cluster as expected.
                """,
                anchor="somalier-ancestry-pca",
                plot=scatter.plot(data, pconfig),
            )
Exemple #3
0
    def somalier_ancestry_barplot(self):
        data = dict()
        c_scale = mqc_colour.mqc_colour_scale(name="Paired").colours
        cats = OrderedDict()
        anc_cats = self.somalier_ancestry_cats

        # use Paired color scale, unless number of categories exceed colors
        if len(anc_cats) <= len(c_scale):
            for i in range(len(anc_cats)):
                c = anc_cats[i]
                if i < (len(c_scale) - 1):
                    col = c_scale[i]
                else:
                    # default col if more cats than cols
                    col = "rgb(211,211,211,0.5)"
                cats[c] = {"name": c, "color": col}
        else:
            cats = None

        for s_name, d in self.somalier_data.items():
            # ensure that only relevant items are added,
            # i.e. only ancestry category values
            ls = {
                k: v
                for k, v in d.items() if (k in self.somalier_ancestry_cats)
            }
            if len(ls) > 0:  # only add dict, if it contains values
                data[s_name] = ls

        if len(data) > 0:
            pconfig = {
                "id": "somalier_ancestry_barplot",
                "title": "Somalier: Sample Predicted Ancestry Proportions",
                "cpswitch_c_active": False,
                "hide_zero_cats": False,
                "ylab": "Predicted Ancestry",
            }

            self.add_section(
                name="Ancestry Barplot",
                description="Predicted ancestries of samples.",
                helptext="""
                Shows the percentwise predicted probability of each
                ancestry. A sample might contain traces of several ancestries.
                If the number of samples is too high, the plot is rendered as a
                non-interactive flat image.
                """,
                anchor="somalier-ancestry",
                plot=bargraph.plot(data=data, cats=cats, pconfig=pconfig),
            )
Exemple #4
0
    def bar_graph_position_in_protein(self):
        title = "Position in protein"
        plot_data, plot_cats, plot_config = self._prep_bar_graph(title)
        htmlid = re.sub("\W+", "_", title).lower()
        if len(plot_data) == 0:
            return

        # Nice graduated colours for categories
        colour_scale = mqc_colour.mqc_colour_scale("YlGnBu", 0,
                                                   len(plot_cats) - 1)
        for idx, k in enumerate(plot_cats):
            plot_cats[k]["color"] = colour_scale.get_colour(idx, lighten=0.8)

        plot_config["cpswitch_c_active"] = False

        self.add_section(
            name=title,
            anchor=htmlid,
            description="Relative position of affected amino acids in protein.",
            plot=bargraph.plot(plot_data, plot_cats, plot_config),
        )
Exemple #5
0
    def somalier_relatedness_plot(self):
        data = dict()
        alpha = 0.6
        relatedness_colours = {
            0: ["Unrelated", "rgba(74, 124, 182, {})".format(alpha)],
            0.49: ["Sib-sib", "rgba(243, 123, 40, {})".format(alpha)],
            0.5: ["Parent-child", "rgba(159, 84, 47, {})".format(alpha)],
        }

        # Get index colour scale
        cscale = mqc_colour.mqc_colour_scale()
        extra_colours = cscale.get_colours("Dark2")
        extra_colours = _make_col_alpha(extra_colours, alpha)

        extra_colour_idx = 0
        for s_name, d in self.somalier_data.items():
            if "ibs0" in d and "ibs2" in d:
                data[s_name] = {"x": d["ibs0"], "y": d["ibs2"]}
            if "relatedness" in d:
                relatedness = d["expected_relatedness"]
                # -1 is not the same family, 0 is same family but unreleaed
                # @brentp says he usually bundles them together
                if relatedness == -1:
                    relatedness = 0

                # New unique value that we've not seen before
                if relatedness not in relatedness_colours:
                    relatedness_colours[relatedness] = [
                        str(relatedness), extra_colours[extra_colour_idx]
                    ]
                    extra_colour_idx += 0
                    if extra_colour_idx > len(extra_colours):
                        extra_colour_idx = 0

                # Assign colour
                data[s_name]["color"] = relatedness_colours[relatedness][1]

        if len(data) > 0:
            pconfig = {
                "id": "somalier_relatedness_plot",
                "title": "Somalier: Sample Shared Allele Rates (IBS)",
                "xlab": "IBS0 (no alleles shared)",
                "ylab": "IBS2 (both alleles shared)",
                "marker_line_width": 0,
            }

            colours_legend = ""
            for val in sorted(relatedness_colours.keys()):
                name, col_rgb = relatedness_colours[val]
                colours_legend += '<span style="color:{}">{}</span>, '.format(
                    col_rgb.replace(str(alpha), "1.0"), name, val)

            self.add_section(
                name="Relatedness",
                anchor="somalier-relatedness",
                description="""
                Shared allele rates between sample pairs.
                Points are coloured by degree of expected-relatedness: {}""".
                format(colours_legend),
                plot=scatter.plot(data, pconfig),
            )
Exemple #6
0
def make_table(dt):
    """
    Build the HTML needed for a MultiQC table.
    :param data: MultiQC datatable object
    """

    table_id = dt.pconfig.get(
        'id', 'table_{}'.format(''.join(random.sample(letters, 4))))
    table_id = report.save_htmlid(table_id)
    t_headers = OrderedDict()
    t_modal_headers = OrderedDict()
    t_rows = OrderedDict()
    dt.raw_vals = defaultdict(lambda: dict())
    empty_cells = dict()
    hidden_cols = 1
    table_title = dt.pconfig.get('table_title')
    if table_title is None:
        table_title = table_id.replace("_", " ").title()

    for idx, k, header in dt.get_headers_in_order():

        rid = header['rid']

        # Build the table header cell
        shared_key = ''
        if header.get('shared_key', None) is not None:
            shared_key = ' data-shared-key={}'.format(header['shared_key'])

        hide = ''
        muted = ''
        checked = ' checked="checked"'
        if header.get('hidden', False) is True:
            hide = 'hidden'
            muted = ' text-muted'
            checked = ''
            hidden_cols += 1

        data_attr = 'data-dmax="{}" data-dmin="{}" data-namespace="{}" {}' \
            .format(header['dmax'], header['dmin'], header['namespace'], shared_key)

        cell_contents = '<span class="mqc_table_tooltip" title="{}: {}">{}</span>' \
            .format(header['namespace'], header['description'], header['title'])

        t_headers[rid] = '<th id="header_{rid}" class="{rid} {h}" {da}>{c}</th>' \
            .format(rid=rid, h=hide, da=data_attr, c=cell_contents)

        empty_cells[rid] = '<td class="data-coloured {rid} {h}"></td>'.format(
            rid=rid, h=hide)

        # Build the modal table row
        t_modal_headers[rid] = """
        <tr class="{rid}{muted}" style="background-color: rgba({col}, 0.15);">
          <td class="sorthandle ui-sortable-handle">||</span></td>
          <td style="text-align:center;">
            <input class="mqc_table_col_visible" type="checkbox" {checked} value="{rid}" data-target="#{tid}">
          </td>
          <td>{name}</td>
          <td>{title}</td>
          <td>{desc}</td>
          <td>{col_id}</td>
          <td>{sk}</td>
        </tr>""".format(rid=rid,
                        muted=muted,
                        checked=checked,
                        tid=table_id,
                        col=header['colour'],
                        name=header['namespace'],
                        title=header['title'],
                        desc=header['description'],
                        col_id='<code>{}</code>'.format(k),
                        sk=header.get('shared_key', ''))

        # Make a colour scale
        if header['scale'] == False:
            c_scale = None
        else:
            c_scale = mqc_colour.mqc_colour_scale(header['scale'],
                                                  header['dmin'],
                                                  header['dmax'])

        # Add the data table cells
        for (s_name, samp) in dt.data[idx].items():
            if k in samp:
                val = samp[k]
                kname = '{}_{}'.format(header['namespace'], rid)
                dt.raw_vals[s_name][kname] = val

                if 'modify' in header and callable(header['modify']):
                    val = header['modify'](val)

                try:
                    dmin = header['dmin']
                    dmax = header['dmax']
                    percentage = ((float(val) - dmin) / (dmax - dmin)) * 100
                    percentage = min(percentage, 100)
                    percentage = max(percentage, 0)
                except (ZeroDivisionError, ValueError):
                    percentage = 0

                try:
                    valstring = str(header['format'].format(val))
                except ValueError:
                    try:
                        valstring = str(header['format'].format(float(val)))
                    except ValueError:
                        valstring = str(val)
                except:
                    valstring = str(val)

                # This is horrible, but Python locale settings are worse
                if config.thousandsSep_format is None:
                    config.thousandsSep_format = '<span class="mqc_thousandSep"></span>'
                if config.decimalPoint_format is None:
                    config.decimalPoint_format = '.'
                valstring = valstring.replace('.', 'DECIMAL').replace(
                    ',', 'THOUSAND')
                valstring = valstring.replace(
                    'DECIMAL', config.decimalPoint_format).replace(
                        'THOUSAND', config.thousandsSep_format)

                # Percentage suffixes etc
                valstring += header.get('suffix', '')
                # Build HTML
                if s_name not in t_rows:
                    t_rows[s_name] = dict()
                t_rows[s_name][rid] = '<td class="{rid} {h}">{v}</td>'.format(
                    rid=rid, h=hide, v=valstring)
                # else:
                #     if c_scale is not None:
                #         col = ' background-color:{};'.format(c_scale.get_colour(val))
                #     else:
                #         col = ''
                #     bar_html = '<span class="bar" style="width:{}%;{}"></span>'.format(percentage, col)
                #     val_html = '<span class="val">{}</span>'.format(valstring)
                #     wrapper_html = '<div class="wrapper">{}{}</div>'.format(bar_html, val_html)
                #
                #     if s_name not in t_rows:
                #         t_rows[s_name] = dict()
                #     t_rows[s_name][rid] = '<td class="data-coloured {rid} {h}">{c}</td>'.format(rid=rid, h=hide, c=wrapper_html)

        # Remove header if we don't have any filled cells for it
        if sum([len(rows) for rows in t_rows.values()]) == 0:
            t_headers.pop(rid, None)
            t_modal_headers.pop(rid, None)
            logger.debug(
                'Removing header {} from general stats table, as no data'.
                format(k))

    #
    # Put everything together
    #

    # Buttons above the table
    html = ''
    if not config.simple_output:

        # Copy Table Button
        html += """
        <button type="button" class="mqc_table_copy_btn btn btn-default btn-sm" data-clipboard-target="#{tid}">
            <span class="glyphicon glyphicon-copy"></span> Copy table
        </button>
        """.format(tid=table_id)

        # Configure Columns Button
        if len(t_headers) > 1:
            html += """
            <button type="button" class="mqc_table_configModal_btn btn btn-default btn-sm" data-toggle="modal" data-target="#{tid}_configModal">
                <span class="glyphicon glyphicon-th"></span> Configure Columns
            </button>
            """.format(tid=table_id)

        # Sort By Highlight button
        html += """
        <button type="button" class="mqc_table_sortHighlight btn btn-default btn-sm" data-target="#{tid}" data-direction="desc" style="display:none;">
            <span class="glyphicon glyphicon-sort-by-attributes-alt"></span> Sort by highlight
        </button>
        """.format(tid=table_id)

        # Scatter Plot Button
        if len(t_headers) > 1:
            html += """
            <button type="button" class="mqc_table_makeScatter btn btn-default btn-sm" data-toggle="modal" data-target="#tableScatterModal" data-table="#{tid}">
                <span class="glyphicon glyphicon glyphicon-stats"></span> Plot
            </button>
            """.format(tid=table_id)

        # "Showing x of y columns" text
        html += """
        <small id="{tid}_numrows_text" class="mqc_table_numrows_text">Showing <sup id="{tid}_numrows" class="mqc_table_numrows">{nrows}</sup>/<sub>{nrows}</sub> rows and <sup id="{tid}_numcols" class="mqc_table_numcols">{ncols_vis}</sup>/<sub>{ncols}</sub> columns.</small>
        """.format(tid=table_id,
                   nrows=len(t_rows),
                   ncols_vis=(len(t_headers) + 1) - hidden_cols,
                   ncols=len(t_headers))

    # Build the table itself
    collapse_class = 'mqc-table-collapse' if len(
        t_rows) > 10 and config.collapse_tables else ''
    html += """
        <div id="{tid}_container" class="mqc_table_container">
            <div class="table-responsive mqc-table-responsive {cc}">
                <table id="{tid}" class="table table-condensed mqc_table" data-title="{title}">
        """.format(tid=table_id, title=table_title, cc=collapse_class)

    # Build the header row
    col1_header = dt.pconfig.get('col1_header', ' ')
    html += '<thead><tr><th class="rowheader">{}</th>{}</tr></thead>'.format(
        col1_header, ''.join(t_headers.values()))

    # Build the table body
    html += '<tbody>'
    t_row_keys = t_rows.keys()
    if dt.pconfig.get('sortRows') is not False:
        t_row_keys = sorted(t_row_keys)
    for s_name in t_row_keys:
        html += '<tr>'
        # Sample name row header
        html += '<th class="rowheader" data-original-sn="{sn}">{sn}</th>'.format(
            sn=s_name)
        for k in t_headers:
            html += t_rows[s_name].get(k, empty_cells[k])
        html += '</tr>'
    html += '</tbody></table></div>'
    if len(t_rows) > 10 and config.collapse_tables:
        html += '<div class="mqc-table-expand"><span class="glyphicon glyphicon-chevron-down" aria-hidden="true"></span></div>'
    html += '</div>'

    # Build the bootstrap modal to customise columns and order
    if not config.simple_output:
        html += """
    <!-- MultiQC Table Columns Modal -->
    <div class="modal fade" id="{tid}_configModal" tabindex="-1">
      <div class="modal-dialog modal-lg">
        <div class="modal-content">
          <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
            <h4 class="modal-title">{title}: Columns</h4>
          </div>
          <div class="modal-body">
            <p>Uncheck the tick box to hide columns. Click and drag the handle on the left to change order.</p>
            <p>
                <button class="btn btn-default btn-sm mqc_configModal_bulkVisible" data-target="#{tid}" data-action="showAll">Show All</button>
                <button class="btn btn-default btn-sm mqc_configModal_bulkVisible" data-target="#{tid}" data-action="showNone">Show None</button>
            </p>
            <table class="table mqc_table mqc_sortable mqc_configModal_table" id="{tid}_configModal_table" data-title="{title}">
              <thead>
                <tr>
                  <th class="sorthandle" style="text-align:center;">Sort</th>
                  <th style="text-align:center;">Visible</th>
                  <th>Group</th>
                  <th>Column</th>
                  <th>Description</th>
                  <th>ID</th>
                  <th>Scale</th>
                </tr>
              </thead>
              <tbody>
                {trows}
              </tbody>
            </table>
        </div>
        <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> </div>
    </div> </div> </div>""".format(tid=table_id,
                                   title=table_title,
                                   trows=''.join(t_modal_headers.values()))

    # Save the raw values to a file if requested
    if dt.pconfig.get('save_file') is True:
        fn = dt.pconfig.get('raw_data_fn', 'multiqc_{}'.format(table_id))
        util_functions.write_data_file(dt.raw_vals, fn)
        report.saved_raw_data[fn] = dt.raw_vals

    return html
Exemple #7
0
def make_table(dt):
    """
    Build the HTML needed for a MultiQC table.
    :param data: MultiQC datatable object
    """

    table_id = dt.pconfig.get(
        "id", "table_{}".format("".join(random.sample(letters, 4))))
    table_id = report.save_htmlid(table_id)
    t_headers = OrderedDict()
    t_modal_headers = OrderedDict()
    t_rows = OrderedDict()
    t_rows_empty = OrderedDict()
    dt.raw_vals = defaultdict(lambda: dict())
    empty_cells = dict()
    hidden_cols = 1
    table_title = dt.pconfig.get("table_title")
    if table_title is None:
        table_title = table_id.replace("_", " ").title()

    for idx, k, header in dt.get_headers_in_order():

        rid = header["rid"]

        # Build the table header cell
        shared_key = ""
        if header.get("shared_key", None) is not None:
            shared_key = " data-shared-key={}".format(header["shared_key"])

        hide = ""
        muted = ""
        checked = ' checked="checked"'
        if header.get("hidden", False) is True:
            hide = "hidden"
            muted = " text-muted"
            checked = ""
            hidden_cols += 1

        data_attr = 'data-dmax="{}" data-dmin="{}" data-namespace="{}" {}'.format(
            header["dmax"], header["dmin"], header["namespace"], shared_key)

        cell_contents = '<span class="mqc_table_tooltip" title="{}: {}">{}</span>'.format(
            header["namespace"], header["description"], header["title"])

        t_headers[
            rid] = '<th id="header_{rid}" class="{rid} {h}" {da}>{c}</th>'.format(
                rid=rid, h=hide, da=data_attr, c=cell_contents)

        empty_cells[rid] = '<td class="data-coloured {rid} {h}"></td>'.format(
            rid=rid, h=hide)

        # Build the modal table row
        t_modal_headers[rid] = """
        <tr class="{rid}{muted}" style="background-color: rgba({col}, 0.15);">
          <td class="sorthandle ui-sortable-handle">||</span></td>
          <td style="text-align:center;">
            <input class="mqc_table_col_visible" type="checkbox" {checked} value="{rid}" data-target="#{tid}">
          </td>
          <td>{name}</td>
          <td>{title}</td>
          <td>{desc}</td>
          <td>{col_id}</td>
          <td>{sk}</td>
        </tr>""".format(
            rid=rid,
            muted=muted,
            checked=checked,
            tid=table_id,
            col=header["colour"],
            name=header["namespace"],
            title=header["title"],
            desc=header["description"],
            col_id="<code>{}</code>".format(k),
            sk=header.get("shared_key", ""),
        )

        # Make a colour scale
        if header["scale"] == False:
            c_scale = None
        else:
            c_scale = mqc_colour.mqc_colour_scale(header["scale"],
                                                  header["dmin"],
                                                  header["dmax"])

        # Add the data table cells
        for (s_name, samp) in dt.data[idx].items():
            if k in samp:
                val = samp[k]
                kname = "{}_{}".format(header["namespace"], rid)
                dt.raw_vals[s_name][kname] = val

                if "modify" in header and callable(header["modify"]):
                    val = header["modify"](val)

                try:
                    dmin = header["dmin"]
                    dmax = header["dmax"]
                    percentage = ((float(val) - dmin) / (dmax - dmin)) * 100
                    percentage = min(percentage, 100)
                    percentage = max(percentage, 0)
                except (ZeroDivisionError, ValueError):
                    percentage = 0

                try:
                    valstring = str(header["format"].format(val))
                except ValueError:
                    try:
                        valstring = str(header["format"].format(float(val)))
                    except ValueError:
                        valstring = str(val)
                except:
                    valstring = str(val)

                # This is horrible, but Python locale settings are worse
                if config.thousandsSep_format is None:
                    config.thousandsSep_format = '<span class="mqc_thousandSep"></span>'
                if config.decimalPoint_format is None:
                    config.decimalPoint_format = "."
                valstring = valstring.replace(".", "DECIMAL").replace(
                    ",", "THOUSAND")
                valstring = valstring.replace(
                    "DECIMAL", config.decimalPoint_format).replace(
                        "THOUSAND", config.thousandsSep_format)

                # Percentage suffixes etc
                valstring += header.get("suffix", "")

                # Conditional formatting
                cmatches = {
                    cfck: False
                    for cfc in config.table_cond_formatting_colours
                    for cfck in cfc
                }
                # Find general rules followed by column-specific rules
                for cfk in ["all_columns", rid]:
                    if cfk in config.table_cond_formatting_rules:
                        # Loop through match types
                        for ftype in cmatches.keys():
                            # Loop through array of comparison types
                            for cmp in config.table_cond_formatting_rules[
                                    cfk].get(ftype, []):
                                try:
                                    # Each comparison should be a dict with single key: val
                                    if "s_eq" in cmp and str(
                                            cmp["s_eq"]).lower() == str(
                                                val).lower():
                                        cmatches[ftype] = True
                                    if "s_contains" in cmp and str(
                                            cmp["s_contains"]).lower() in str(
                                                val).lower():
                                        cmatches[ftype] = True
                                    if "s_ne" in cmp and str(
                                            cmp["s_ne"]).lower() != str(
                                                val).lower():
                                        cmatches[ftype] = True
                                    if "eq" in cmp and float(
                                            cmp["eq"]) == float(val):
                                        cmatches[ftype] = True
                                    if "ne" in cmp and float(
                                            cmp["ne"]) != float(val):
                                        cmatches[ftype] = True
                                    if "gt" in cmp and float(
                                            cmp["gt"]) < float(val):
                                        cmatches[ftype] = True
                                    if "lt" in cmp and float(
                                            cmp["lt"]) > float(val):
                                        cmatches[ftype] = True
                                except:
                                    logger.warning(
                                        "Not able to apply table conditional formatting to '{}' ({})"
                                        .format(val, cmp))
                # Apply HTML in order of config keys
                bgcol = None
                for cfc in config.table_cond_formatting_colours:
                    for cfck in cfc:  # should always be one, but you never know
                        if cmatches[cfck]:
                            bgcol = cfc[cfck]
                if bgcol is not None:
                    valstring = '<span class="badge" style="background-color:{}">{}</span>'.format(
                        bgcol, valstring)

                # Build HTML
                if not header["scale"]:
                    if s_name not in t_rows:
                        t_rows[s_name] = dict()
                    t_rows[s_name][
                        rid] = '<td class="{rid} {h}">{v}</td>'.format(
                            rid=rid, h=hide, v=valstring)
                else:
                    if c_scale is not None:
                        col = " background-color:{};".format(
                            c_scale.get_colour(val))
                    else:
                        col = ""
                    bar_html = '<span class="bar" style="width:{}%;{}"></span>'.format(
                        percentage, col)
                    val_html = '<span class="val">{}</span>'.format(valstring)
                    wrapper_html = '<div class="wrapper">{}{}</div>'.format(
                        bar_html, val_html)

                    if s_name not in t_rows:
                        t_rows[s_name] = dict()
                    t_rows[s_name][
                        rid] = '<td class="data-coloured {rid} {h}">{c}</td>'.format(
                            rid=rid, h=hide, c=wrapper_html)

                # Is this cell hidden or empty?
                if s_name not in t_rows_empty:
                    t_rows_empty[s_name] = dict()
                t_rows_empty[s_name][rid] = header.get(
                    "hidden", False) or str(val).strip() == ""

        # Remove header if we don't have any filled cells for it
        if sum([len(rows) for rows in t_rows.values()]) == 0:
            if header.get("hidden", False) is True:
                hidden_cols -= 1
            t_headers.pop(rid, None)
            t_modal_headers.pop(rid, None)
            logger.debug("Removing header {} from table, as no data".format(k))

    #
    # Put everything together
    #

    # Buttons above the table
    html = ""
    if not config.simple_output:

        # Copy Table Button
        html += """
        <button type="button" class="mqc_table_copy_btn btn btn-default btn-sm" data-clipboard-target="#{tid}">
            <span class="glyphicon glyphicon-copy"></span> Copy table
        </button>
        """.format(tid=table_id)

        # Configure Columns Button
        if len(t_headers) > 1:
            html += """
            <button type="button" class="mqc_table_configModal_btn btn btn-default btn-sm" data-toggle="modal" data-target="#{tid}_configModal">
                <span class="glyphicon glyphicon-th"></span> Configure Columns
            </button>
            """.format(tid=table_id)

        # Sort By Highlight button
        html += """
        <button type="button" class="mqc_table_sortHighlight btn btn-default btn-sm" data-target="#{tid}" data-direction="desc" style="display:none;">
            <span class="glyphicon glyphicon-sort-by-attributes-alt"></span> Sort by highlight
        </button>
        """.format(tid=table_id)

        # Scatter Plot Button
        if len(t_headers) > 1:
            html += """
            <button type="button" class="mqc_table_makeScatter btn btn-default btn-sm" data-toggle="modal" data-target="#tableScatterModal" data-table="#{tid}">
                <span class="glyphicon glyphicon glyphicon-stats"></span> Plot
            </button>
            """.format(tid=table_id)

        # "Showing x of y columns" text
        row_visibilities = [
            all(t_rows_empty[s_name].values()) for s_name in t_rows_empty
        ]
        visible_rows = [x for x in row_visibilities if not x]

        # Visible rows
        t_showing_rows_txt = (
            'Showing <sup id="{tid}_numrows" class="mqc_table_numrows">{nvisrows}</sup>/<sub>{nrows}</sub> rows'
            .format(tid=table_id,
                    nvisrows=len(visible_rows),
                    nrows=len(t_rows)))

        # How many columns are visible?
        ncols_vis = (len(t_headers) + 1) - hidden_cols
        t_showing_cols_txt = ""
        if len(t_headers) > 1:
            t_showing_cols_txt = ' and <sup id="{tid}_numcols" class="mqc_table_numcols">{ncols_vis}</sup>/<sub>{ncols}</sub> columns'.format(
                tid=table_id, ncols_vis=ncols_vis, ncols=len(t_headers))

        # Build table header text
        html += """
        <small id="{tid}_numrows_text" class="mqc_table_numrows_text">{rows}{cols}.</small>
        """.format(tid=table_id,
                   rows=t_showing_rows_txt,
                   cols=t_showing_cols_txt)

    # Build the table itself
    collapse_class = "mqc-table-collapse" if len(
        t_rows) > 10 and config.collapse_tables else ""
    html += """
        <div id="{tid}_container" class="mqc_table_container">
            <div class="table-responsive mqc-table-responsive {cc}">
                <table id="{tid}" class="table table-condensed mqc_table" data-title="{title}">
        """.format(tid=table_id, title=table_title, cc=collapse_class)

    # Build the header row
    col1_header = dt.pconfig.get("col1_header", "Sample Name")
    html += '<thead><tr><th class="rowheader">{}</th>{}</tr></thead>'.format(
        col1_header, "".join(t_headers.values()))

    # Build the table body
    html += "<tbody>"
    t_row_keys = t_rows.keys()
    if dt.pconfig.get("sortRows") is not False:
        t_row_keys = sorted(t_row_keys)
    for s_name in t_row_keys:
        # Hide the row if all cells are empty or hidden
        row_hidden = ' style="display:none"' if all(
            t_rows_empty[s_name].values()) else ""
        html += "<tr{}>".format(row_hidden)
        # Sample name row header
        html += '<th class="rowheader" data-original-sn="{sn}">{sn}</th>'.format(
            sn=s_name)
        for k in t_headers:
            html += t_rows[s_name].get(k, empty_cells[k])
        html += "</tr>"
    html += "</tbody></table></div>"
    if len(t_rows) > 10 and config.collapse_tables:
        html += '<div class="mqc-table-expand"><span class="glyphicon glyphicon-chevron-down" aria-hidden="true"></span></div>'
    html += "</div>"

    # Build the bootstrap modal to customise columns and order
    if not config.simple_output:
        html += """
    <!-- MultiQC Table Columns Modal -->
    <div class="modal fade" id="{tid}_configModal" tabindex="-1">
      <div class="modal-dialog modal-lg">
        <div class="modal-content">
          <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
            <h4 class="modal-title">{title}: Columns</h4>
          </div>
          <div class="modal-body">
            <p>Uncheck the tick box to hide columns. Click and drag the handle on the left to change order.</p>
            <p>
                <button class="btn btn-default btn-sm mqc_configModal_bulkVisible" data-target="#{tid}" data-action="showAll">Show All</button>
                <button class="btn btn-default btn-sm mqc_configModal_bulkVisible" data-target="#{tid}" data-action="showNone">Show None</button>
            </p>
            <table class="table mqc_table mqc_sortable mqc_configModal_table" id="{tid}_configModal_table" data-title="{title}">
              <thead>
                <tr>
                  <th class="sorthandle" style="text-align:center;">Sort</th>
                  <th style="text-align:center;">Visible</th>
                  <th>Group</th>
                  <th>Column</th>
                  <th>Description</th>
                  <th>ID</th>
                  <th>Scale</th>
                </tr>
              </thead>
              <tbody>
                {trows}
              </tbody>
            </table>
        </div>
        <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> </div>
    </div> </div> </div>""".format(tid=table_id,
                                   title=table_title,
                                   trows="".join(t_modal_headers.values()))

    # Save the raw values to a file if requested
    if dt.pconfig.get("save_file") is True:
        fn = dt.pconfig.get("raw_data_fn", "multiqc_{}".format(table_id))
        util_functions.write_data_file(dt.raw_vals, fn)
        report.saved_raw_data[fn] = dt.raw_vals

    return html
Exemple #8
0
def make_table(dt, hide_bar=None):
    """
    Build the HTML needed for a MultiQC table.
    :param data: MultiQC datatable object
    """

    table_id = dt.pconfig.get(
        'id', 'table_{}'.format(''.join(random.sample(letters, 4))))
    table_id = report.save_htmlid(table_id)
    t_headers = OrderedDict()
    t_modal_headers = OrderedDict()
    t_rows = OrderedDict()
    t_rows_empty = OrderedDict()
    dt.raw_vals = defaultdict(lambda: dict())
    empty_cells = dict()
    hidden_cols = 1
    table_title = dt.pconfig.get('table_title')
    if table_title is None:
        table_title = table_id.replace("_", " ").title()

    for idx, k, header in dt.get_headers_in_order():

        rid = header['rid']

        # Build the table header cell
        shared_key = ''
        if header.get('shared_key', None) is not None:
            shared_key = ' data-shared-key={}'.format(header['shared_key'])

        hide = ''
        muted = ''
        checked = ' checked="checked"'
        if header.get('hidden', False) is True:
            hide = 'hidden'
            muted = ' text-muted'
            checked = ''
            hidden_cols += 1

        data_attr = 'data-dmax="{}" data-dmin="{}" data-namespace="{}" {}' \
            .format(header['dmax'], header['dmin'], header['namespace'], shared_key)

        if header.get('namespace'):
            cell_contents = '<span class="mqc_table_tooltip" title="{}: {}">{}</span>' \
                .format(header['namespace'], header['description'], header['title'])
        else:
            cell_contents = '<span class="mqc_table_tooltip" title="{} {}">{}</span>' \
                .format(header['namespace'], header['description'], header['title'])

        t_headers[rid] = '<th id="header_{rid}" class="{rid} {h}" {da}>{c}</th>' \
            .format(rid=rid, h=hide, da=data_attr, c=cell_contents)

        empty_cells[rid] = '<td class="data-coloured {rid} {h}"></td>'.format(
            rid=rid, h=hide)

        # Build the modal table row
        t_modal_headers[rid] = """
        <tr class="{rid}{muted}" style="background-color: rgba({col}, 0.15);">
          <td class="sorthandle ui-sortable-handle">||</span></td>
          <td style="text-align:center;">
            <input class="mqc_table_col_visible" type="checkbox" {checked} value="{rid}" data-target="#{tid}">
          </td>
          <td>{name}</td>
          <td>{title}</td>
          <td>{desc}</td>
          <td>{col_id}</td>
          <td>{sk}</td>
        </tr>""".format(rid=rid,
                        muted=muted,
                        checked=checked,
                        tid=table_id,
                        col=header['colour'],
                        name=header['namespace'],
                        title=header['title'],
                        desc=header['description'],
                        col_id='<code>{}</code>'.format(k),
                        sk=header.get('shared_key', ''))

        # Make a colour scale
        if header['scale'] == False:
            c_scale = None
        else:
            c_scale = mqc_colour.mqc_colour_scale(header['scale'],
                                                  header['dmin'],
                                                  header['dmax'])

        # Add the data table cells
        for (s_name, samp) in dt.data[idx].items():
            if k in samp:
                val = samp[k]
                kname = '{}_{}'.format(header['namespace'], rid)
                dt.raw_vals[s_name][kname] = val

                # if "is_int" in header.keys():
                #     val = int(val)
                #     print(val)

                if 'modify' in header and callable(header['modify']):
                    val = header['modify'](val)

                try:
                    dmin = header['dmin']
                    dmax = header['dmax']
                    percentage = ((float(val) - dmin) / (dmax - dmin)) * 100
                    percentage = min(percentage, 100)
                    percentage = max(percentage, 0)
                except (ZeroDivisionError, ValueError):
                    percentage = 0

                try:
                    if not header.get("is_int"):
                        valstring = str(header['format'].format(val))
                    else:
                        valstring = str(int(val))

                except ValueError:
                    try:
                        valstring = str(header['format'].format(float(val)))

                    except ValueError:
                        valstring = str(val)

                except:
                    valstring = str(val)

                # This is horrible, but Python locale settings are worse
                if config.thousandsSep_format is None:
                    config.thousandsSep_format = '<span class="mqc_thousandSep"></span>'
                if config.decimalPoint_format is None:
                    config.decimalPoint_format = '.'
                valstring = valstring.replace('.', 'DECIMAL').replace(
                    ',', 'THOUSAND')
                valstring = valstring.replace(
                    'DECIMAL', config.decimalPoint_format).replace(
                        'THOUSAND', config.thousandsSep_format)

                # Percentage suffixes etc
                if header.get('suffix') == "show_perc":
                    suff_dict = header.get("suffix_dict")
                    valstring += str(suff_dict.get(s_name))
                else:
                    valstring += header.get('suffix', '')

                # Conditional formatting
                cmatches = {
                    cfck: False
                    for cfc in config.table_cond_formatting_colours
                    for cfck in cfc
                }
                # Find general rules followed by column-specific rules
                for cfk in ['all_columns', rid]:
                    if cfk in config.table_cond_formatting_rules:
                        # Loop through match types
                        for ftype in cmatches.keys():
                            # Loop through array of comparison types
                            for cmp in config.table_cond_formatting_rules[
                                    cfk].get(ftype, []):
                                try:
                                    # Each comparison should be a dict with single key: val
                                    if 's_eq' in cmp and str(
                                            cmp['s_eq']).lower() == str(
                                                val).lower():
                                        cmatches[ftype] = True
                                    if 's_contains' in cmp and str(
                                            cmp['s_contains']).lower() in str(
                                                val).lower():
                                        cmatches[ftype] = True
                                    if 's_ne' in cmp and str(
                                            cmp['s_ne']).lower() != str(
                                                val).lower():
                                        cmatches[ftype] = True
                                    if 'eq' in cmp and float(
                                            cmp['eq']) == float(val):
                                        cmatches[ftype] = True
                                    if 'ne' in cmp and float(
                                            cmp['ne']) != float(val):
                                        cmatches[ftype] = True
                                    if 'gt' in cmp and float(
                                            cmp['gt']) < float(val):
                                        cmatches[ftype] = True
                                    if 'lt' in cmp and float(
                                            cmp['lt']) > float(val):
                                        cmatches[ftype] = True
                                except:
                                    logger.warn(
                                        "Not able to apply table conditional formatting to '{}' ({})"
                                        .format(val, cmp))
                # Apply HTML in order of config keys
                bgcol = None
                for cfc in config.table_cond_formatting_colours:
                    for cfck in cfc:  # should always be one, but you never know
                        if cmatches[cfck]:
                            bgcol = cfc[cfck]
                if bgcol is not None:
                    valstring = '<span class="badge" style="background-color:{}">{}</span>'.format(
                        bgcol, valstring)

                # Build HTML
                if not header['scale']:
                    if s_name not in t_rows:
                        t_rows[s_name] = dict()
                    t_rows[s_name][
                        rid] = '<td class="{rid} {h}">{v}</td>'.format(
                            rid=rid, h=hide, v=valstring)

                # coloring with quartiles
                elif header['scale'] == "quart":

                    col_dict = header['col_dict']
                    bar_dict = header.get('bar_dict')
                    col = ' background-color:{};'.format(col_dict.get(s_name))
                    if bar_dict:
                        bar_html = '<span class="bar" style="width:{}%;{}"></span>'.format(
                            bar_dict.get(s_name) + 3, col)
                    else:
                        bar_html = '<span class="bar" style="width:{}%;{}"></span>'.format(
                            percentage, col)
                    # bar percentage here

                    val_html = '<span class="val">{}</span>'.format(valstring)
                    wrapper_html = '<div class="wrapper">{}{}</div>'.format(
                        bar_html, val_html)
                    if s_name not in t_rows:
                        t_rows[s_name] = dict()
                    t_rows[s_name][
                        rid] = '<td class="data-coloured {rid} {h}">{c}</td>'.format(
                            rid=rid, h=hide, c=wrapper_html)

                else:
                    if c_scale is not None:
                        col = ' background-color:{};'.format(
                            c_scale.get_colour(val))
                    else:
                        col = ''
                    bar_html = '<span class="bar" style="width:{}%;{}"></span>'.format(
                        percentage, col)
                    val_html = '<span class="val">{}</span>'.format(valstring)
                    wrapper_html = '<div class="wrapper">{}{}</div>'.format(
                        bar_html, val_html)

                    if s_name not in t_rows:
                        t_rows[s_name] = dict()
                    t_rows[s_name][
                        rid] = '<td class="data-coloured {rid} {h}">{c}</td>'.format(
                            rid=rid, h=hide, c=wrapper_html)

                # Is this cell hidden or empty?
                if s_name not in t_rows_empty:
                    t_rows_empty[s_name] = dict()
                t_rows_empty[s_name][rid] = header.get(
                    'hidden', False) or str(val).strip() == ''

        # Remove header if we don't have any filled cells for it
        if sum([len(rows) for rows in t_rows.values()]) == 0:
            t_headers.pop(rid, None)
            t_modal_headers.pop(rid, None)
            logger.debug(
                'Removing header {} from general stats table, as no data'.
                format(k))

    #
    # Put everything together
    #

    # Buttons above the table
    html = ''
    if not config.simple_output:
        # Copy Table Button
        html += """<div class="row">
                    <div class="col-sm-2">
        <button type="button" class="mqc_table_copy_btn btn btn-default btn-sm" data-clipboard-target="#{tid}">
            <span class="glyphicon glyphicon-copy"></span> Copy table
        </button>
        </div>
        """.format(tid=table_id)

        # <div class="progress-bar progress-bar-warning progress-bar-striped" style="width: 20%">
        #                 <span class="sr-only">20% Complete (warning)</span>
        #               </div>
        #               <div class="progress-bar progress-bar-danger" style="width: 10%">
        #                 <span class="sr-only">10% Complete (danger)</span>
        #               </div>

        if not hide_bar:
            html += """ <div class="col-sm-2">
                <div id="quartiles_bar">
                <div class="progress">
                  <div class="progress-bar progress-bar-q1" style="width: 25%">
                    <span class="sr-only">35% Complete (success)</span> Q1
                  </div>
    
                  <div class="progress-bar progress-bar-q2" style="width: 25%">
                    <span class="sr-only">35% Complete (success)</span> Q2
                  </div>
                
                  <div class="progress-bar progress-bar-q3" style="width: 25%">
                    <span class="sr-only">35% Complete (success)</span> Q3
                  </div>
    
                  <div class="progress-bar progress-bar-q4" style="width: 25%">
                    <span class="sr-only">35% Complete (success)</span> Q4
                  </div>
                </div>
                </div>
                         </div>
                         </div>
                """
        else:
            html += """ </div>
                """
        # Configure Columns Button
        # if len(t_headers) > 1:
        #     html += """
        #     <button type="button" class="mqc_table_configModal_btn btn btn-default btn-sm" data-toggle="modal" data-target="#{tid}_configModal">
        #         <span class="glyphicon glyphicon-th"></span> Configure Columns
        #     </button>
        #     """.format(tid=table_id)

        # Sort By Highlight button
        # html += """
        # <button type="button" class="mqc_table_sortHighlight btn btn-default btn-sm" data-target="#{tid}" data-direction="desc" style="display:none;">
        #     <span class="glyphicon glyphicon-sort-by-attributes-alt"></span> Sort by highlight
        # </button>
        # """.format(tid=table_id)

        # Scatter Plot Button
        # if len(t_headers) > 1:
        #     html += """
        #     <button type="button" class="mqc_table_makeScatter btn btn-default btn-sm" data-toggle="modal" data-target="#tableScatterModal" data-table="#{tid}">
        #         <span class="glyphicon glyphicon glyphicon-stats"></span> Plot
        #     </button>
        #     """.format(tid=table_id)

        # "Showing x of y columns" text
        row_visibilities = [
            all(t_rows_empty[s_name].values()) for s_name in t_rows_empty
        ]
        visible_rows = [x for x in row_visibilities if not x]
        # html += """
        # <small id="{tid}_numrows_text" class="mqc_table_numrows_text">Showing <sup id="{tid}_numrows" class="mqc_table_numrows">{nvisrows}</sup>/<sub>{nrows}</sub> rows and <sup id="{tid}_numcols" class="mqc_table_numcols">{ncols_vis}</sup>/<sub>{ncols}</sub> columns.</small>
        # """.format(tid=table_id, nvisrows=len(visible_rows), nrows=len(t_rows), ncols_vis = (len(t_headers)+1)-hidden_cols, ncols=len(t_headers))

        # Add text
        # html += """
        #         <small id="{tid}_numrows_text" class="mqc_table_numrows_text">Showing <sup id="{tid}_numrows" class="mqc_table_numrows">{nvisrows}</sup>/<sub>{nrows}</sub> rows and <sup id="{tid}_numcols" class="mqc_table_numcols">{ncols_vis}</sup>/<sub>{ncols}</sub> columns.</small>
        #         """.format(tid=table_id, nvisrows=len(visible_rows), nrows=len(t_rows),
        #                    ncols_vis=(len(t_headers) + 1) - hidden_cols, ncols=len(t_headers))

    # Build the table itself
    collapse_class = 'mqc-table-collapse' if len(
        t_rows) > 10 and config.collapse_tables else ''
    html += """
        <div id="{tid}_container" class="mqc_table_container">
            <div class="table-responsive mqc-table-responsive {cc}">
                <table id="{tid}" class="table table-condensed mqc_table" data-title="{title}">
        """.format(tid=table_id, title=table_title, cc=collapse_class)

    # Build the header row
    col1_header = dt.pconfig.get('col1_header', 'Sample Name')
    html += '<thead><tr><th class="rowheader">{}</th>{}</tr></thead>'.format(
        col1_header, ''.join(t_headers.values()))

    # Build the table body
    html += '<tbody>'
    t_row_keys = t_rows.keys()
    if dt.pconfig.get('sortRows') is not False:
        t_row_keys = sorted(t_row_keys)
    for s_name in t_row_keys:
        # Hide the row if all cells are empty or hidden
        row_hidden = ' style="display:none"' if all(
            t_rows_empty[s_name].values()) else ''
        html += '<tr{}>'.format(row_hidden)
        # Sample name row header
        html += '<th class="rowheader" data-original-sn="{sn}">{sn}</th>'.format(
            sn=s_name)
        for k in t_headers:
            html += t_rows[s_name].get(k, empty_cells[k])
        html += '</tr>'
    html += '</tbody></table></div>'
    if len(t_rows) > 10 and config.collapse_tables:
        html += '<div class="mqc-table-expand"><span class="glyphicon glyphicon-chevron-down" aria-hidden="true"></span></div>'
    html += '</div>'

    # Build the bootstrap modal to customise columns and order
    # if not config.simple_output:
    #     html += """
    # <!-- MultiQC Table Columns Modal -->
    # <div class="modal fade" id="{tid}_configModal" tabindex="-1">
    #   <div class="modal-dialog modal-lg">
    #     <div class="modal-content">
    #       <div class="modal-header">
    #         <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
    #         <h4 class="modal-title">{title}: Columns</h4>
    #       </div>
    #       <div class="modal-body">
    #         <p>Uncheck the tick box to hide columns. Click and drag the handle on the left to change order.</p>
    #         <p>
    #             <button class="btn btn-default btn-sm mqc_configModal_bulkVisible" data-target="#{tid}" data-action="showAll">Show All</button>
    #             <button class="btn btn-default btn-sm mqc_configModal_bulkVisible" data-target="#{tid}" data-action="showNone">Show None</button>
    #         </p>
    #         <table class="table mqc_table mqc_sortable mqc_configModal_table" id="{tid}_configModal_table" data-title="{title}">
    #           <thead>
    #             <tr>
    #               <th class="sorthandle" style="text-align:center;">Sort</th>
    #               <th style="text-align:center;">Visible</th>
    #               <th>Group</th>
    #               <th>Column</th>
    #               <th>Description</th>
    #               <th>ID</th>
    #               <th>Scale</th>
    #             </tr>
    #           </thead>
    #           <tbody>
    #             {trows}
    #           </tbody>
    #         </table>
    #     </div>
    #     <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> </div>
    # </div> </div> </div>""".format( tid=table_id, title=table_title, trows=''.join(t_modal_headers.values()) )

    # Save the raw values to a file if requested
    if dt.pconfig.get('save_file') is True:
        fn = dt.pconfig.get('raw_data_fn', 'multiqc_{}'.format(table_id))
        util_functions.write_data_file(dt.raw_vals, fn)
        report.saved_raw_data[fn] = dt.raw_vals

    return html
Exemple #9
0
    def __init__(self):

        # Initialise the parent module
        super().__init__(
            name="Pangolin",
            anchor="pangolin",
            href="https://github.com/cov-lineages/pangolin",
            info=
            "uses variant calls to assign SARS-CoV-2 genome sequences to global lineages.",
            doi="10.1093/ve/veab064",
        )

        # Find and parse the sample files
        self.pangolin_data = dict()
        self.lineage_colours = dict()
        for f in self.find_log_files("pangolin", filehandles=True):
            self.parse_pangolin_log(f)
            self.add_data_source(f)

        # Filter out parsed samples based on sample name
        self.pangolin_data = self.ignore_samples(self.pangolin_data)

        # Stop if we didn't find anything
        if len(self.pangolin_data) == 0:
            raise UserWarning
        log.info("Found {} samples".format(len(self.pangolin_data)))
        self.write_data_file(self.pangolin_data, "multiqc_pangolin")

        # Assign some lineage colours
        # First, remove blank / None
        self.lineage_colours.pop("", None)
        self.lineage_colours.pop("None", None)
        cols = mqc_colour.mqc_colour_scale("Dark2", 0,
                                           len(self.lineage_colours))
        for idx, k in enumerate(self.lineage_colours):
            self.lineage_colours[k] = cols.get_colour(idx)
        # Manually add back None as grey
        self.lineage_colours["None"] = "#EFEFEF"

        self.pangolin_general_stats_table()

        self.add_section(
            name="Run table",
            anchor="pangolin-run",
            description=
            "Statistics gathered from the input pangolin files. Hover over the column headers for descriptions and click _Help_ for more in-depth documentation.",
            helptext="""
            This table shows some of the metrics parsed by Pangolin.
            Hover over the column headers to see a description of the contents. Longer help text for certain columns is shown below:

            * **Conflict**
                * In the pangoLEARN decision tree model, a given sequence gets assigned to the most likely category based on known diversity.
                  If a sequence can fit into more than one category, the conflict score will be greater than `0` and reflect the number of categories the sequence could fit into.
                  If the conflict score is `0`, this means that within the current decision tree there is only one category that the sequence could be assigned to.
            * **Ambiguity score**
                * This score is a function of the quantity of missing data in a sequence.
                  It represents the proportion of relevant sites in a sequence which were imputed to the reference values.
                  A score of `1` indicates that no sites were imputed, while a score of `0` indicates that more sites were imputed than were not imputed.
                  This score only includes sites which are used by the decision tree to classify a sequence.
            * **Scorpio conflict**
                * The conflict score is the proportion of defining variants which have the reference allele in the sequence.
                  Ambiguous/other non-ref/alt bases at each of the variant positions contribute only to the denominators of these scores.
            * **Note**
                * If any conflicts from the decision tree, this field will output the alternative assignments.
                  If the sequence failed QC this field will describe why.
                  If the sequence met the SNP thresholds for scorpio to call a constellation, it’ll describe the exact SNP counts of Alt, Ref and Amb (Alternative, reference and ambiguous) alleles for that call.
            """,
            plot=self.pangolin_table(),
        )
Exemple #10
0
    def reads_by_quality_plot(self):
        """Make the HighCharts HTML to plot the reads by quality"""
        def _get_total_reads(data_dict):
            stat_type = self._stat_types[0]
            for stat_type in self._stat_types:
                total_key = f"Number of reads_{stat_type}"
                if total_key in data_dict:
                    return data_dict[total_key], stat_type
            return None, None

        bar_data = {}
        stat_type = "unrecognized"
        # Order of keys, from >Q5 to >Q15
        _range_names = {
            "&gt;Q5": "&lt;Q5",
            "&gt;Q7": "Q5-7",
            "&gt;Q10": "Q7-10",
            "&gt;Q12": "Q10-12",
            "&gt;Q15": "Q12-15",
            "rest": "&gt;Q15",
        }
        for s_name, data_dict in self.nanostat_data.items():
            reads_total, stat_type = _get_total_reads(data_dict)
            if s_name in bar_data and stat_type == "aligned":
                log.debug(
                    "Sample '{s_name}' duplicated in the quality plot - ignoring aligned data"
                )
                continue
            elif s_name in bar_data and stat_type == "seq summary":
                log.debug(
                    "Sample '{s_name}' duplicated in the quality plot - overwriting with seq summary data"
                )
            bar_data[s_name] = {}

            prev_reads = reads_total
            for k, range_name in _range_names.items():
                if k != "rest":
                    data_key = f"{k}_{stat_type}"
                    reads_gt = data_dict[data_key]

                    bar_data[s_name][range_name] = prev_reads - reads_gt

                    if bar_data[s_name][range_name] < 0:
                        log.error(
                            f"Error on {s_name} {range_name} {data_key} . Negative number of reads"
                        )
                    prev_reads = reads_gt
                else:
                    data_key = f"&gt;Q15_{stat_type}"
                    bar_data[s_name][range_name] = data_dict[data_key]

        cats = OrderedDict()
        keys = reversed(list(_range_names.values()))
        colours = mqc_colour.mqc_colour_scale("RdYlGn-rev", 0,
                                              len(_range_names))
        for idx, k in enumerate(keys):
            cats[k] = {
                "name": "Reads " + k,
                "color": colours.get_colour(idx, lighten=1)
            }

        # Config for the plot
        config = {
            "id": "nanostat_quality_dist",
            "title": "NanoStat: Reads by quality",
            "ylab": "# Reads",
            "cpswitch_counts_label": "Number of Reads",
        }

        # Add the report section
        self.add_section(
            name="Reads by quality",
            anchor=f"nanostat_read_qualities",
            description=
            "Read counts categorised by read quality (phred score).",
            helptext="""
                Sequencing machines assign each generated read a quality score using the
                [Phred scale](https://en.wikipedia.org/wiki/Phred_quality_score).
                The phred score represents the liklelyhood that a given read contains errors.
                So, high quality reads have a high score.

                Data may come from NanoPlot reports generated with sequencing summary files or alignment stats.
                If a sample has data from both, the sequencing summary is preferred.
            """,
            plot=bargraph.plot(bar_data, cats, config),
        )
Exemple #11
0
def make_table (dt):
    """
    Build the HTML needed for a MultiQC table.
    :param data: MultiQC datatable object
    """

    table_id = dt.pconfig.get('id', 'table_{}'.format(''.join(random.sample(letters, 4))) )
    table_id = report.save_htmlid(table_id)
    t_headers = OrderedDict()
    t_modal_headers = OrderedDict()
    t_rows = OrderedDict()
    dt.raw_vals = defaultdict(lambda: dict())
    empty_cells = dict()
    hidden_cols = 1
    table_title = dt.pconfig.get('table_title')
    if table_title is None:
        table_title = table_id.replace("_", " ").title()

    for idx, k, header in dt.get_headers_in_order():

        rid = header['rid']

        # Build the table header cell
        shared_key = ''
        if header.get('shared_key', None) is not None:
            shared_key = ' data-shared-key={}'.format(header['shared_key'])

        hide = ''
        muted = ''
        checked = ' checked="checked"'
        if header.get('hidden', False) is True:
            hide = 'hidden'
            muted = ' text-muted'
            checked = ''
            hidden_cols += 1

        data_attr = 'data-dmax="{}" data-dmin="{}" data-namespace="{}" {}' \
            .format(header['dmax'], header['dmin'], header['namespace'], shared_key)

        cell_contents = '<span class="mqc_table_tooltip" title="{}: {}">{}</span>' \
            .format(header['namespace'], header['description'], header['title'])

        t_headers[rid] = '<th id="header_{rid}" class="{rid} {h}" {da}>{c}</th>' \
            .format(rid=rid, h=hide, da=data_attr, c=cell_contents)

        empty_cells[rid] = '<td class="data-coloured {rid} {h}"></td>'.format(rid=rid, h=hide)

        # Build the modal table row
        t_modal_headers[rid] = """
        <tr class="{rid}{muted}" style="background-color: rgba({col}, 0.15);">
          <td class="sorthandle ui-sortable-handle">||</span></td>
          <td style="text-align:center;">
            <input class="mqc_table_col_visible" type="checkbox" {checked} value="{rid}" data-target="#{tid}">
          </td>
          <td>{name}</td>
          <td>{title}</td>
          <td>{desc}</td>
          <td>{col_id}</td>
          <td>{sk}</td>
        </tr>""".format(
                rid = rid,
                muted = muted,
                checked = checked,
                tid = table_id,
                col = header['colour'],
                name = header['namespace'],
                title = header['title'],
                desc = header['description'],
                col_id = '<code>{}</code>'.format(k),
                sk = header.get('shared_key', '')
            )

        # Make a colour scale
        if header['scale'] == False:
            c_scale = None
        else:
            c_scale = mqc_colour.mqc_colour_scale(header['scale'], header['dmin'], header['dmax'])

        # Add the data table cells
        for (s_name, samp) in dt.data[idx].items():
            if k in samp:
                val = samp[k]
                kname = '{}_{}'.format(header['namespace'], rid)
                dt.raw_vals[s_name][kname] = val

                if 'modify' in header and callable(header['modify']):
                    val = header['modify'](val)

                try:
                    dmin = header['dmin']
                    dmax = header['dmax']
                    percentage = ((float(val) - dmin) / (dmax - dmin)) * 100
                    percentage = min(percentage, 100)
                    percentage = max(percentage, 0)
                except (ZeroDivisionError,ValueError):
                    percentage = 0

                try:
                    valstring = str(header['format'].format(val))
                except ValueError:
                    try:
                        valstring = str(header['format'].format(float(val)))
                    except ValueError:
                        valstring = str(val)
                except:
                    valstring = str(val)

                # This is horrible, but Python locale settings are worse
                if config.thousandsSep_format is None:
                    config.thousandsSep_format = '<span class="mqc_thousandSep"></span>'
                if config.decimalPoint_format is None:
                    config.decimalPoint_format = '.'
                valstring = valstring.replace('.', 'DECIMAL').replace(',', 'THOUSAND')
                valstring = valstring.replace('DECIMAL', config.decimalPoint_format).replace('THOUSAND', config.thousandsSep_format)

                # Percentage suffixes etc
                valstring += header.get('suffix', '')

                # Conditional formatting
                cmatches = { cfck: False for cfc in config.table_cond_formatting_colours for cfck in cfc }
                # Find general rules followed by column-specific rules
                for cfk in ['all_columns', rid]:
                    if cfk in config.table_cond_formatting_rules:
                        # Loop through match types
                        for ftype in cmatches.keys():
                            # Loop through array of comparison types
                            for cmp in config.table_cond_formatting_rules[cfk].get(ftype, []):
                                try:
                                    # Each comparison should be a dict with single key: val
                                    if 's_eq' in cmp and str(cmp['s_eq']).lower() == str(val).lower():
                                        cmatches[ftype] = True
                                    if 's_contains' in cmp and str(cmp['s_contains']).lower() in str(val).lower():
                                        cmatches[ftype] = True
                                    if 's_ne' in cmp and str(cmp['s_ne']).lower() != str(val).lower():
                                        cmatches[ftype] = True
                                    if 'eq' in cmp and float(cmp['eq']) == float(val):
                                        cmatches[ftype] = True
                                    if 'ne' in cmp and float(cmp['ne']) != float(val):
                                        cmatches[ftype] = True
                                    if 'gt' in cmp and float(cmp['gt']) < float(val):
                                        cmatches[ftype] = True
                                    if 'lt' in cmp and float(cmp['lt']) > float(val):
                                        cmatches[ftype] = True
                                except:
                                    logger.warn("Not able to apply table conditional formatting to '{}' ({})".format(val, cmp))
                # Apply HTML in order of config keys
                bgcol = None
                for cfc in config.table_cond_formatting_colours:
                    for cfck in cfc: # should always be one, but you never know
                        if cmatches[cfck]:
                            bgcol = cfc[cfck]
                if bgcol is not None:
                    valstring = '<span class="badge" style="background-color:{}">{}</span>'.format(bgcol, valstring)

                # Build HTML
                if not header['scale']:
                    if s_name not in t_rows:
                        t_rows[s_name] = dict()
                    t_rows[s_name][rid] = '<td class="{rid} {h}">{v}</td>'.format(rid=rid, h=hide, v=valstring)
                else:
                    if c_scale is not None:
                        col = ' background-color:{};'.format(c_scale.get_colour(val))
                    else:
                        col = ''
                    bar_html = '<span class="bar" style="width:{}%;{}"></span>'.format(percentage, col)
                    val_html = '<span class="val">{}</span>'.format(valstring)
                    wrapper_html = '<div class="wrapper">{}{}</div>'.format(bar_html, val_html)

                    if s_name not in t_rows:
                        t_rows[s_name] = dict()
                    t_rows[s_name][rid] = '<td class="data-coloured {rid} {h}">{c}</td>'.format(rid=rid, h=hide, c=wrapper_html)

        # Remove header if we don't have any filled cells for it
        if sum([len(rows) for rows in t_rows.values()]) == 0:
            t_headers.pop(rid, None)
            t_modal_headers.pop(rid, None)
            logger.debug('Removing header {} from general stats table, as no data'.format(k))

    #
    # Put everything together
    #

    # Buttons above the table
    html = ''
    if not config.simple_output:

        # Copy Table Button
        html += """
        <button type="button" class="mqc_table_copy_btn btn btn-default btn-sm" data-clipboard-target="#{tid}">
            <span class="glyphicon glyphicon-copy"></span> Copy table
        </button>
        """.format(tid=table_id)

        # Configure Columns Button
        if len(t_headers) > 1:
            html += """
            <button type="button" class="mqc_table_configModal_btn btn btn-default btn-sm" data-toggle="modal" data-target="#{tid}_configModal">
                <span class="glyphicon glyphicon-th"></span> Configure Columns
            </button>
            """.format(tid=table_id)

        # Sort By Highlight button
        html += """
        <button type="button" class="mqc_table_sortHighlight btn btn-default btn-sm" data-target="#{tid}" data-direction="desc" style="display:none;">
            <span class="glyphicon glyphicon-sort-by-attributes-alt"></span> Sort by highlight
        </button>
        """.format(tid=table_id)

        # Scatter Plot Button
        if len(t_headers) > 1:
            html += """
            <button type="button" class="mqc_table_makeScatter btn btn-default btn-sm" data-toggle="modal" data-target="#tableScatterModal" data-table="#{tid}">
                <span class="glyphicon glyphicon glyphicon-stats"></span> Plot
            </button>
            """.format(tid=table_id)

        # "Showing x of y columns" text
        html += """
        <small id="{tid}_numrows_text" class="mqc_table_numrows_text">Showing <sup id="{tid}_numrows" class="mqc_table_numrows">{nrows}</sup>/<sub>{nrows}</sub> rows and <sup id="{tid}_numcols" class="mqc_table_numcols">{ncols_vis}</sup>/<sub>{ncols}</sub> columns.</small>
        """.format(tid=table_id, nrows=len(t_rows), ncols_vis = (len(t_headers)+1)-hidden_cols, ncols=len(t_headers))

    # Build the table itself
    collapse_class = 'mqc-table-collapse' if len(t_rows) > 10 and config.collapse_tables else ''
    html += """
        <div id="{tid}_container" class="mqc_table_container">
            <div class="table-responsive mqc-table-responsive {cc}">
                <table id="{tid}" class="table table-condensed mqc_table" data-title="{title}">
        """.format( tid=table_id, title=table_title, cc=collapse_class)

    # Build the header row
    col1_header = dt.pconfig.get('col1_header', 'Sample Name')
    html += '<thead><tr><th class="rowheader">{}</th>{}</tr></thead>'.format(col1_header, ''.join(t_headers.values()))

    # Build the table body
    html += '<tbody>'
    t_row_keys = t_rows.keys()
    if dt.pconfig.get('sortRows') is not False:
        t_row_keys = sorted(t_row_keys)
    for s_name in t_row_keys:
        html += '<tr>'
        # Sample name row header
        html += '<th class="rowheader" data-original-sn="{sn}">{sn}</th>'.format(sn=s_name)
        for k in t_headers:
            html += t_rows[s_name].get(k, empty_cells[k])
        html += '</tr>'
    html += '</tbody></table></div>'
    if len(t_rows) > 10 and config.collapse_tables:
        html += '<div class="mqc-table-expand"><span class="glyphicon glyphicon-chevron-down" aria-hidden="true"></span></div>'
    html += '</div>'

    # Build the bootstrap modal to customise columns and order
    if not config.simple_output:
        html += """
    <!-- MultiQC Table Columns Modal -->
    <div class="modal fade" id="{tid}_configModal" tabindex="-1">
      <div class="modal-dialog modal-lg">
        <div class="modal-content">
          <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
            <h4 class="modal-title">{title}: Columns</h4>
          </div>
          <div class="modal-body">
            <p>Uncheck the tick box to hide columns. Click and drag the handle on the left to change order.</p>
            <p>
                <button class="btn btn-default btn-sm mqc_configModal_bulkVisible" data-target="#{tid}" data-action="showAll">Show All</button>
                <button class="btn btn-default btn-sm mqc_configModal_bulkVisible" data-target="#{tid}" data-action="showNone">Show None</button>
            </p>
            <table class="table mqc_table mqc_sortable mqc_configModal_table" id="{tid}_configModal_table" data-title="{title}">
              <thead>
                <tr>
                  <th class="sorthandle" style="text-align:center;">Sort</th>
                  <th style="text-align:center;">Visible</th>
                  <th>Group</th>
                  <th>Column</th>
                  <th>Description</th>
                  <th>ID</th>
                  <th>Scale</th>
                </tr>
              </thead>
              <tbody>
                {trows}
              </tbody>
            </table>
        </div>
        <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> </div>
    </div> </div> </div>""".format( tid=table_id, title=table_title, trows=''.join(t_modal_headers.values()) )

    # Save the raw values to a file if requested
    if dt.pconfig.get('save_file') is True:
        fn = dt.pconfig.get('raw_data_fn', 'multiqc_{}'.format(table_id) )
        util_functions.write_data_file(dt.raw_vals, fn )
        report.saved_raw_data[fn] = dt.raw_vals

    return html