Skip to content

plots

main.plots ¤

Plots for displaying database data.

Functions¤

add_varea_glyph(plot, df, upper_trace, lower_trace, colour) ¤

Adds a varea glyph to add shading between traces.

The shading is applied when the upper trace is above the lower trace. If below, no shading is applied. VArea creates this shading between two sets of y-values: the element-wise maximum of the two traces and the lower trace. Otherwise, the shading is applied whenever either trace is above the other.

Parameters:

Name Type Description Default
plot figure

the plot to add the glyph to

required
df DataFrame

pandas DataFrame containing trace data

required
upper_trace str

the label of the upper trace

required
lower_trace str

the label of the lower trace

required
colour str

the colour to apply to the shading

required
Source code in main/plots.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def add_varea_glyph(
    plot: figure, df: pd.DataFrame, upper_trace: str, lower_trace: str, colour: str
) -> None:
    """Adds a varea glyph to add shading between traces.

    The shading is applied when the upper trace is above the lower trace. If below,
    no shading is applied. VArea creates this shading between two sets of y-values:
    the element-wise maximum of the two traces and the lower trace. Otherwise, the
    shading is applied whenever either trace is above the other.

    Args:
        plot: the plot to add the glyph to
        df: pandas DataFrame containing trace data
        upper_trace: the label of the upper trace
        lower_trace: the label of the lower trace
        colour: the colour to apply to the shading
    """
    source = ColumnDataSource(
        {
            "index": df["index"],
            "y1": df[lower_trace],
            "y2": df[upper_trace].combine(df[lower_trace], max),
        }
    )
    plot.add_glyph(
        source, VArea(x="index", y1="y1", y2="y2", fill_color=colour, fill_alpha=0.3)
    )

create_bar_plot(title, months, values, x_range=None) ¤

Creates a bar plot with dates versus values.

Parameters:

Name Type Description Default
title str

plot title

required
months list[str]

a list of months to display on the x-axis

required
values list[float]

a list of total charge values for the bar height indicate on the y-axis

required
x_range list[str] | None

(optional) list of values to use as the x_range for the displayed plot

None

Returns:

Type Description
figure

Bokeh figure for the bar chart.

Source code in main/plots.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def create_bar_plot(
    title: str,
    months: list[str],
    values: list[float],
    x_range: list[str] | None = None,
) -> figure:
    """Creates a bar plot with dates versus values.

    Args:
        title: plot title
        months: a list of months to display on the x-axis
        values: a list of total charge values for the bar height indicate on the y-axis
        x_range: (optional) list of values to use as the x_range for the displayed plot

    Returns:
        Bokeh figure for the bar chart.
    """
    source = ColumnDataSource(data=dict(months=months, values=values))
    plot = figure(
        x_range=months,  # type: ignore[arg-type]
        title=title,
        height=500,
        background_fill_color="#efefef",
        sizing_mode="stretch_width",
    )
    plot.yaxis.axis_label = "Total charge (£)"
    plot.xaxis.axis_label = "Month-Year"
    plot.vbar(x="months", top="values", width=0.5, source=source)
    if x_range:
        plot.x_range.factors = x_range  # type: ignore[attr-defined]
    # Add basic tooltips to show monthly totals
    hover = HoverTool()
    hover.tooltips = [
        ("Month", "@months"),
        ("Total", "£@values"),
    ]
    plot.add_tools(hover)
    return plot

create_capacity_planning_layout() ¤

Create the capacity planning plot in layout with widgets.

Creates the capacity planning plot plus the associated widgets used to control the data displayed in the plot.

Returns:

Type Description
Row

A Row object (the Row containing a Column of widgets and the plot).

Source code in main/plots.py
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
def create_capacity_planning_layout() -> Row:
    """Create the capacity planning plot in layout with widgets.

    Creates the capacity planning plot plus the associated widgets used to control the
    data displayed in the plot.

    Returns:
        A Row object (the Row containing a Column of widgets and the plot).
    """
    start, end = timezone.now(), timezone.now() + timedelta(days=365)
    # Min and max dates are three years before and ahead of current date
    min_date, max_date = start - timedelta(days=1095), start + timedelta(days=1095)

    # Get the plot to display (it is created with all data, but only the dates
    # in the x_range provided are shown)
    plot = create_capacity_planning_plot(
        start_date=min_date, end_date=max_date, x_range=(start, end)
    )

    # Create date picker widgets to control the dates shown in the plot
    start_picker, end_picker = widgets.get_plot_date_pickers(
        min_date=min_date.date(),
        max_date=max_date.date(),
        default_start=start.date(),
        default_end=end.date(),
    )
    widgets.add_timeseries_callback_to_date_pickers(start_picker, end_picker, plot)

    # Create buttons to set plot dates to some defaults
    calendar_button = Button(
        label="Current calendar year",
    )
    widgets.add_callback_to_button(
        button=calendar_button,
        dates=get_calendar_year_dates(),
        plot=plot,
        start_picker=start_picker,
        end_picker=end_picker,
    )

    financial_button = Button(
        label="Current financial year",
    )
    widgets.add_callback_to_button(
        button=financial_button,
        dates=get_financial_year_dates(),
        plot=plot,
        start_picker=start_picker,
        end_picker=end_picker,
    )

    # Create layout to display widgets aligned as a column next to the plot
    plot_layout = row(
        column(start_picker, end_picker, calendar_button, financial_button),
        plot,
        sizing_mode="stretch_width",
    )
    return plot_layout

