def __init__(self, text: str = '', color: Optional[str] = None, width: Optional[int] = None, truncate: Optional[int] = None, padding: int = 10, collapsible: bool = False, align: str = 'c', minwidth: int = 0) -> None: self.text = text if truncate is None else _truncate(text, truncate) self.width = max( (get_text_width(self.text, 11) if width is None else width) + padding, minwidth) self.color = color self.collapsible = collapsible self.align = align
def graph_releases(project: str) -> Any: releases = [ _Release(**row) for row in get_db().get_project_releases(project) ] row_height = 20 bar_height_fraction = 0.75 width = 1140 height = row_height * len(releases) padding = 5 max_fadein = 20 max_time = time.time() min_time = min(min((release.start_ts for release in releases)), max_time - 1.0) version_column_width = padding * 2 + max( (get_text_width(release.version, 13, True) for release in releases)) time_column_width = width - version_column_width doc = XmlDocument('svg', xmlns='http://www.w3.org/2000/svg', width=width, height=height) with doc.tag('linearGradient', id='untrustedFadein', x2='100%', y2=0): doc.tag('stop', ('offset', 0), ('stop-color', '#9f9f9f'), ('stop-opacity', 0)) doc.tag('stop', ('offset', 1), ('stop-color', '#9f9f9f'), ('stop-opacity', 1)) with doc.tag('linearGradient', id='trustedFadein', x2='100%', y2=0): doc.tag('stop', ('offset', 0), ('stop-color', '#e05d44'), ('stop-opacity', 0)) doc.tag('stop', ('offset', 1), ('stop-color', '#e05d44'), ('stop-opacity', 1)) doc.tag('line', x1=time_column_width + 0.5, x2=time_column_width + 0.5, y1=0, y2=height, stroke='#00000040') def draw_bar(left: float, right: Optional[float], trusted: bool) -> None: left_frac = (left - min_time) / (max_time - min_time) right_frac = (right - min_time) / ( max_time - min_time) if right is not None else 1.0 fillname = '#e05d44' if trusted else '#9f9f9f' x = left_frac * time_column_width width = (right_frac - left_frac) * time_column_width y = nrow * row_height + row_height * (1.0 - bar_height_fraction) / 2 + 0.5 height = row_height * bar_height_fraction if left < config['HISTORY_CUTOFF_TIMESTAMP']: fadein_fillname = 'url(#trustedFadein)' if trusted else 'url(#untrustedFadein)' if width < max_fadein: # bar too small for separate fadein part, so just fill whole bar with gradient fillname = fadein_fillname else: # draw separated fadein part doc.tag('rect', x=x, width=max_fadein, y=y, height=height, fill=fadein_fillname) x += max_fadein width -= max_fadein doc.tag('rect', x=x, width=width, y=y, height=height, fill=fillname) for nrow, release in enumerate(releases): with doc.tag('g', ('fill', '#000'), ('font-family', 'DejaVu Sans,Verdana,Geneva,sans-serif'), ('font-size', 13), ('font-weight', 'bold'), ('text-anchor', 'left')): with doc.tag('text', x=time_column_width + padding + 0.5, y=nrow * row_height + row_height // 2 + 5 + 0.5): doc.text(release.version) if release.trusted_start_ts is not None: draw_bar(release.trusted_start_ts, release.end_ts, True) if release.trusted_start_ts != release.start_ts: draw_bar(release.start_ts, release.trusted_start_ts, False) else: draw_bar(release.start_ts, release.end_ts, False) if nrow > 0: doc.tag('line', x1=0, x2=width, y1=nrow * row_height + 0.5, y2=nrow * row_height + 0.5, stroke='#00000040') return (doc.render(), {'Content-type': 'image/svg+xml'})
def render_generic_badge(rows: List[List[BadgeCell]], header: Optional[str] = None, min_width: int = 0) -> Tuple[str, Dict[str, str]]: num_rows = len(rows) num_columns = len(rows[0]) if rows else 0 column_widths = [0] * num_columns column_collapsed = [True] * num_columns # calculate column widths for row in rows: for ncol, cell in enumerate(row): column_widths[ncol] = max(column_widths[ncol], cell.width) if cell.text or not cell.collapsible: column_collapsed[ncol] = False # handle collapsed columns and finalize metrics column_offsets = [0] * num_columns for ncol in range(num_columns): if column_collapsed[ncol]: column_widths[ncol] = 0 if ncol < num_columns - 1: column_offsets[ncol + 1] = column_offsets[ncol] + column_widths[ncol] header_height = 0 if header: header_height = 25 min_width = max(min_width, get_text_width(header, 15, bold=True) + 10) total_height = header_height + 20 * num_rows total_width = (column_offsets[-1] + column_widths[-1]) if num_columns else 0 # force minimal width by expanding leftmost column if total_width < min_width: increment = min_width - total_width total_width = min_width for ncol in range(num_columns): flexible_column = 0 if ncol == flexible_column: column_widths[ncol] += increment elif ncol > flexible_column: column_offsets[ncol] += increment doc = XmlDocument('svg', xmlns='http://www.w3.org/2000/svg', width=total_width, height=total_height) # define clip path for rounded corners with doc.tag('clipPath', id='clip'): doc.tag('rect', rx=3, width='100%', height='100%', fill='#000') # define linear gradient for bevel effect with doc.tag('linearGradient', id='grad', x2=0, y2='100%'): doc.tag('stop', ('offset', 0), ('stop-color', '#bbb'), ('stop-opacity', '.1')) doc.tag('stop', ('offset', 1), ('stop-opacity', '.1')) # graphical data with doc.tag('g', ('clip-path', 'url(#clip)')): # background doc.tag('rect', width='100%', height='100%', fill='#555') # header if header: with doc.tag('g', ('fill', '#fff'), ('text-anchor', 'middle'), ('font-family', 'DejaVu Sans,Verdana,Geneva,sans-serif'), ('font-size', 15), ('font-weight', 'bold')): with doc.tag('text', x=total_width / 2, y=18, fill='#010101', **{'fill-opacity': '.3'}): doc.text(header) with doc.tag('text', x=total_width / 2, y=17): doc.text(header) # rows for nrow, row in enumerate(rows): # cell backgrounds for ncol, cell in enumerate(row): if cell.color is None or column_collapsed[ncol]: continue doc.tag( 'rect', x=column_offsets[ncol], y=header_height + nrow * 20, width=column_widths[ncol], height=20, fill=cell.color ) # gradient doc.tag('rect', y=header_height + nrow * 20, width='100%', height=20, fill='url(#grad)') # cell texts with doc.tag('g', ('fill', '#fff'), ('font-family', 'DejaVu Sans,Verdana,Geneva,sans-serif'), ('font-size', 11)): for ncol, cell in enumerate(row): if not cell.text or column_collapsed[ncol]: continue text_x: float if cell.align.startswith('l'): text_x = column_offsets[ncol] + 5 text_anchor = 'start' elif cell.align.startswith('r'): text_x = column_offsets[ncol] + column_widths[ncol] - 5 text_anchor = 'end' else: text_x = column_offsets[ncol] + column_widths[ncol] / 2 text_anchor = 'middle' with doc.tag('text', x=text_x, y=header_height + nrow * 20 + 15, fill='#010101', **{'fill-opacity': '.3', 'text-anchor': text_anchor}): doc.text(cell.text) with doc.tag('text', x=text_x, y=header_height + nrow * 20 + 14, **{'text-anchor': text_anchor}): doc.text(cell.text) return ( doc.render(), {'Content-type': 'image/svg+xml'} )