def multi_spec_dropdown(id_: str, role: str) -> dcc.Dropdown: """Constructs spec dropdown for page 2 composition app. Parameters ---------- id_ : str html id of the component role : str specs to include in the figure; one of {'tank', 'healer', 'mdps', 'rdps'} Returns ------- dropdown : dcc.Dropdown dropdown component with specs of given role """ dropdown = dcc.Dropdown( id=id_, className="spec-input", options=[ { "label": spec["spec_name"].upper() + " " + spec["class_name"].upper(), "value": spec["token"], } for spec in blizzcolors.Specs().specs if spec["role"][-3:] in role ], multi=True, placeholder="ALL INCLUDED BY DEFAULT.", ) drop_wrap = html.Div( children=[html.Label(role.upper()), dropdown], ) return drop_wrap
def __init__(self, data, xaxis_type, spec_role, patch): self.specs = blizzcolors.Specs() self.xaxis_type = xaxis_type self.spec_role = spec_role self.data = self.normalize_for_role(data, spec_role) self.patch = patch self.traces = None
def _sort_annos_by_spec(annotations, keep_role): """Given a dictionary of annotations, return those that match role.""" valid_roles = ["tank", "healer", "mdps", "rdps"] specs = blizzcolors.Specs() if keep_role not in valid_roles: raise ValueError("Spec role invalid. Must be one of: %s") keep_annotations = [] for spec_id, annotation in annotations.items(): spec_role = specs.get_role(spec_id) if spec_role == keep_role: keep_annotations.append(annotation) return keep_annotations
def _make_spec_name_annos(self, sorted_summary): """Make spec name annotations.""" annotations = {} vertical_offset = self._calculate_vertical_offset() num_specs = len(sorted_summary) specs = blizzcolors.Specs() for index, row in enumerate(list(sorted_summary.values)): spec_id = row[0] spec_name = specs.get_spec_name(spec_id).upper() baseline_y = vertical_offset * (num_specs - index) anno = self._make_spec_name_annotation(position_x=0, position_y=baseline_y, text=spec_name) annotations[spec_id] = anno return annotations
def _construct_traces(self, sorted_summary): """Makes line/fill traces of the data distribution.""" key_levels = list(self.data) key_levels_x = [i - 2 for i in list(key_levels)] # x is 0, key is 2 vertical_offset = self._calculate_vertical_offset() num_specs = len(self.summary) specs = blizzcolors.Specs() traces = {} for index, row in enumerate(list(sorted_summary.values)): spec_id = row[0] spec_color = "rgba(%d,%d,%d,0.9)" % specs.get_color(spec_id) runs = self.data.loc[self.data.index == spec_id].values[0] # horizontal baseline for each ridge distribution baseline_y = vertical_offset * (num_specs - index) baseline = go.Scatter( x=key_levels_x, y=[baseline_y] * len(key_levels_x), line=dict(width=0.5, color="black"), hoverinfo="skip", mode="lines", ) # the distribution of runs vs key level (the star of the show) ridge = go.Scatter( x=key_levels_x, y=[baseline_y + run for run in runs], fill="tozerox", fillcolor=spec_color, mode="lines", line=dict(width=1, color="black", shape="spline"), name=specs.get_spec_name(spec_id).upper(), text=[f"{y:,}" for y in runs], customdata=[i + 2 for i in key_levels_x], hovertemplate="KEY LEVEL: +%{customdata}<br>RUNS: %{text}", ) traces[spec_id] = {"ridge": ridge, "baseline": baseline} return traces
def _get_ridge_recolor_pattern(self, keep_role): """Generates color list that informs recolor of the traces. Given a spec role, recolors all traces not of that role to gray. Parameters ---------- keep_role : str spec role ('tank', 'healer', 'mdps', 'rdps') Returns ------- recolor : list list of colors, where color at index i corresponds to trace at index i in fig.data """ valid_roles = ["tank", "healer", "mdps", "rdps"] keep_role = keep_role.lower() if keep_role not in valid_roles: raise ValueError("Spec role invalid. Must be one of: %s") recolor = [] specs = blizzcolors.Specs() custom_gray = "rgba(0,0,0,0.3)" for spec_id, spec_traces in self.traces.items(): spec_role = specs.get_role(spec_id) # reassign color based on spec role new_ridge_color = None if spec_role == keep_role: new_ridge_color = spec_traces["ridge"].fillcolor else: new_ridge_color = custom_gray # keep baseline the original color baseline_color = spec_traces["baseline"].line.color recolor.append(new_ridge_color) recolor.append(baseline_color) return recolor
def format_output(result0: pd.DataFrame) -> html.Table: """Formats results of the comp search into a data table.""" # Formatting the results is a massive PITA. # There are 40+ columns, and condensing them into something that # both fits on the screen and is interpretable is rough. # So here is what we do: # Find the tanks and condense them into a single column. tank_cols = [ "death_knight_blood", "demon_hunter_vengeance", "druid_guardian", "monk_brewmaster", "paladin_protection", "warrior_protection", ] tanks = result0[tank_cols].apply(lambda row: tank_cols[list(row).index(1)], axis=1) # Find the healers and condense them into a single column. healer_cols = [ "druid_restoration", "monk_mistweaver", "paladin_holy", "priest_discipline", "priest_holy", "shaman_restoration", ] healers = result0[healer_cols].apply( lambda row: healer_cols[list(row).index(1)], axis=1) # drop both healers and tanks from the main table result0.drop( inplace=True, labels=tank_cols + healer_cols, axis=1, ) # and append the condensed columns result0["tank"] = tanks result0["healer"] = healers # remove stats for now stats = result0[["run_count", "level_mean", "level_max"]].copy(deep=True) stats = stats.round(decimals=1) stats = list(stats.values) result0.drop( inplace=True, labels=[ "composition", "run_count", "level_mean", "level_std", "level_max" ], axis=1, ) t0 = time.time() # Drop DPS columns that are all 0. mask = list(result0.sum(axis=0) != 0) result0 = result0.loc[:, mask] # rearrange columns new_order = ["tank", "healer"] dps_cols = [dps for dps in result0.columns if dps not in new_order] dps_order = result0[dps_cols].sum(axis=0) dps_order = dps_order.sort_values(ascending=False, inplace=False).index.values new_order.extend(dps_order) result0 = result0[new_order] rows = result0.apply(lambda row: list(row), axis=1) colors = dict([[spec["token"], spec["color"]] for spec in blizzcolors.Specs().specs]) chars = 10 if len(result0.columns) >= 20: chars = 2 elif len(result0.columns) < 20 and len(result0.columns) >= 17: chars = 3 elif len(result0.columns) < 17 and len(result0.columns) >= 10: chars = 4 abbr = dict([[spec["token"], spec["abbr"][:chars]] for spec in blizzcolors.Specs().specs]) table = html.Table(children=[]) for row_index, row in enumerate(rows): row_ = html.Tr(children=[ html.Td(stats[row_index][0]), html.Td("%1.1f" % stats[row_index][1]), html.Td(stats[row_index][2]), ]) bg_color = "lightgray" if row_index % 2 == 0 else "white" for index, cell in enumerate(row): style = {} if result0.columns[index] in ["tank", "healer"]: color = colors[cell] style = { "background-color": "rgb(%d,%d,%d)" % color, "border": "1px solid black", } title = cell.upper().replace("_", " ") cell = abbr[cell] elif cell == 0: style = {"background-color": bg_color, "color": bg_color} title = "" else: style = { "background-color": "rgb(%d,%d,%d)" % colors[result0.columns[index]], "border": "1px solid black", } title = result0.columns[index].upper().replace("_", " ") if cell == 1: cell = abbr[result0.columns[index]] else: cell = "x%d" % cell row_.children.append(html.Td(cell, title=title, style=style)) row_.style = {"background-color": bg_color} table.children.append(row_) table.children.insert( 0, html.Tr( [ html.Th(column[:chars]) for column in ["N", "AVG", "MAX", "TANK", "HLR"] + [abbr[tkn] for tkn in result0.columns[2:]] ], style={"font-size": "15px"}, ), ) return table
def create_figure(self, bounds: List[int]) -> go.Figure: """Creates horizontal bar figure for meta index. Parameters ---------- bounds : List[int] list of ints that defines key level cohorts to calculate the index Returns ------- figure : go.Figure The bar chart of spec meta indices """ spec_meta = self._calculate_index(bounds) spec_meta.sort_values(by="spec_meta_index", inplace=True) spec_utils = blizzcolors.Specs() fig = go.Figure() fig = self._add_tier_annotations(fig) trace = go.Bar( x=list(spec_meta.spec_meta_index), y=list(range(len(spec_meta))), marker_color=[ "rgba(%d,%d,%d,0.9)" % spec_utils.get_color(spec_id) if spec_utils.get_role(spec_id) == self.spec_role or self.spec_role == "all" else "rgba(1,1,1,0.4)" for spec_id in spec_meta.index ], text=[ spec_utils.get_spec_name(spec_id).upper() for spec_id in spec_meta.index ], hovertemplate="%{text}, meta ratio = %{x:1.2f}<extra></extra>", marker_line_color="black", orientation="h", ) fig.add_traces(trace) fig.update_layout( height=1100, xaxis_title= "Meta Ratio (spec % at meta level / spec % at population level)", showlegend=False, title=dict( text="Spec Tiers", font_size=15, x=0.5, xanchor="center", yanchor="top", ), yaxis_range=[-1, len(spec_meta.index)], ) # This is annoying... I have to update bar labels separetly. # I can't do it as part of go.Bar() definition, because some of the labels # are identical and plotly merges them into a single bar # (eg Frost mage and Frost DK are shown as a single bar) fig.update_yaxes( tickmode="array", tickvals=trace.y, ticktext=[ spec_utils.get_spec_name(spec_id).upper() for spec_id in spec_meta.index ], ) return fig
def __init__(self, data, patch): """Inits with spec run data.""" self.data = data.sum(axis=1).sort_values(ascending=False) self.patch = patch self.specs = blizzcolors.Specs()