create_capacity_planning_plot(start_date, end_date, x_range=None) ¤

Generates all the time series data and creates the capacity planning plot.

Includes all business days between the selected start and end date, inclusive of the start date. Timeseries for the effort (separate traces depending on project status) and capacity (aggregated over all users) are calculated.

Parameters:

Name Type Description Default
start_date datetime

datetime object representing the start of the plotting period

required
end_date datetime

datetime object representing the end of the plotting period

required
x_range tuple[datetime, datetime] | None

(optional) tuple of datetimes to use as the x_range for the displayed plot

None

Returns:

Type Description
figure

Bokeh figure containing timeseries data.

Source code in main/plots.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
def create_capacity_planning_plot(
    start_date: datetime,
    end_date: datetime,
    x_range: tuple[datetime, datetime] | None = None,
) -> figure:
    """Generates all the time series data and creates the capacity planning plot.

    Includes all business days between the selected start and end date, inclusive of
    the start date. Timeseries for the effort (separate traces depending on project
    status) and capacity (aggregated over all users) are calculated.

    Args:
        start_date: datetime object representing the start of the plotting period
        end_date: datetime object representing the end of the plotting period
        x_range: (optional) tuple of datetimes to use as the x_range for the displayed
            plot

    Returns:
        Bokeh figure containing timeseries data.
    """
    # Create overall capacity timeseries
    capacity_timeseries = timeseries.get_capacity_timeseries(start_date, end_date)
    traces = [
        {"timeseries": capacity_timeseries, "colour": "darkgreen", "label": "Capacity"}
    ]

    # Create individual effort timeseries according to project status
    projects = (  # Traces are cumulative
        ("Tentative", "firebrick", ["Tentative", "Confirmed", "Active"]),
        ("Confirmed", "orange", ["Confirmed", "Active"]),
        ("Active", "navy", ["Active"]),
    )
    for status, colour, filter in projects:
        effort_timeseries = timeseries.get_effort_timeseries(
            start_date, end_date, filter
        )
        traces.append(
            {
                "timeseries": effort_timeseries,
                "colour": colour,
                "label": f"{status} project effort",
            },
        )

    # Apply area shading between select traces
    vareas = (
        (("Capacity", "Confirmed project effort"), "green"),
        (("Confirmed project effort", "Active project effort"), "yellow"),
        (("Tentative project effort", "Confirmed project effort"), "red"),
    )

    plot = create_timeseries_plot(
        title="Project effort and team capacity over time",
        traces=traces,
        x_range=x_range,
        vareas=vareas,
    )
    return plot

create_cost_recovery_layout() ¤

Create the cost recovery plots in layout with widgets.

Creates the cost recovery timeseries plot and bar plot for monthly charges, plus the associated widgets used to control the data displayed in the plots.

Returns:

Type Description
Row

A Row object (the Row containing a Column of widgets and a Column of plots).

