├── LICENSE
├── README.md
├── clock_plot
├── __init__.py
└── clock.py
├── data
├── eden_2_houseid_324_combined_data.csv
└── seasonal_gas_usage.png
├── examples.ipynb
├── pyproject.toml
├── release_notes.txt
└── setup.py
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [2022] [Energy Systems Catapult]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## clock_plot
2 |
3 | clock_plot provides a simple way to visualize timeseries data mapping 24 hours onto the 360 degrees of a polar plot.
4 | For usage, please see the [examples.ipynb](examples.ipynb) Jupyter notebook
5 |
6 | 
7 |
8 | ## Installation
9 | To install this package run:
10 | `pip install clock_plot`
11 |
12 | ## Available features
13 | Time features are automatically generated for your timeseries. These features include:
14 | | Feature | Type | Description | Example Values |
15 | | ---------- | ---- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
16 | | year | int | Calendar year | 2022 |
17 | | month | str | Calendar month | "January" |
18 | | year_month | int | Calendar year and month in the format YYYYMM | 202201 |
19 | | day | int | Day of calendar year | 25 |
20 | | date | str | Expressed in the format YYYY-MM-DD | "2022-01-25" |
21 | | week | int | ISO week of the calendar year | 5 |
22 | | dayofweek | str | Short version of day of week | "Tue" |
23 | | weekend | str | Either "weekday" or "weekend", where "weekend" is Saturday and Sunday | "weekend" (Sat/Sun)
"weekday" (Mon-Fri) |
24 | | hour | int | Hour of the day in 24 clock | 14 |
25 | | minute | int | Minute of the hour | 42 |
26 | | degrees | int | Angle around 24 hour clock-face measured in degrees | 341 |
27 | | season | str | Season of the year defined based on month, with Winter being Dec-Feb | "Winter" (Dec-Feb)
"Spring" (Mar-May)
"Summer" (Jun-Aug)
"Autumn" (Sep-Nov) |
28 |
29 | These can be used to filter your data and format your plot.
30 |
31 | For example you could filter for a particular year, plot seasons with different colors and weekday vs weekend days with different line dashes.
32 | Examples of this are given in examples.ipynb
33 |
34 | ## When should you use these plots?
35 | Radar/polar plots (of which clock plots are a special case) are [much maligned by visualisation experts](https://www.data-to-viz.com/caveat/spider.html), and for good reason. Whilst some of the common limitations are overcome with clock plots, two key ones remain:
36 | 1. It is harder to read quantitative values than on a linear axis
37 | 2. Areas scale quadratically (with the square of the value) rather than linearly, which can lead to overestimation of differences
38 |
39 | Clock plots are therefore most suited for cases where understanding absolute values is less important and one or more of the following is true:
40 | - behaviour around midnight is particularly important
41 | - there are a 2-3 daily peaks and understanding at a glance when those are occurring is more important than their exact magnitude
42 | - you want a distinctive, eye-catching graphic to engage people with your work
43 |
44 | Note that they are particularly poorly suited to:
45 | - timeseries with negative values (the radial axis becomes very unintuitive)
46 | - timeseries with little within day variation (you just get circles!)
47 |
48 | If you're not sure which is best for a particular use case, you can quickly toggle between a clock plot and a linear plot by adding `mode="line"` to your clock_plot call.
--------------------------------------------------------------------------------
/clock_plot/__init__.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | CLOCK_PLOT_DIR = Path(__file__).parent.resolve()
4 |
--------------------------------------------------------------------------------
/clock_plot/clock.py:
--------------------------------------------------------------------------------
1 | import warnings
2 | from typing import Union
3 | import pandas as pd
4 | import numpy as np
5 | import plotly.express as px
6 | import plotly.graph_objects as go
7 | import scipy
8 |
9 |
10 | def create_datetime_vars(data_in: pd.DataFrame, datetime_col: str, bins_per_day: int = 24) -> pd.DataFrame:
11 | """Create all the datetime-related columns that might be used for analysis/plotting
12 | columns and their possible values are:
13 | year (int): Calendar year e.g. 2022
14 | month (str): Calendar month e.g. "January"
15 | year_month (int): Calendar year and month in the format YYYYMM e.g. 202201
16 | day (int): Day of calendar year e.g. 25
17 | date (str): Expressed in the format YYYY-MM-DD e.g. 2022-01-25
18 | week (int): Week of the calendar year e.g. 5
19 | dayofweek (str): Short version of day of week e.g. Tue
20 | weekend (str): Either "weekday" or "weekend" where weekends are where dayofweek is either "Sat" or "Sun"
21 | hour (int): Hour of the day in 24 clock e.g. 14
22 | minute (int): Minute of the hour e.g. 42
23 | degrees (int): Angle around 24 hour clock-face measured in degrees, calculated using hours and minutes
24 | season (str): Season of the year defined as:
25 | "Winter" where month is either "December", "January" or "February"
26 | "Spring" where month is either "March", "April" or "May"
27 | "Summer" where month is either "June", "July" or "August"
28 | "Autumn" where month is either "September", "October" or "November"
29 |
30 | Args:
31 | data (pandas.DataFrame): The DataFrame involved
32 | datetime_col (str): The column containing the datetime
33 | bins_per_day (int): The number of bins into which data will be aggregated (over a day).
34 | This is useful when you have unequally spaced datetimes datatimes. (Defaults to 24)
35 |
36 | Returns:
37 | pandas.DataFrame: Copy of the input DataFrame with datetime-related columns added
38 | """
39 | data = data_in.copy()
40 | data[datetime_col] = pd.to_datetime(data[datetime_col])
41 |
42 | data["year"] = data[datetime_col].dt.year
43 | data["month"] = data[datetime_col].dt.strftime("%B")
44 | data["year_month"] = data[datetime_col].dt.strftime("%Y%m").astype(int)
45 | data["day"] = data[datetime_col].dt.day
46 | data["date"] = data[datetime_col].dt.strftime("%Y-%m-%d")
47 | data["week"] = data[datetime_col].dt.isocalendar().week
48 | data["dayofweek"] = data[datetime_col].dt.strftime("%a")
49 | data["weekend"] = "Weekday"
50 | data.loc[(data["dayofweek"] == "Sat") | (data["dayofweek"] == "Sun"), "weekend"] = "Weekend"
51 | data["hour"] = data[datetime_col].dt.hour
52 | data["minute"] = data[datetime_col].dt.minute
53 | data["degrees"] = 360 * data["hour"] / 24
54 | # Temporarily keep this methodology, in the long-term this needs fixing to use the binning method for all
55 | # circumstances
56 | if bins_per_day != 24:
57 | data["degrees"] = data["degrees"] + (360 * data["minute"] / (60 * 24))
58 | data["degree_bins"] = pd.cut(data["degrees"], bins=bins_per_day)
59 | data["degrees"] = data["degree_bins"].map(lambda x: x.mid).astype(int)
60 |
61 | data["season"] = data["month"].map(
62 | {
63 | "December": "Winter",
64 | "January": "Winter",
65 | "February": "Winter",
66 | "March": "Spring",
67 | "April": "Spring",
68 | "May": "Spring",
69 | "June": "Summer",
70 | "July": "Summer",
71 | "August": "Summer",
72 | "September": "Autumn",
73 | "October": "Autumn",
74 | "November": "Autumn",
75 | }
76 | )
77 |
78 | return data
79 |
80 |
81 | def default_category_orders() -> dict:
82 | """Returns the default dictionary of category orders"""
83 | day_order = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
84 | weekend_order = ["Weekday", "Weekend"]
85 | season_order = ["Spring", "Summer", "Autumn", "Winter"]
86 | month_order = [
87 | "January",
88 | "February",
89 | "March",
90 | "April",
91 | "May",
92 | "June",
93 | "July",
94 | "August",
95 | "September",
96 | "October",
97 | "November",
98 | "December",
99 | ]
100 |
101 | category_orders = {
102 | "dayofweek": day_order,
103 | "weekend": weekend_order,
104 | "season": season_order,
105 | "month": month_order,
106 | }
107 |
108 | return category_orders
109 |
110 |
111 | def create_title(title_start: str, filters: dict, line_group: str):
112 | """Automatically generates chart titles for the given filters and line groupings
113 |
114 | Args:
115 | title_start (str): The start of the title
116 | filters (dict): The filters applied to the data
117 | line_group (str): The lowest level of grouping applied to the data
118 |
119 | Returns:
120 | str: The title string
121 | """
122 | # Try to tidy up the list of filter values ready for printing in the chart title. This will work
123 | # for straightforward cases (single values, non-nested lists) but will fail for nested lists, in
124 | # which case just give up and don't bother to include it in the title
125 | if filters:
126 | try:
127 | tidied_filter_vals = ""
128 | keys = list(filters.keys())
129 | values = []
130 | for key in keys:
131 | values.append(filters[key])
132 |
133 | tidied_filter_vals = tidied_filter_vals + " & ".join([str(x) for x in values if type(x) is not list])
134 | if any(isinstance(val, list) for val in values) & any(not isinstance(val, list) for val in values):
135 | tidied_filter_vals = tidied_filter_vals + " & "
136 | tidied_filter_vals = tidied_filter_vals + " & ".join(
137 | [str(y) for x in values if type(x) is list for y in x]
138 | )
139 | except Exception:
140 | tidied_filter_vals = ""
141 |
142 | title = "by Hour of Day"
143 | if title_start != "":
144 | title = f"{title_start} {title}"
145 | if filters:
146 | title = title + f" for {tidied_filter_vals}"
147 | if line_group is not None:
148 | title = title + f"
(each line represents a single {line_group})"
149 |
150 | return title
151 |
152 |
153 | def filter_and_group_data(
154 | data: pd.DataFrame,
155 | datetime_col: str,
156 | value_col: str,
157 | filters: dict = {},
158 | aggregate: dict = {None: "mean"},
159 | line_group: str = None,
160 | color: str = None,
161 | line_dash: str = None,
162 | bins_per_day: int = None,
163 | ) -> Union[pd.DataFrame, pd.DataFrame]:
164 | """Filter and then group the data based on given parameters
165 |
166 | Args:
167 | data (pd.DataFrame): The data to filter and group
168 | datetime_col (str): The datetime column name
169 | value_col (str): The name of the column containing the values to be grouped
170 | filters (dict, optional): Dictionary fiters, key-value pairs are column names and values to keep respectively.
171 | Defaults to {}.
172 | aggregate (_type_, optional): Dictionary containing a single key-value pair used to specify an additional
173 | level of aggregation line.
174 | Key gives column name to aggregate and value gives the aggregation method. Defaults to {None: "mean"}.
175 | line_group (str, optional): Name of column to use a the lowest level of grouping when plotting.
176 | Defaults to None.
177 | color (str, optional): Name of column to use to group by color. Defaults to None.
178 | line_dash (str, optional): Name of column to use to group by line dash. Defaults to None.
179 | bins_per_day (int, optional): Number of bins to group data into for each hour. Defaults to None.
180 |
181 | Raises:
182 | Exception: When an expected column is not present in the given DataFrame
183 | Exception: When the given filters leave 0 rows of data remaining
184 |
185 | Returns:
186 | Union[pd.DataFrame, pd.DataFrame]: The filtered and grouped data
187 | """
188 | agg_col = list(aggregate.keys())[0]
189 | agg_fn = list(aggregate.values())[0]
190 | relevant_cols = [col for col in [line_group, color, line_dash, "degrees", agg_col] + list(filters.keys()) if col]
191 |
192 | # If columns have been specified that don't exist (or bins_per_day is manually specified) then
193 | # generate the datatime vars to see if that helps
194 | if not set(relevant_cols).issubset(data.columns) or bins_per_day or len(relevant_cols) == 0:
195 | data = create_datetime_vars(data, datetime_col, bins_per_day)
196 | # If there are still missing columns raise an Exception
197 | if not set(relevant_cols).issubset(data.columns):
198 | missing_cols = [col for col in relevant_cols if col not in data.columns]
199 | raise KeyError(f"The following columns are missing from the supplied dataset: {missing_cols}")
200 |
201 | filtered_data = data
202 | if len(filters) > 0:
203 | # Apply all the specified filters
204 | for col, val in filters.items():
205 | # Note that the check that col is in filtered_data.columns has already been done above
206 | if col is not None:
207 | if type(val) is list:
208 | filtered_data = filtered_data[filtered_data[col].isin(val)]
209 | else:
210 | filtered_data = filtered_data[filtered_data[col] == val]
211 |
212 | if len(filtered_data) == 0:
213 | raise Exception("Filtering data leaves 0 rows remaining. Check the filters that have been specified")
214 |
215 | # Group by the required columns (using the list comprehension to remove columns that are None)
216 | grouped_data = (
217 | filtered_data.groupby([col for col in [line_group, color, line_dash, "degrees"] if col])[value_col]
218 | .agg(agg_fn)
219 | .reset_index()
220 | )
221 |
222 | if (grouped_data[value_col] < 0).any():
223 | warnings.warn(
224 | "Data contains negative values. A plot will be produced but they are often difficult to" " interpret"
225 | )
226 |
227 | return filtered_data, grouped_data
228 |
229 |
230 | def spline_interp(grouped_data: pd.DataFrame, value_col: str, groups: list, grouped_columns: list) -> pd.DataFrame:
231 | """Perform spline interpolation on the given data
232 |
233 | Args:
234 | grouped_data (pd.DataFrame): The data to interpolate
235 | value_col (str): The column name holding the values to be interpolated
236 | groups (list): The groups of values on which the data will be separated for interpolartion
237 | grouped_columns (list): The column names that were used for grouping the data
238 |
239 | Returns:
240 | pd.DataFrame: The interpolated data
241 | """
242 | interp_data = pd.DataFrame()
243 | for values in groups:
244 | filt = (grouped_data[grouped_columns] == values).all(axis=1)
245 | df = grouped_data[filt].copy()
246 | # It only makes sense to interpolate if we have enough data, here we choose 8 points (i.e. 3 hour intervals)
247 | if len(df) >= 8:
248 | # Want to put first values at end and last values at start to use for interpolation
249 | # We use 3 values as we are doing a cublic spline. This is the minimum needed for good interpolation
250 | # around 0-360 deg.
251 | start = range(0, 3)
252 | end = range(-3, 0)
253 | df = pd.concat([pd.DataFrame(df.iloc[end, :]), df, pd.DataFrame(df.iloc[start, :])], axis=0)
254 | df.iloc[end, df.columns == "degrees"] = df.iloc[end]["degrees"] + 360
255 | df.iloc[start, df.columns == "degrees"] = df.iloc[start]["degrees"] - 360
256 | # Reindex so we have rows for every degree value (plus the original rows)
257 | new_index = np.linspace(0, 360, 361)
258 | df = df.set_index("degrees")
259 | df = df.reindex(df.index.union(new_index))
260 | df[grouped_columns] = values
261 | df[value_col] = df[value_col].astype(float).interpolate(method="cubicspline")
262 | df.reset_index(inplace=True)
263 | df = df.loc[(df["degrees"] >= 0) & (df["degrees"] < 360)]
264 | interp_data = pd.concat([interp_data, df])
265 |
266 | return interp_data
267 |
268 |
269 | def plot_averages(
270 | fig: go.Figure,
271 | data: pd.DataFrame,
272 | value_col: str,
273 | aggregate: dict,
274 | color: str,
275 | line_shape: str = "spline",
276 | color_discrete_sequence: list = px.colors.qualitative.G10,
277 | category_orders: dict = default_category_orders(),
278 | mode: str = "polar",
279 | ):
280 | """Add traces for the aggregated data
281 |
282 | Args:
283 | fig (go.Figure): The existing figure to which the traces will be added
284 | data (pd.DataFrame): The data to aggregate
285 | value_col (str): Name of the column containing the value to aggregate
286 | aggregate (dict): Dictionary containing a single key-value pair.
287 | Key gives column name to aggregate and value gives the aggregation method.
288 | color (str): Name of column to use to group by color.
289 | line_shape (str, optional): Whether to smooth the lines, one of either 'linear' or 'spline'.
290 | Defaults to 'spline'
291 | color_discrete_sequence (list, optional): List of colors to use for the chart.
292 | Defaults to px.colors.qualitative.G10
293 | category_orders (dict, optional): Dictionary where the key-value pairs are column names and a list of values
294 | in the desired order. This is used to set relative ordering of categories
295 | and is important for fixing line colors and legend orders.
296 | Defaults to default_category_orders()
297 | mode (str, optional): Whether to plot "polar" or "flat" (cartesian). Defaults to "polar".
298 |
299 | Returns:
300 | go.Figure: The figure with added traces for aggregated data
301 | """
302 | agg_col = list(aggregate.keys())[0]
303 | agg_fn = list(aggregate.values())[0]
304 | agg_data = data.groupby([agg_col, "degrees"])[value_col].agg(agg_fn).reset_index()
305 | # Want to change the data labels to reflect that it is aggregated ( This appears in the legend )
306 | agg_data[agg_col] = agg_data[agg_col].map(("{} (" + agg_fn + ")").format)
307 | # Also need to add these categories to the category_orders dict ( So the color and legend order are consistent)
308 | if agg_col in category_orders:
309 | category_orders[agg_col] = category_orders[agg_col] + [
310 | f"{value} ({agg_fn})" for value in category_orders[agg_col]
311 | ]
312 |
313 | if mode == "polar":
314 | agg_fig = px.line_polar(
315 | agg_data,
316 | theta="degrees",
317 | r=value_col,
318 | color=color,
319 | line_close=True,
320 | line_shape=line_shape,
321 | category_orders=category_orders,
322 | color_discrete_sequence=color_discrete_sequence,
323 | )
324 | else:
325 | agg_fig = px.line(
326 | agg_data,
327 | x="degrees",
328 | y=value_col,
329 | color=color,
330 | line_shape=line_shape,
331 | category_orders=category_orders,
332 | color_discrete_sequence=color_discrete_sequence,
333 | )
334 | agg_fig.update_traces(line_width=6)
335 | fig.add_traces(list(agg_fig.select_traces()))
336 |
337 | return fig
338 |
339 |
340 | def clock_plot(
341 | data: pd.DataFrame,
342 | datetime_col: str,
343 | value_col: str,
344 | filters: dict = {},
345 | aggregate: dict = {None: "mean"},
346 | line_group: str = None,
347 | color: str = None,
348 | line_dash: str = None,
349 | line_shape: str = "spline",
350 | title_start: str = "",
351 | title: str = None,
352 | bins_per_day: int = 24,
353 | show: bool = True,
354 | color_discrete_sequence: list = None,
355 | category_orders: dict = {},
356 | text_noon: bool = True,
357 | mode: str = "polar",
358 | **kwargs,
359 | ):
360 | """Plot a polar chart showing value by hour of day
361 |
362 | Args:
363 | data (pandas.DataFrame): DataFrame containing the values to plot as a timeseries
364 | datetime_col (str): Name of the column containing the datetime
365 | value_col (str): Name of the column containing the value to plot
366 | filters (dict, optional): Dictionary filters, key-value pairs are column names and values to keep respectively.
367 | Defaults to {}.
368 | aggregate (_type_, optional): Dictionary containing a single key-value pair.
369 | Key gives column name to aggregate and value gives the aggregation method.
370 | Defaults to {None: "mean"}.
371 | line_group (str, optional): Name of column to use a the lowest level of grouping when plotting.
372 | Defaults to None.
373 | color (str, optional): Name of column to use to group by color. Defaults to None.
374 | line_dash (str, optional): Name of column to use to group by line dash. Defaults to None.
375 | line_shape (str, optional): Whether to smooth the lines, one of either 'linear' or 'spline'.
376 | Defaults to "spline".
377 | title_start (str, optional): A string to prefix to the automated chart title. Defaults to "".
378 | title (str, optional): Overrides the automated chart title. Defaults to None.
379 | bins_per_day (int, optional): Number of bins to group data into over a day. This is useful when using
380 | irregularly spaced datetimes. Defaults to 24.
381 | show (bool, optional): Whether to display the chart after it is generated. Defaults to True.
382 | color_discrete_sequence (list, optional): List of colors to use for the chart. Defaults to None.
383 | category_orders (dict, optional): Dictionary where the key-value pairs are column names and a list of values
384 | in the desired order. This is used to set relative ordering of categories
385 | and is important for fixing line colors and legend orders. Defaults to {}.
386 | text_noon (bool, optional): Whether to replace hours 0 and 12 with "Midnight" and "Noon" respectively.
387 | Defaults to True.
388 | mode (str, optional): Whether to plot "polar" or "line" (cartesian). Defaults to "polar".
389 | **kwargs: Accepts and passes on any arguments accepted by plotly express line_polar()
390 |
391 | Returns:
392 | (plotly.graph_objects.Figure): The generated plotly figure object containing the polar charts.
393 | """
394 | filtered_data, grouped_data = filter_and_group_data(
395 | data,
396 | datetime_col,
397 | value_col,
398 | filters,
399 | aggregate,
400 | line_group,
401 | color,
402 | line_dash,
403 | bins_per_day,
404 | )
405 |
406 | # The order in which categories are plotted determins their colour which we want to be consistent and correct
407 | def_category_orders = default_category_orders()
408 | def_category_orders.update(category_orders)
409 | category_orders = def_category_orders
410 |
411 | # Specify the color sequence to use (we use a custom one for season so that the colours are intuitive)
412 | # TODO: Allow continuous colour sequences to be passed
413 | if color_discrete_sequence is None:
414 | if color is not None and "season" in color:
415 | color_discrete_sequence = ["green", "red", "orange", "blue"]
416 | elif list(aggregate.keys())[0] is not None and "season" in list(aggregate.keys()):
417 | color_discrete_sequence = ["green", "red", "orange", "blue"]
418 | else:
419 | color_discrete_sequence = px.colors.qualitative.G10
420 |
421 | if title is None:
422 | title = create_title(title_start, filters, line_group)
423 |
424 | # Plotly struggles to do splines for more than 20 or so curves, so revert to line_shape = linear and do
425 | # interpolation before hand
426 | orig_line_shape = line_shape
427 | columns = [col for col in [color, line_group, line_dash] if col]
428 | if len(columns) > 0:
429 | groups = list(filtered_data.groupby(columns).groups.keys())
430 | else:
431 | groups = []
432 | grp_length = len(groups)
433 | if grp_length > 20 and line_shape == "spline":
434 | line_shape = "linear"
435 | grouped_data = spline_interp(grouped_data, value_col, groups, columns)
436 |
437 | tick_text = list(range(0, 24))
438 | if text_noon:
439 | tick_text[0] = "Midnight"
440 | tick_text[12] = "Noon"
441 |
442 | if mode == "polar":
443 | # Default to 800x800 unless size is manually specified
444 | if "width" not in kwargs.keys() and "height" not in kwargs.keys():
445 | kwargs["width"] = 800
446 | kwargs["height"] = 800
447 |
448 | fig = px.line_polar(
449 | grouped_data,
450 | theta="degrees",
451 | r=value_col,
452 | line_group=line_group,
453 | color=color,
454 | line_dash=line_dash,
455 | line_close=True,
456 | line_shape=line_shape,
457 | category_orders=category_orders,
458 | title=title,
459 | color_discrete_sequence=color_discrete_sequence,
460 | **kwargs,
461 | )
462 | # Calculate the tickmarks for 24 hour clock
463 | fig.update_polars(
464 | angularaxis_tickvals=np.array(range(0, 360, int(360 / 24))),
465 | angularaxis_ticktext=tick_text,
466 | )
467 | else:
468 | # Default to 800x600 unless size is manually specified
469 | if "width" not in kwargs.keys() and "height" not in kwargs.keys():
470 | kwargs["width"] = 800
471 | kwargs["height"] = 600
472 | fig = px.line(
473 | grouped_data,
474 | x="degrees",
475 | y=value_col,
476 | line_group=line_group,
477 | color=color,
478 | line_dash=line_dash,
479 | line_shape=line_shape,
480 | category_orders=category_orders,
481 | title=title,
482 | color_discrete_sequence=color_discrete_sequence,
483 | **kwargs,
484 | )
485 | # Calculate the tickmarks for 24 hour clock
486 | fig.update_layout(
487 | xaxis_tickvals=np.array(range(0, 360, int(360 / 24))),
488 | xaxis_ticktext=tick_text,
489 | )
490 |
491 | # If there are a lot of lines, make them smaller and slightly transparent
492 | if grp_length > 12:
493 | fig.update_traces(line_width=0.5)
494 | fig.update_traces(opacity=0.7)
495 | elif grp_length <= 8:
496 | fig.update_traces(line_width=3)
497 |
498 | # Plot the averages (if required)
499 | if list(aggregate.keys())[0] is not None:
500 | fig = plot_averages(
501 | fig,
502 | filtered_data,
503 | value_col,
504 | aggregate,
505 | color,
506 | orig_line_shape,
507 | color_discrete_sequence,
508 | category_orders,
509 | mode,
510 | )
511 |
512 | if show:
513 | fig.show()
514 | return fig
515 |
--------------------------------------------------------------------------------
/data/seasonal_gas_usage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ES-Catapult/clock_plot/7284e0bfe908ae0484557486496b8a399a349d53/data/seasonal_gas_usage.png
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=42"]
3 | build-backend = "setuptools.build_meta"
--------------------------------------------------------------------------------
/release_notes.txt:
--------------------------------------------------------------------------------
1 | 0.2.1 - Add the ability to easily toggle back to an ordinary line plot
2 |
3 | 0.2 - Default to showing 0 and 12 as "Midnight" and "Noon". Can revert to numbers with text_noon=False
4 |
5 | 0.1.4 - Fix bug where plotting using just datetime_col and value_col didn't work.
6 |
7 | 0.1.3 - Initial release
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r", encoding="utf-8") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="clock_plot",
8 | version="0.2.1",
9 | author="Samuel Young",
10 | author_email="samuel.young.work@gmail.com",
11 | description="This package provides a simple way to visualize patterns in timeseries data mapping 24 hours onto a polar plot",
12 | long_description=long_description,
13 | long_description_content_type="text/markdown",
14 | url="https://github.com/esc-data-science/clock_plot",
15 | project_urls={
16 | "Bug Tracker": "https://github.com/esc-data-science/clock_plot/issues",
17 | },
18 | classifiers=[
19 | "Programming Language :: Python :: 3",
20 | "License :: OSI Approved :: MIT License",
21 | "Operating System :: OS Independent",
22 | ],
23 | packages=setuptools.find_packages(),
24 | python_requires=">=3.7",
25 | install_requires=["numpy>=1.20.3", "pandas>=1.3.4", "plotly>=4.0.0", "scipy>=1.7.1"],
26 | )
27 |
--------------------------------------------------------------------------------