예제 #1
0
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)
예제 #2
0
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