Source code in main/plots.py
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
def create_cost_recovery_layout() -> Row:
    """Create the cost recovery plots in layout with widgets.

    Creates the cost recovery timeseries plot and bar plot for monthly charges, plus the
    associated widgets used to control the data displayed in the plots.

    Returns:
        A Row object (the Row containing a Column of widgets and a Column of plots).
    """
    dates = get_month_dates_for_previous_years()

    # Get start and end date as datetimes for capacity timeseries
    min_date = datetime.combine(dates[0][0], time.min)
    max_date = datetime.combine(dates[-1][1], time.min)
    start = datetime.combine(dates[-12][0], time.min)

    # Get x-axis values for bar plot
    chart_months = [f"{date[0].strftime('%b')} {date[0].year}" for date in dates]

    # Plots are initialised with data for last 3 years but only the last year is shown
    # by default
    timeseries_plot, bar_plot = create_cost_recovery_plots(
        dates=dates,
        start_date=min_date,
        end_date=max_date,
        x_range=(start, max_date),
        chart_months=chart_months,
    )

    # Create date picker widgets to control the dates shown in the plot
    start_picker, end_picker = widgets.get_plot_date_pickers(
        min_date=min_date.date(),
        max_date=max_date.date(),
        default_start=start.date(),
        default_end=max_date.date(),
    )
    widgets.add_timeseries_callback_to_date_pickers(
        start_picker=start_picker, end_picker=end_picker, plot=timeseries_plot
    )
    widgets.add_bar_callback_to_date_pickers(
        start_picker=start_picker,
        end_picker=end_picker,
        plot=bar_plot,
        chart_months=chart_months,
    )

    # Create button to set plots to calendar year
    calendar_button = Button(
        label="Current calendar year",
    )
    widgets.add_callback_to_button(
        button=calendar_button,
        dates=get_calendar_year_dates(),
        plot=timeseries_plot,
        start_picker=start_picker,
        end_picker=end_picker,
        include_future_dates=False,
    )
    widgets.add_bar_callback_to_button(
        button=calendar_button,
        dates=get_calendar_year_dates(),
        plot=bar_plot,
        chart_months=chart_months,
    )

    # Create button to set plots to financial year
    financial_button = Button(
        label="Current financial year",
    )
    widgets.add_callback_to_button(
        button=financial_button,
        dates=get_financial_year_dates(),
        plot=timeseries_plot,
        start_picker=start_picker,
        end_picker=end_picker,
        include_future_dates=False,
    )
    widgets.add_bar_callback_to_button(
        button=financial_button,
        dates=get_financial_year_dates(),
        plot=bar_plot,
        chart_months=chart_months,
    )

    # Create layout to display widgets aligned as a column next to the plot
    plot_layout = row(
        column(start_picker, end_picker, calendar_button, financial_button),
        column(
            timeseries_plot,
            bar_plot,
            sizing_mode="stretch_width",
        ),
        sizing_mode="stretch_width",
    )
    return plot_layout

create_cost_recovery_plots(dates, start_date, end_date, x_range, chart_months) ¤

Creates the cost recovery plot for the last year.

Provides an overview of team capacity over the past year and the project charging.

Parameters:

Name Type Description Default
dates list[tuple[date, date]]

list of tuples (from oldest to most recent) containing dates for all months of the last 3 years; each tuple contains two dates for the first and last date of the month

required
start_date datetime

datetime object representing the start of the timeseries plotting period

required
end_date datetime

datetime object representing the end of the timeseries plotting period

required
x_range tuple[datetime, datetime]

(optional) tuple of datetimes to use as the x_range for the displayed plot

required
chart_months list[str]

list of months for x-axis in bar chart

required

Returns:

Type Description
tuple[figure, figure]

Tuple of Bokeh figures containing cost recovery data timeseries data and monthly charges for the past year.

Source code in main/plots.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
def create_cost_recovery_plots(
    dates: list[tuple[date, date]],
    start_date: datetime,
    end_date: datetime,
    x_range: tuple[datetime, datetime],
    chart_months: list[str],
) -> tuple[figure, figure]:
    """Creates the cost recovery plot for the last year.

    Provides an overview of team capacity over the past year and the project charging.

    Args:
        dates: list of tuples (from oldest to most recent) containing dates for all
            months of the last 3 years; each tuple contains two dates for the first
            and last date of the month
        start_date: datetime object representing the start of the timeseries plotting
            period
        end_date: datetime object representing the end of the timeseries plotting
            period
        x_range: (optional) tuple of datetimes to use as the x_range for the displayed
            plot
        chart_months: list of months for x-axis in bar chart

    Returns:
        Tuple of Bokeh figures containing cost recovery data timeseries data and
            monthly charges for the past year.
    """
    # Create timeseries plot
    cost_recovery_timeseries, monthly_totals = timeseries.get_cost_recovery_timeseries(
        dates
    )
    capacity_timeseries = timeseries.get_capacity_timeseries(
        start_date=start_date, end_date=end_date
    )

    internal_effort_timeseries = timeseries.get_internal_effort_timeseries(
        start_date=start_date, end_date=end_date
    )

    number_team_members = timeseries.get_team_members_timeseries(
        start_date=start_date, end_date=end_date
    )

    internal_project_effort = internal_effort_timeseries / number_team_members

    # charged project effort using cost recovery timeseries divided by team members
    charged_project_effort = cost_recovery_timeseries / number_team_members

    # total project effort for all projects
    total_project_effort = charged_project_effort + internal_project_effort

    # in %
    avg_project_capacity_pct = capacity_timeseries / number_team_members * 100

    total_capacity_used_pct = total_project_effort * 100

    charged_capacity_used_pct = charged_project_effort * 100

    traces = [
        {
            "timeseries": avg_project_capacity_pct,
            "colour": "gold",
            "label": "Average capacity for project work %",
        },
        {
            "timeseries": total_capacity_used_pct,
            "colour": "navy",
            "label": "Fraction of capacity used for all projects %",
        },
        {
            "timeseries": charged_capacity_used_pct,
            "colour": "green",
            "label": "Fraction of capacity used for charged projects %",
        },
    ]
    timeseries_plot = create_timeseries_plot(
        title=("Team capacity and project charging over time"),
        traces=traces,
        x_range=x_range,
    )

    # Create bar plot for monthly charges
    bar_plot = create_bar_plot(
        title=("Total monthly charges"),
        months=chart_months,
        values=monthly_totals,
        x_range=(chart_months[-12:]),
    )
    return timeseries_plot, bar_plot

