def pe_time_series_selection_to_sunburst_and_transaction_table( figure, selectedData, time_resolution, time_span, data_store, param_store): """Selecting specific points from the time series chart updates the account burst and the detail labels. Reminder to self: When you think selectedData input is broken, remember that unaltered default action in the graph is to zoom, not to select. Note: all of the necessary information is in figure but that doesn't seem to trigger reliably. Adding selectedData as a second Input causes reliable triggering. """ params: Params() = Params.from_json(param_store) preventupdate_if_empty(data_store) trans, atree, eras, dstore = Datastore.get_parts(data_store, params.pe_roots) if not time_resolution: time_resolution = params.init_time_res if not time_span: time_span = params.init_time_span if len(trans) == 0: app.logger.error( "Tried to make burst figure from transactions, but no transactions provided." ) raise PreventUpdate() unit = params.unit ts_label = CONST["time_span_lookup"].get(time_span)["label"] min_period_start: np.datetime64 = None max_period_end: np.datetime64 = None selected_accounts = [] selected_trans = pd.DataFrame() desc_account_count = 0 colormap = {} # Get the names and colors of all accounts in the Input figure. # If anything is clicked, set the selection dates, accounts, and transactions. if figure: for trace in figure.get("data"): account = trace.get("name") points = trace.get("selectedpoints") colormap[account] = trace.get("marker").get("color") if not points: continue selected_accounts.append(account) for point in points: point_x = trace["x"][point] period_start, period_end = period_to_date_range( time_resolution, point_x, eras) if min_period_start is None: min_period_start = period_start else: min_period_start = min(min_period_start, period_start) if max_period_end is None: max_period_end = period_end else: max_period_end = max(max_period_end, period_end) desc_accounts = atree.get_descendent_ids(account) desc_account_count = desc_account_count + len(desc_accounts) subtree_accounts = [account] + desc_accounts new_trans = (trans.loc[trans["account"].isin( subtree_accounts)].loc[trans["date"] >= period_start].loc[ trans["date"] <= period_end]) new_trans = Ledger.positize( new_trans) # each top-level account should net positive if len(selected_trans) > 0: selected_trans = selected_trans.append(new_trans) else: selected_trans = new_trans selected_count = len(selected_trans) if selected_count > 0 and len(selected_accounts) > 0: # If there are selected data, describe the contents of the sunburst # TODO: desc_account_count is still wrong. description = Burst.pretty_account_label( selected_accounts, desc_account_count, selected_count, ) else: # If no trans are selected, show everything. Note that we # could logically get here even if valid accounts are # selected, in which case it would be confusing to get back # all trans instead of none, but this should never happen haha # because any clickable bar must have $$, and so, trans description = f"Click a bar in the graph to filter from {len(trans):,d} records" selected_trans = trans min_period_start = trans["date"].min() max_period_end = trans["date"].max() title = f"{ts_label} {unit} from {pretty_date(min_period_start)} to {pretty_date(max_period_end)}" pe_selection_store = { "start": min_period_start, "end": max_period_end, "count": len(selected_trans), "accounts": selected_accounts, } duration = round( pd.to_timedelta((max_period_end - min_period_start), unit="ms") / np.timedelta64(1, "M")) factor = Ledger.prorate_factor(time_span, duration=duration) try: sun_fig = Burst.from_trans(atree, selected_trans, time_span, unit, factor, colormap, title) except LError as E: text = f"Failed to generate sunburst. Error: {E}" app.logger.warning(text) description = text return (sun_fig, title, description, pe_selection_store)
def periodic_bar( trans: pd.DataFrame, account_tree: ATree, account_id: str, time_resolution: str, time_span: str, factor: float, eras: pd.DataFrame, color_num: int = 0, deep: bool = False, positize: bool = False, unit: str = CONST["unit"], sel_start_date: str = None, sel_end_date: str = None, ) -> go.Bar: """returns a go.Bar object with total by time_resolution period for the selected account. If deep, include total for all descendent accounts.""" if deep: tba = trans[trans[CONST["account_col"]].isin( [account_id] + account_tree.get_descendent_ids(account_id))] else: tba = trans[trans[CONST["account_col"]] == account_id] if len(tba) == 0: raise LError('no transactions sent to periodic_bar') tba = Ledger.positize(tba) tba = tba.set_index("date") tba['selected'] = True if sel_start_date: tba.loc[tba.index < np.datetime64(sel_start_date), 'selected'] = False if sel_end_date: tba.loc[tba.index > np.datetime64(sel_end_date), 'selected'] = False tr: dict = CONST["time_res_lookup"][time_resolution] tr_format: str = tr.get("format", None) # e.g., %Y-%m abbrev = CONST["time_span_lookup"][time_span]["abbrev"] try: marker_color = disc_colors[color_num] except IndexError: # don't ever run out of colors marker_color = "var(--Cyan)" if time_resolution in [ "decade", "year", "quarter", "month", "week", "day" ]: if time_resolution == "decade": tba["year"] = tba.index.year tba["dec"] = tba["year"].floordiv(10).mul(10).astype("str") bin_amounts = tba.groupby("dec").sum()["amount"].to_frame( name="value") bin_amounts['selected'] = tba.groupby("dec").all()['selected'] bin_amounts["x"] = bin_amounts.index else: bin_amounts = (tba.resample( tr["resample_keyword"]).sum()["amount"].to_frame(name="value")) bin_amounts["x"] = bin_amounts.index.to_period().strftime( tr_format) bin_amounts["selected"] = tba.resample( tr["resample_keyword"]).apply(all)['selected'] bin_amounts = bin_amounts[bin_amounts['value'] > 0] bin_amounts["y"] = bin_amounts["value"] * factor bin_amounts["unit"] = unit bin_amounts["abbrev"] = abbrev bin_amounts[ "label_pre"] = f"{account_id}<br>{unit}" # this works because these are variables, not column names selected = bin_amounts.reset_index()[bin_amounts.reset_index() ['selected']].index.to_list() trace = go.Bar( name=account_id, x=bin_amounts.x, y=bin_amounts.y, text=bin_amounts.label_pre, textposition="auto", selectedpoints=selected, opacity=0.9, customdata=bin_amounts.abbrev, texttemplate="%{text}%{y:,.0f}%{customdata}", hovertemplate= "%{x}<br>%{text}%{y:,.0f}%{customdata}<extra></extra>", marker_color=marker_color, ) elif time_resolution == "era": if len(eras) == 0: raise LError("era was selected but no eras are available.") # convert the era dates to a series that can be used for grouping bins = eras.date_start.sort_values() bin_boundary_dates = bins.tolist() bin_labels = bins.index.tolist() bin_labels = bin_labels[0:-1] try: tba["bin"] = pd.cut( x=tba.index, bins=bin_boundary_dates, labels=bin_labels, duplicates="drop", ) except ValueError as E: app.logger.warning( "An error making the bins for eras. Probably because of design errors" + f" in how eras are parsed and loaded: {E}. Bins are {bins}") return None bin_amounts = pd.DataFrame({ "date": bin_boundary_dates[0:-1], "value": tba.groupby("bin")["amount"].sum(), }) bin_amounts["date_start"] = bin_boundary_dates[0:-1] bin_amounts["date_end"] = bin_boundary_dates[1:] bin_amounts[ "delta"] = bin_amounts["date_end"] - bin_amounts["date_start"] bin_amounts["width"] = bin_amounts["delta"] / np.timedelta64(1, "ms") bin_amounts[ "midpoint"] = bin_amounts["date_start"] + bin_amounts["delta"] / 2 bin_amounts["months"] = bin_amounts["delta"] / np.timedelta64(1, "M") bin_amounts["title"] = bin_amounts.index.astype(str) bin_amounts[ "value"] = bin_amounts["value"] * factor / bin_amounts["months"] bin_amounts["pretty_value"] = bin_amounts["value"].apply( "{:,.0f}".format) bin_amounts["suffix"] = str(unit) + str(abbrev) bin_amounts["account_id"] = account_id bin_amounts["customdata"] = ("From " + bin_amounts["date_start"].astype(str) + " to " + bin_amounts["date_end"].astype(str)) bin_amounts["text"] = (bin_amounts["account_id"] + "<br>" + bin_amounts["pretty_value"] + " " + bin_amounts["suffix"] + "<br>" + bin_amounts.index.astype(str)) trace = go.Bar( name=account_id, x=bin_amounts.midpoint, width=bin_amounts.width, y=bin_amounts.value, customdata=bin_amounts.customdata, text=bin_amounts.text, textposition="auto", opacity=0.9, texttemplate="%{text}", hovertemplate="%{customdata}<extra></extra>", marker_color=marker_color, ) else: raise LError( f"Invalid keyword for time_resolution: {time_resolution}, or eras is specified but empty." ) return trace