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))
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), )
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), )
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), )
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), )
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">×</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
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">×</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
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">×</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
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(), )
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 = { ">Q5": "<Q5", ">Q7": "Q5-7", ">Q10": "Q7-10", ">Q12": "Q10-12", ">Q15": "Q12-15", "rest": ">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">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), )
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">×</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