create_timeseries_plot(title, traces, x_range=None, vareas=None) ¤

Creates a generic timeseries plot.

Parameters:

Name Type Description Default
title str

plot title

required
traces list[dict[str, Any]]

a list of dictionaries with keys for the 'timeseries' data, 'label' and 'colour'

required
x_range tuple[datetime, datetime] | None

(optional) tuple of datetimes to use as the x_range for the displayed plot

None
vareas tuple[tuple[tuple[str, str], str], ...] | None

(optional) tuple of tuples, containing a tuple of trace labels to apply shading between and the colour to use, e.g. ((("Capacity", "Project effort"), "Green"), ...)

None

Returns:

Type Description
figure

Bokeh figure containing timeseries data.

Source code in main/plots.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
def create_timeseries_plot(  # type: ignore[explicit-any]
    title: str,
    traces: list[dict[str, Any]],
    x_range: tuple[datetime, datetime] | None = None,
    vareas: tuple[tuple[tuple[str, str], str], ...] | None = None,
) -> figure:
    """Creates a generic timeseries plot.

    Args:
        title: plot title
        traces: a list of dictionaries with keys for the 'timeseries' data, 'label' and
            'colour'
        x_range: (optional) tuple of datetimes to use as the x_range for the displayed
            plot
        vareas: (optional) tuple of tuples, containing a tuple of trace labels to apply
            shading between and the colour to use, e.g.
            ((("Capacity", "Project effort"), "Green"), ...)

    Returns:
        Bokeh figure containing timeseries data.
    """
    # Create ColumnDataSource from trace data
    df = pd.DataFrame({trace["label"]: trace["timeseries"] for trace in traces})
    df.reset_index(inplace=True)
    df["index"] = pd.to_datetime(df["index"]).dt.date
    source = ColumnDataSource(df)

    plot = figure(
        title=title,
        height=500,
        background_fill_color="#efefef",
        x_axis_type="datetime",  # type: ignore[call-arg]
        tools="save,xpan,xwheel_zoom,reset",
        sizing_mode="stretch_width",
    )
    if x_range:
        plot.x_range = Range1d(x_range[0], x_range[1])
    plot.yaxis.axis_label = "Value"
    plot.xaxis.axis_label = "Date"

    lines = []
    for trace in traces:
        line = plot.line(
            "index",
            trace["label"],
            source=source,
            line_width=2,
            color=trace["colour"],
            legend_label=trace["label"],
        )
        lines.append(line)

    # If provided, add varea shading between traces
    if vareas:
        for labels, colour in vareas:
            add_varea_glyph(plot, df, labels[0], labels[1], colour)

    hover = HoverTool(
        tooltips=[
            ("Date", "$x{%F}"),
            ("Value", "$y{0.00}"),
        ],
        formatters={"$x": "datetime"},
        renderers=lines,  # type: ignore[arg-type]
    )
    plot.add_tools(hover)

    plot.legend.click_policy = "hide"  # hides traces when clicked in legend

    plot.legend.location = "bottom_left"

    return plot

html_components_from_plot(plot, prefix=None) ¤

Generate HTML components from a Bokeh plot that can be added to the context.

Parameters:

Name Type Description Default
plot figure | Row

Bokeh figure to be added to the context

required
prefix str | None

optional prefix to use in the context keys

None
Source code in main/plots.py
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
def html_components_from_plot(
    plot: figure | Row, prefix: str | None = None
) -> dict[str, str]:
    """Generate HTML components from a Bokeh plot that can be added to the context.

    Args:
        plot: Bokeh figure to be added to the context
        prefix: optional prefix to use in the context keys
    """
    script, div = components(plot)
    if prefix:
        return {
            f"{prefix}_script": script,
            f"{prefix}_div": div,
        }

    return {
        "script": script,
        "div": div,
    }