Skip to content

Commit 0793a88

Browse files
committed
time planning evaluation feature
1 parent dcefff1 commit 0793a88

File tree

9 files changed

+511
-38
lines changed

9 files changed

+511
-38
lines changed
Binary file not shown.

notebooks/walkthrough/01-user-setup.ipynb

Lines changed: 333 additions & 15 deletions
Large diffs are not rendered by default.

notebooks/walkthrough/02-projects-setup.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"metadata": {},
5454
"outputs": [],
5555
"source": [
56-
"app = tuttle.app.App()"
56+
"app = tuttle.app.App(home_dir=\".demo_home\")"
5757
]
5858
},
5959
{

notebooks/walkthrough/03-timetracking-invoicing.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"metadata": {},
5555
"outputs": [],
5656
"source": [
57-
"app = tuttle.app.App()"
57+
"app = tuttle.app.App(home_dir=\".demo_home\")"
5858
]
5959
},
6060
{
@@ -124,7 +124,7 @@
124124
{
125125
"data": {
126126
"application/vnd.jupyter.widget-view+json": {
127-
"model_id": "985d76df8ef041ef9972c10ec83daada",
127+
"model_id": "3f3d378ae0a54c1ab6ef7688ec02d25c",
128128
"version_major": 2,
129129
"version_minor": 0
130130
},

notebooks/walkthrough/04-planning-forecasting.ipynb

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,70 @@
55
"id": "eb6a0974-df12-4247-b398-b4e3641b09f3",
66
"metadata": {},
77
"source": [
8-
"# Planning Ahead\n"
8+
"# Planning Ahead and Forecasting Revenue\n"
9+
]
10+
},
11+
{
12+
"cell_type": "markdown",
13+
"id": "be57da1d-f6b1-40db-be21-00aa84d464cc",
14+
"metadata": {},
15+
"source": [
16+
"Planning your time is essential for a freelance business. Tuttle enables you to easily forecast your revenue based on the planning decisions that you make."
17+
]
18+
},
19+
{
20+
"cell_type": "markdown",
21+
"id": "dfd8549c-1e57-43fb-8c15-772f196a8473",
22+
"metadata": {},
23+
"source": [
24+
"## Preamble"
25+
]
26+
},
27+
{
28+
"cell_type": "code",
29+
"execution_count": 1,
30+
"id": "daf6add6-6e4c-49c0-9967-77b1925cf2ac",
31+
"metadata": {},
32+
"outputs": [],
33+
"source": [
34+
"from pathlib import Path\n",
35+
"import ipywidgets\n",
36+
"from IPython import display\n",
37+
"import datetime"
38+
]
39+
},
40+
{
41+
"cell_type": "code",
42+
"execution_count": 2,
43+
"id": "1872b84b-46c8-47ab-8ff6-505d8e555327",
44+
"metadata": {},
45+
"outputs": [],
46+
"source": [
47+
"import tuttle"
48+
]
49+
},
50+
{
51+
"cell_type": "code",
52+
"execution_count": 3,
53+
"id": "10f0f645-f758-4164-8e43-dbe63c147233",
54+
"metadata": {},
55+
"outputs": [],
56+
"source": [
57+
"app = tuttle.app.App()"
58+
]
59+
},
60+
{
61+
"cell_type": "markdown",
62+
"id": "e5dc0958-2a7d-429c-b9f5-6fe17103864b",
63+
"metadata": {},
64+
"source": [
65+
"## How to Plan Project Time"
966
]
1067
},
1168
{
1269
"cell_type": "code",
1370
"execution_count": null,
14-
"id": "28fda6d1-a61d-4c19-b621-030790f7d149",
71+
"id": "232f62f7-706d-47f4-b837-c677af8e332a",
1572
"metadata": {},
1673
"outputs": [],
1774
"source": []

tuttle/app.py

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,20 @@
33
import os
44
import sys
55

6+
import pandas
67
import sqlmodel
78

8-
from . import model
9+
from . import model, timetracking, dataviz
910

1011

1112
class App:
1213
"""The main application class"""
1314

14-
def __init__(self, debug_mode=False, verbose=False, in_memory=False):
15-
if debug_mode:
16-
self.home = Path("./test_home")
17-
else:
15+
def __init__(self, home_dir=None, verbose=False, in_memory=False):
16+
if home_dir is None:
1817
self.home = Path.home() / ".tuttle"
18+
else:
19+
self.home = Path(home_dir)
1920
if not os.path.exists(self.home):
2021
os.mkdir(self.home)
2122
if in_memory:
@@ -111,3 +112,52 @@ def get_project(
111112
return project
112113
else:
113114
raise ValueError("either project title or tag required")
115+
116+
def eval_time_planning(
117+
self,
118+
planning_source,
119+
by="project",
120+
):
121+
def duration_to_revenue(
122+
row,
123+
):
124+
if isinstance(row.name, tuple):
125+
tag = row.name[0]
126+
else:
127+
tag = row.name
128+
project = self.get_project(tag=tag)
129+
units = row["duration"] / project.contract.unit.to_timedelta()
130+
rate = project.contract.rate
131+
revenue = units * float(rate)
132+
return {
133+
"units": units,
134+
"revenue": revenue,
135+
"currency": project.contract.currency,
136+
}
137+
138+
planning_data = timetracking.get_time_planning_data(
139+
planning_source,
140+
)
141+
if by == "project":
142+
grouped_planning_data = (
143+
planning_data.filter(["tag", "duration"]).groupby("tag").sum()
144+
)
145+
elif by == ("month", "project"):
146+
grouped_planning_data = (
147+
planning_data.filter(["tag", "duration"])
148+
.groupby(["tag", pandas.Grouper(freq="1M")])
149+
.sum()
150+
)
151+
else:
152+
raise ValueError(f"unrecognized grouping parameter: {by}")
153+
# postprocess planning data
154+
expanded_data = grouped_planning_data.join(
155+
grouped_planning_data.apply(
156+
duration_to_revenue, axis=1, result_type="expand"
157+
)
158+
)
159+
plot = dataviz.plot_eval_time_planning(
160+
expanded_data,
161+
by=by,
162+
)
163+
return plot

tuttle/dataviz.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99
# ALTAIR THEMES
1010

1111
# Vega color schemes: https://vega.github.io/vega/docs/schemes/
12-
default_color_scheme = "category20c"
12+
default_color_scheme = "category20"
1313

1414

1515
def tuttle_dark():
1616
return {
1717
"config": {
1818
#'view': {'continuousHeight': 300, 'continuousWidth': 400}, # from the default theme
19-
"range": {"category": {"scheme": "category20c"}}
19+
"range": {"category": {"scheme": "category20"}}
2020
}
2121
}
2222

@@ -33,7 +33,46 @@ def enable_theme(theme_name="tuttle_dark"):
3333
raise ValueError("unknown theme: {theme_name}")
3434

3535

36-
@deprecated("Use dataviz.enable_theme('default_dark') instead")
37-
def enable_dark_theme():
38-
"""Enable the built-in Altair dark theme"""
39-
altair.renderers.set_embed_options(theme="dark")
36+
def plot_eval_time_planning(
37+
planning_data,
38+
by,
39+
):
40+
if by == "project":
41+
plot_data = (
42+
planning_data.reset_index()
43+
.filter(["tag", "revenue"])
44+
.rename(columns={"tag": "project"})
45+
)
46+
plot = (
47+
altair.Chart(plot_data)
48+
.mark_bar()
49+
.encode(
50+
y="project:N",
51+
x="revenue:Q",
52+
)
53+
.properties(width=600)
54+
)
55+
elif by == ("month", "project"):
56+
plot_data = (
57+
planning_data.reset_index()
58+
.filter(["tag", "begin", "revenue"])
59+
.rename(columns={"tag": "project", "begin": "month_end"})
60+
)
61+
plot = (
62+
altair.Chart(plot_data)
63+
.mark_bar()
64+
.encode(
65+
y=altair.Y(
66+
"yearmonth(month_end):O",
67+
axis=altair.Axis(title="month"),
68+
),
69+
x=altair.X(
70+
"revenue:Q",
71+
),
72+
color="project:N",
73+
)
74+
.properties(width=600)
75+
)
76+
else:
77+
raise ValueError(f"unknown mode {by}")
78+
return plot

tuttle/schema.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@
2222
"duration": Column(Timedelta),
2323
},
2424
)
25+
26+
time_planning = time_tracking # REVIEW: identical?

tuttle/timetracking.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,14 +213,21 @@ def progress(
213213
return total_time.loc[tag]["duration"] / budget
214214

215215

216-
def eval_time_allocation(
216+
@check_io(
217+
out=schema.time_planning,
218+
)
219+
def get_time_planning_data(
217220
source,
218-
):
221+
from_date: datetime.date = None,
222+
) -> DataFrame:
223+
"""Get time planning data from a source."""
224+
if from_date is None:
225+
from_date = datetime.date.today()
219226
if issubclass(type(source), Calendar):
220227
cal = source
221-
timetracking_data = cal.to_data()
228+
planning_data = cal.to_data()
222229
elif isinstance(source, pandas.DataFrame):
223-
timetracking_data = source
224-
schema.time_tracking.validate(timetracking_data)
225-
226-
return timetracking_data
230+
planning_data = source
231+
schema.time_tracking.validate(planning_data)
232+
planning_data = planning_data[str(from_date) :]
233+
return planning_data

0 commit comments

Comments
 (0)