caf.viz 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- caf.viz-0.1.1/LICENSE +22 -0
- caf.viz-0.1.1/PKG-INFO +66 -0
- caf.viz-0.1.1/README.md +11 -0
- caf.viz-0.1.1/pyproject.toml +133 -0
- caf.viz-0.1.1/setup.cfg +9 -0
- caf.viz-0.1.1/src/caf/viz/__init__.py +5 -0
- caf.viz-0.1.1/src/caf/viz/_version.py +14 -0
- caf.viz-0.1.1/src/caf/viz/py.typed +0 -0
- caf.viz-0.1.1/src/caf/viz/subplots.py +157 -0
- caf.viz-0.1.1/src/caf/viz/utils.py +21 -0
- caf.viz-0.1.1/src/caf/viz/xy_plot.py +368 -0
- caf.viz-0.1.1/src/caf.viz.egg-info/PKG-INFO +66 -0
- caf.viz-0.1.1/src/caf.viz.egg-info/SOURCES.txt +17 -0
- caf.viz-0.1.1/src/caf.viz.egg-info/dependency_links.txt +1 -0
- caf.viz-0.1.1/src/caf.viz.egg-info/requires.txt +18 -0
- caf.viz-0.1.1/src/caf.viz.egg-info/top_level.txt +1 -0
- caf.viz-0.1.1/tests/test_example.py +23 -0
- caf.viz-0.1.1/tests/test_xy_plot.py +86 -0
caf.viz-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright © Transport for the North (“TfN”) (2024).
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Use of this software and associated documentation files (“the Software”) by you indicates your acceptance of the terms and conditions below (“the Licence”).
|
|
5
|
+
We make available the Software to you on the basis of this Licence. We do not sell the Software to you. We remain the owners of the Software at all times.
|
|
6
|
+
|
|
7
|
+
We grant you a non-exclusive, worldwide, royalty-free, perpetual licence:
|
|
8
|
+
(a) to use the Software;
|
|
9
|
+
(b) to develop, modify and maintain the Software;
|
|
10
|
+
(c) copy, publish, distribute and transmit the Software;
|
|
11
|
+
(d) adapt the Software.
|
|
12
|
+
|
|
13
|
+
Where you do any of the above you must acknowledge the source of the Software in your product or application by including or linking the following attribution statement in a prominent and noticeable location in or in the context of your product or application:
|
|
14
|
+
“Outputs derived from CAF.Viz (the Common Analytical Framework Visualisation Suite), developed by Transport for the North [https://github.com/Transport-for-the-North/caf.viz]”
|
|
15
|
+
|
|
16
|
+
Any adaptations applied to the Software before application should be briefly described alongside the above citation. We encourage you to submit to us any adaptations to the Software and where you choose to do so your submissions will be considered for integration into the Software.
|
|
17
|
+
|
|
18
|
+
The above are important conditions of this Licence and if you fail to comply with them the rights granted to you under this Licence will end automatically.
|
|
19
|
+
This Licence does not grant you any right to use the Software in a way that suggests any official status or that we endorse you or your use of the Software.
|
|
20
|
+
The Software is licenced 'as is' and we exclude all representations, warranties, obligations and liabilities in relation to the Software to the maximum extent permitted by law.
|
|
21
|
+
We are not liable for any errors or omissions in the Software and shall not be liable for any loss, injury or damage of any kind caused by its use. We do not guarantee the continued supply of the Software.
|
|
22
|
+
This Licence is governed by the laws of the jurisdiction in which we have our principal place of business.
|
caf.viz-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: caf.viz
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python-based visualisation tools for transport data
|
|
5
|
+
Author: Transport for the North
|
|
6
|
+
License: Copyright © Transport for the North (“TfN”) (2024).
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Use of this software and associated documentation files (“the Software”) by you indicates your acceptance of the terms and conditions below (“the Licence”).
|
|
10
|
+
We make available the Software to you on the basis of this Licence. We do not sell the Software to you. We remain the owners of the Software at all times.
|
|
11
|
+
|
|
12
|
+
We grant you a non-exclusive, worldwide, royalty-free, perpetual licence:
|
|
13
|
+
(a) to use the Software;
|
|
14
|
+
(b) to develop, modify and maintain the Software;
|
|
15
|
+
(c) copy, publish, distribute and transmit the Software;
|
|
16
|
+
(d) adapt the Software.
|
|
17
|
+
|
|
18
|
+
Where you do any of the above you must acknowledge the source of the Software in your product or application by including or linking the following attribution statement in a prominent and noticeable location in or in the context of your product or application:
|
|
19
|
+
“Outputs derived from CAF.Viz (the Common Analytical Framework Visualisation Suite), developed by Transport for the North [https://github.com/Transport-for-the-North/caf.viz]”
|
|
20
|
+
|
|
21
|
+
Any adaptations applied to the Software before application should be briefly described alongside the above citation. We encourage you to submit to us any adaptations to the Software and where you choose to do so your submissions will be considered for integration into the Software.
|
|
22
|
+
|
|
23
|
+
The above are important conditions of this Licence and if you fail to comply with them the rights granted to you under this Licence will end automatically.
|
|
24
|
+
This Licence does not grant you any right to use the Software in a way that suggests any official status or that we endorse you or your use of the Software.
|
|
25
|
+
The Software is licenced 'as is' and we exclude all representations, warranties, obligations and liabilities in relation to the Software to the maximum extent permitted by law.
|
|
26
|
+
We are not liable for any errors or omissions in the Software and shall not be liable for any loss, injury or damage of any kind caused by its use. We do not guarantee the continued supply of the Software.
|
|
27
|
+
This Licence is governed by the laws of the jurisdiction in which we have our principal place of business.
|
|
28
|
+
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/Transport-for-the-North/caf.viz/issues
|
|
30
|
+
Project-URL: Homepage, https://github.com/Transport-for-the-North/caf.viz
|
|
31
|
+
Project-URL: Source, https://github.com/Transport-for-the-North/caf.viz
|
|
32
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
36
|
+
Requires-Python: >=3.10
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
License-File: LICENSE
|
|
39
|
+
Requires-Dist: numpy>=1.19.0
|
|
40
|
+
Requires-Dist: pandas>=1.4.0
|
|
41
|
+
Requires-Dist: matplotlib>=3.8.2
|
|
42
|
+
Requires-Dist: scipy>=1.11.4
|
|
43
|
+
Requires-Dist: pydantic>=2.0
|
|
44
|
+
Provides-Extra: lint
|
|
45
|
+
Requires-Dist: black>=22.6.0; extra == "lint"
|
|
46
|
+
Requires-Dist: isort>=5.12.0; extra == "lint"
|
|
47
|
+
Requires-Dist: mypy>=1.0.0; extra == "lint"
|
|
48
|
+
Requires-Dist: mypy_extensions>=1.0.0; extra == "lint"
|
|
49
|
+
Requires-Dist: pydocstyle[toml]>=6.1.1; extra == "lint"
|
|
50
|
+
Requires-Dist: pylint>=2.14.5; extra == "lint"
|
|
51
|
+
Provides-Extra: test
|
|
52
|
+
Requires-Dist: pytest>=7.4.0; extra == "test"
|
|
53
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "test"
|
|
54
|
+
Requires-Dist: pytest-xdist>=3.3.1; extra == "test"
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
<h1 align="center">CAF Viz</h1>
|
|
59
|
+
|
|
60
|
+
<p align="center">
|
|
61
|
+
<a href="https://github.com/PyCQA/pylint"><img alt="linting: pylint" src="https://img.shields.io/badge/linting-pylint-yellowgreen"></a>
|
|
62
|
+
<a href="https://google.github.io/styleguide/pyguide.html"><img alt="code format: Google Style Guide" src="https://img.shields.io/badge/code%20style-Google%20Style%20Guide-blue"></a>
|
|
63
|
+
<a href="https://github.com/psf/black"><img alt="code style: black" src="https://img.shields.io/badge/code%20format-black-000000.svg"></a>
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
Python-based visualisation tools for transport data.
|
caf.viz-0.1.1/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
<h1 align="center">CAF Viz</h1>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://github.com/PyCQA/pylint"><img alt="linting: pylint" src="https://img.shields.io/badge/linting-pylint-yellowgreen"></a>
|
|
7
|
+
<a href="https://google.github.io/styleguide/pyguide.html"><img alt="code format: Google Style Guide" src="https://img.shields.io/badge/code%20style-Google%20Style%20Guide-blue"></a>
|
|
8
|
+
<a href="https://github.com/psf/black"><img alt="code style: black" src="https://img.shields.io/badge/code%20format-black-000000.svg"></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
Python-based visualisation tools for transport data.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64.0.0", "versioningit"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "caf.viz"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python-based visualisation tools for transport data"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Transport for the North" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
15
|
+
"Programming Language :: Python :: 3.10",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"numpy>=1.19.0",
|
|
21
|
+
"pandas>=1.4.0",
|
|
22
|
+
"matplotlib>=3.8.2",
|
|
23
|
+
"scipy>=1.11.4",
|
|
24
|
+
"pydantic>=2.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
lint = [
|
|
29
|
+
"black>=22.6.0",
|
|
30
|
+
"isort>=5.12.0",
|
|
31
|
+
"mypy>=1.0.0",
|
|
32
|
+
"mypy_extensions>=1.0.0",
|
|
33
|
+
"pydocstyle[toml]>=6.1.1",
|
|
34
|
+
"pylint>=2.14.5",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
test = [
|
|
38
|
+
"pytest>=7.4.0",
|
|
39
|
+
"pytest-cov>=4.1.0",
|
|
40
|
+
"pytest-xdist>=3.3.1",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
"Bug Tracker" = "https://github.com/Transport-for-the-North/caf.viz/issues"
|
|
45
|
+
Homepage = "https://github.com/Transport-for-the-North/caf.viz"
|
|
46
|
+
Source = "https://github.com/Transport-for-the-North/caf.viz"
|
|
47
|
+
|
|
48
|
+
# Packaging options
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["src"]
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.package-data]
|
|
53
|
+
"caf.viz" = ["py.typed"]
|
|
54
|
+
|
|
55
|
+
# Linters
|
|
56
|
+
[tool.black]
|
|
57
|
+
line-length = 95
|
|
58
|
+
target-version = ['py310']
|
|
59
|
+
|
|
60
|
+
[tool.pylint.messages_control]
|
|
61
|
+
disable = [
|
|
62
|
+
"line-too-long", # Compatibility with black
|
|
63
|
+
"too-many-lines",
|
|
64
|
+
"use-dict-literal",
|
|
65
|
+
"use-list-literal",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[tool.isort]
|
|
69
|
+
profile = "black"
|
|
70
|
+
src_paths = ["src", "tests"]
|
|
71
|
+
|
|
72
|
+
skip = ["__init__.py", "_version.py"]
|
|
73
|
+
skip_glob = [".*"]
|
|
74
|
+
|
|
75
|
+
# Section commenting
|
|
76
|
+
import_heading_stdlib = "Built-Ins"
|
|
77
|
+
import_heading_thirdparty = "Third Party"
|
|
78
|
+
import_heading_firstparty = "Local Imports"
|
|
79
|
+
|
|
80
|
+
# Pylint settings
|
|
81
|
+
[tool.pylint.basic]
|
|
82
|
+
good-names = ["df", "fn", "ca", "tp", "to", "x", "x1", "x2", "i", "f", "q"]
|
|
83
|
+
extension-pkg-allow-list = ["pydantic"]
|
|
84
|
+
|
|
85
|
+
[tool.pylint.main]
|
|
86
|
+
ignore = ["__init__.py", "_version.py", "versioneer.py"]
|
|
87
|
+
|
|
88
|
+
[tool.pylint.design]
|
|
89
|
+
max-args = 10
|
|
90
|
+
max-attributes = 15
|
|
91
|
+
max-statements = 100
|
|
92
|
+
|
|
93
|
+
[tool.mypy]
|
|
94
|
+
packages = ["caf.viz"]
|
|
95
|
+
ignore_missing_imports = true
|
|
96
|
+
allow_redefinition = true
|
|
97
|
+
|
|
98
|
+
[tool.pydocstyle]
|
|
99
|
+
convention = "numpy"
|
|
100
|
+
match = '(?!_test)(?!test_)(?!__init__).*\.py'
|
|
101
|
+
add-ignore = [
|
|
102
|
+
"D202", # Temporary compatibility with black
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
# Tests
|
|
106
|
+
[tool.pytest.ini_options]
|
|
107
|
+
pythonpath = ["src"]
|
|
108
|
+
addopts = '--cov=caf --cov-report=xml --doctest-modules -n auto'
|
|
109
|
+
doctest_optionflags = "NORMALIZE_WHITESPACE"
|
|
110
|
+
markers = [
|
|
111
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
112
|
+
"integration: integration tests, these may require manual checks of the outputs",
|
|
113
|
+
"serial: marks tests to not run in parallel",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
[tool.coverage.report]
|
|
117
|
+
include_namespace_packages = true
|
|
118
|
+
|
|
119
|
+
# Versioning
|
|
120
|
+
[tool.versioningit.vcs]
|
|
121
|
+
method = "git"
|
|
122
|
+
match = ["v*"]
|
|
123
|
+
default-tag = "0.0.1"
|
|
124
|
+
|
|
125
|
+
[tool.versioningit.onbuild]
|
|
126
|
+
source-file = "src/caf/viz/_version.py"
|
|
127
|
+
build-file = "caf/viz/_version.py"
|
|
128
|
+
|
|
129
|
+
[tool.versioningit.format]
|
|
130
|
+
distance = "{base_version}+{distance}.{vcs}{rev}"
|
|
131
|
+
dirty = "{base_version}+{distance}.{vcs}{rev}.dirty"
|
|
132
|
+
|
|
133
|
+
distance-dirty = "{base_version}+{distance}.{vcs}{rev}.dirty"
|
caf.viz-0.1.1/setup.cfg
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Always get the current version in "editable" installs
|
|
2
|
+
# `pip install -e .` / `python setup.py develop`
|
|
3
|
+
def _get_version() -> str:
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from versioningit import get_version
|
|
6
|
+
|
|
7
|
+
return get_version(
|
|
8
|
+
project_dir=Path(__file__).parents[3],
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# The following _get_version() call will get replaced by versioningit with a
|
|
13
|
+
# static version string during a build
|
|
14
|
+
__version__ = "0.1.1"
|
|
File without changes
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Functionality for helping creating figures with subplots."""
|
|
3
|
+
|
|
4
|
+
##### IMPORTS #####
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
# Built-Ins
|
|
8
|
+
import itertools
|
|
9
|
+
import logging
|
|
10
|
+
import math
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any, Callable, Concatenate, TypeAlias
|
|
13
|
+
|
|
14
|
+
# Third Party
|
|
15
|
+
import numpy as np
|
|
16
|
+
from matplotlib import axes, figure
|
|
17
|
+
from matplotlib import pyplot as plt
|
|
18
|
+
|
|
19
|
+
##### CONSTANTS #####
|
|
20
|
+
|
|
21
|
+
LOG = logging.getLogger(__name__)
|
|
22
|
+
_FIGURE_WIDTH_FACTOR = 5
|
|
23
|
+
_FIGURE_HEIGHT_FACTOR = 4
|
|
24
|
+
|
|
25
|
+
# Python 3.10 raises an error when Concatenate ends with ellipsis,
|
|
26
|
+
# although this should be allowed according to the Python docs
|
|
27
|
+
if sys.version_info.major == 3 and sys.version_info.minor > 10:
|
|
28
|
+
PlotFunction: TypeAlias = Callable[Concatenate[figure.Figure, axes.Axes, ...], None]
|
|
29
|
+
|
|
30
|
+
##### FUNCTIONS & CLASSES #####
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def subplotter(
|
|
34
|
+
func: PlotFunction,
|
|
35
|
+
plot_args: list[dict[str, Any]],
|
|
36
|
+
nrows: int,
|
|
37
|
+
ncols: int,
|
|
38
|
+
**subplots_kwargs,
|
|
39
|
+
) -> figure.Figure:
|
|
40
|
+
"""Plot subplots in a grid, axes plots are created using `func`.
|
|
41
|
+
|
|
42
|
+
A roughly square grid of subplots will be created, with a figure
|
|
43
|
+
size calculated based on the number of subplots in the grid.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
func : (figure.Figure, axes.Axes, ...) -> None
|
|
48
|
+
Function to plot data onto the individual subplot axes,
|
|
49
|
+
this will be call once with a single axes for each set of
|
|
50
|
+
arguments in `plot_args`.
|
|
51
|
+
plot_args : list[dict[str, Any]]
|
|
52
|
+
List of arguments for plotting `func`, the length of
|
|
53
|
+
this defines the number of subplot axes generated.
|
|
54
|
+
nrows : int
|
|
55
|
+
Number of rows of subplots to create.
|
|
56
|
+
ncols : int
|
|
57
|
+
Number of columns of subplots to create.
|
|
58
|
+
**subplots_kwargs : Keyword arguments
|
|
59
|
+
Any other keyword arguments are passed to `plt.subplots`.
|
|
60
|
+
This cannot contain the parameter: 'squeeze'.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
figure.Figure
|
|
65
|
+
The plotted figure.
|
|
66
|
+
|
|
67
|
+
Raises
|
|
68
|
+
------
|
|
69
|
+
ValueError
|
|
70
|
+
- If length of `plot_args` is 0, or not enough Axes are created
|
|
71
|
+
to contain all the subplots i.e. ncols * nrows < len(plot_args).
|
|
72
|
+
- If one of 'squeeze' is given in `subplots_kwargs`.
|
|
73
|
+
"""
|
|
74
|
+
if len(plot_args) == 0:
|
|
75
|
+
raise ValueError("no arguments given for plotting")
|
|
76
|
+
|
|
77
|
+
n_subplots = len(plot_args)
|
|
78
|
+
if nrows * ncols < n_subplots:
|
|
79
|
+
raise ValueError(f"not enough Axes rows and columns to fit all {n_subplots} subplots")
|
|
80
|
+
|
|
81
|
+
if "squeeze" in subplots_kwargs:
|
|
82
|
+
raise ValueError("squeeze cannot be defined in `subplot_kwargs` when using subplotter")
|
|
83
|
+
|
|
84
|
+
subplots_kwargs = subplots_kwargs.copy()
|
|
85
|
+
figsize = subplots_kwargs.pop(
|
|
86
|
+
"figsize",
|
|
87
|
+
(math.ceil(_FIGURE_WIDTH_FACTOR * ncols), math.ceil(_FIGURE_HEIGHT_FACTOR * nrows)),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
axs: list[list[axes.Axes]]
|
|
91
|
+
fig, axs = plt.subplots(
|
|
92
|
+
nrows=nrows,
|
|
93
|
+
ncols=ncols,
|
|
94
|
+
squeeze=False,
|
|
95
|
+
figsize=figsize,
|
|
96
|
+
**subplots_kwargs,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
grid_iter = itertools.product(range(nrows), range(ncols))
|
|
100
|
+
for (i, j), args in itertools.zip_longest(grid_iter, plot_args):
|
|
101
|
+
if args is None:
|
|
102
|
+
# Turn off axes if no more data to plot
|
|
103
|
+
axs[i][j].set_axis_off()
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
func(fig, axs[i][j], **args)
|
|
107
|
+
|
|
108
|
+
return fig
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def grid_plot(
|
|
112
|
+
func: PlotFunction, plot_args: list[dict[str, Any]], **subplots_kwargs
|
|
113
|
+
) -> figure.Figure:
|
|
114
|
+
"""Plot subplots in a grid, axes plots are created using `func`.
|
|
115
|
+
|
|
116
|
+
A roughly square grid of subplots will be created, with a figure
|
|
117
|
+
size calculated based on the number of subplots in the grid.
|
|
118
|
+
|
|
119
|
+
Parameters
|
|
120
|
+
----------
|
|
121
|
+
func : (figure.Figure, axes.Axes, ...) -> None
|
|
122
|
+
Function to plot data onto the individual subplot axes,
|
|
123
|
+
this will be call once with a single axes for each set of
|
|
124
|
+
arguments in `plot_args`.
|
|
125
|
+
plot_args : list[dict[str, Any]]
|
|
126
|
+
List of arguments for plotting `func`, the length of
|
|
127
|
+
this defines the number of subplot axes generated.
|
|
128
|
+
**subplots_kwargs : Keyword arguments
|
|
129
|
+
Any other keyword arguments are passed to `plt.subplots`.
|
|
130
|
+
This cannot contain the parameters:
|
|
131
|
+
'squeeze', 'nrows' or 'ncols'.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
figure.Figure
|
|
136
|
+
The plotted figure.
|
|
137
|
+
|
|
138
|
+
Raises
|
|
139
|
+
------
|
|
140
|
+
ValueError
|
|
141
|
+
- If length of `plot_args` is 0.
|
|
142
|
+
- If one of 'squeeze', 'nrows' or 'ncols' is given in `subplots_kwargs`.
|
|
143
|
+
"""
|
|
144
|
+
if len(plot_args) == 0:
|
|
145
|
+
raise ValueError("no arguments given for plotting")
|
|
146
|
+
|
|
147
|
+
n_subplots = len(plot_args)
|
|
148
|
+
ncols = int(np.ceil(np.sqrt(n_subplots)))
|
|
149
|
+
nrows = int(np.ceil(n_subplots / ncols))
|
|
150
|
+
|
|
151
|
+
for param in ("squeeze", "nrows", "ncols"):
|
|
152
|
+
if param in subplots_kwargs:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"{param} cannot be defined in `subplot_kwargs` when using grid_plot"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return subplotter(func, plot_args, nrows=nrows, ncols=ncols, **subplots_kwargs)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Miscellaneous utility functions for visualisation."""
|
|
3
|
+
|
|
4
|
+
##### IMPORTS #####
|
|
5
|
+
|
|
6
|
+
# Built-Ins
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
##### CONSTANTS #####
|
|
11
|
+
|
|
12
|
+
LOG = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
##### CLASSES & FUNCTIONS #####
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def normalise_name(name: str) -> str:
|
|
19
|
+
"""Convert name to lowercase and replace spaces with underscore."""
|
|
20
|
+
name = name.lower().strip()
|
|
21
|
+
return re.sub(r"\s+", "_", name)
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Functionality for plotting of general X / Y data."""
|
|
3
|
+
|
|
4
|
+
##### IMPORTS #####
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
# Built-Ins
|
|
9
|
+
import enum
|
|
10
|
+
import logging
|
|
11
|
+
import warnings
|
|
12
|
+
from typing import Sequence
|
|
13
|
+
|
|
14
|
+
# Third Party
|
|
15
|
+
import matplotlib as mpl
|
|
16
|
+
import numpy as np
|
|
17
|
+
import pandas as pd
|
|
18
|
+
from matplotlib import axes, figure, ticker
|
|
19
|
+
from pydantic import dataclasses
|
|
20
|
+
from scipy import stats
|
|
21
|
+
|
|
22
|
+
# Local Imports
|
|
23
|
+
from caf.viz import subplots, utils
|
|
24
|
+
|
|
25
|
+
##### CONSTANTS #####
|
|
26
|
+
|
|
27
|
+
LOG = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
##### FUNCTIONS & CLASSES #####
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclasses.dataclass(config={"arbitrary_types_allowed": True})
|
|
34
|
+
class BasicData:
|
|
35
|
+
"""Basic data for XY plots."""
|
|
36
|
+
|
|
37
|
+
data: pd.DataFrame
|
|
38
|
+
x_column: str
|
|
39
|
+
y_column: str
|
|
40
|
+
x_label: str | None = None
|
|
41
|
+
y_label: str | None = None
|
|
42
|
+
title: str | None = None
|
|
43
|
+
auto_label: bool = True
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclasses.dataclass
|
|
47
|
+
class CmapData:
|
|
48
|
+
"""Colormap parameters."""
|
|
49
|
+
|
|
50
|
+
column: str
|
|
51
|
+
label: str | None = None
|
|
52
|
+
auto_label: bool = True
|
|
53
|
+
cmap: str = mpl.rcParams.get("image.cmap") # type: ignore
|
|
54
|
+
|
|
55
|
+
def get_label(self) -> str | None:
|
|
56
|
+
"""Return label for colorbar."""
|
|
57
|
+
if self.label is not None:
|
|
58
|
+
return self.label
|
|
59
|
+
if self.auto_label:
|
|
60
|
+
return self.column
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class XYPlotType(enum.Enum):
|
|
65
|
+
"""Types of 2D XY plots."""
|
|
66
|
+
|
|
67
|
+
SCATTER = "scatter"
|
|
68
|
+
SCATTER_DENSITY = "scatter_density"
|
|
69
|
+
HEXBIN = "hexbin"
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def _missing_(cls, value) -> XYPlotType | None:
|
|
73
|
+
normal = utils.normalise_name(str(value))
|
|
74
|
+
for member in cls:
|
|
75
|
+
if member.value == normal:
|
|
76
|
+
return member
|
|
77
|
+
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"'{value}' is not a valid XYPlotType should be one of: "
|
|
80
|
+
+ ", ".join(f"'{i.value}'" for i in cls)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def hexbin(
|
|
85
|
+
fig: figure.Figure,
|
|
86
|
+
ax: axes.Axes,
|
|
87
|
+
data: BasicData,
|
|
88
|
+
cmap: CmapData | None = None,
|
|
89
|
+
gridsize: int = 50,
|
|
90
|
+
colorbar: bool = True,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Add hexbin plot to given `ax` and add colorbar to `fig`.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
fig : figure.Figure
|
|
97
|
+
Figure containing the plot Axes.
|
|
98
|
+
ax : axes.Axes
|
|
99
|
+
Axes to add hexbin plot to.
|
|
100
|
+
data : BasicData
|
|
101
|
+
Data for creating the hexbin plot.
|
|
102
|
+
cmap : CmapData, optional
|
|
103
|
+
Data for defining the hexbin colormap.
|
|
104
|
+
gridsize : int, default 50
|
|
105
|
+
Number of hexagons in the x-direction, i.e.
|
|
106
|
+
width of the hexbin grid.
|
|
107
|
+
colorbar : bool, default True
|
|
108
|
+
If True add colorbar to `fig`.
|
|
109
|
+
|
|
110
|
+
Raises
|
|
111
|
+
------
|
|
112
|
+
KeyError
|
|
113
|
+
If colormap data column isn't present in `data.data`.
|
|
114
|
+
"""
|
|
115
|
+
weights = None
|
|
116
|
+
if cmap is not None:
|
|
117
|
+
if cmap.column not in data.data.columns:
|
|
118
|
+
raise KeyError(f"colormap data column ('{cmap.column}') not present")
|
|
119
|
+
weights = cmap.column
|
|
120
|
+
|
|
121
|
+
hb = ax.hexbin(
|
|
122
|
+
data.x_column,
|
|
123
|
+
data.y_column,
|
|
124
|
+
C=weights,
|
|
125
|
+
data=data.data,
|
|
126
|
+
mincnt=1,
|
|
127
|
+
gridsize=gridsize,
|
|
128
|
+
linewidths=0.1,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if not colorbar:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
if cmap is None:
|
|
135
|
+
label: str | None = "Count"
|
|
136
|
+
else:
|
|
137
|
+
label = cmap.get_label()
|
|
138
|
+
|
|
139
|
+
fig.colorbar(hb, ax=ax, label=label)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def scatter(
|
|
143
|
+
fig: figure.Figure,
|
|
144
|
+
ax: axes.Axes,
|
|
145
|
+
data: BasicData,
|
|
146
|
+
density: bool = False,
|
|
147
|
+
cmap: CmapData | None = None,
|
|
148
|
+
colorbar: bool = True,
|
|
149
|
+
**kwargs,
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Add scatter plot to given `ax`, with optional density colormap.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
fig : figure.Figure
|
|
156
|
+
Figure containing plot Axes.
|
|
157
|
+
ax : axes.Axes
|
|
158
|
+
Axes to add scatter plot to.
|
|
159
|
+
data : BasicData
|
|
160
|
+
DataFrame and columns containing data to be plotted.
|
|
161
|
+
density : bool, default False
|
|
162
|
+
Color scatter plot points based on the desntity,
|
|
163
|
+
uses gaussian_kde to estimate density.
|
|
164
|
+
cmap : CmapData, optional
|
|
165
|
+
Data for colouring scatter plot points,
|
|
166
|
+
cannot be used if `density` is True.
|
|
167
|
+
colorbar : bool, default True
|
|
168
|
+
If True and `density` is True add colour bar to `fig`.
|
|
169
|
+
kwargs : Keyword arguments
|
|
170
|
+
Additional keyword arguments to pass to `ax.scatter`,
|
|
171
|
+
'c' parameter will be ignored if `density` or `cmap`
|
|
172
|
+
are used.
|
|
173
|
+
|
|
174
|
+
Raises
|
|
175
|
+
------
|
|
176
|
+
ValueError
|
|
177
|
+
If `cmap` is given and `density` is True.
|
|
178
|
+
|
|
179
|
+
See Also
|
|
180
|
+
--------
|
|
181
|
+
axes.Axes.scatter: base matplotlib method for creating scatter plots.
|
|
182
|
+
"""
|
|
183
|
+
xy_data = data.data[[data.x_column, data.y_column]].values.T
|
|
184
|
+
z_data = None
|
|
185
|
+
|
|
186
|
+
if cmap is not None and density:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
"cmap and density shouldn't both be given,"
|
|
189
|
+
" unsure which to use for colouring scatter plot"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if cmap is not None:
|
|
193
|
+
z_data = data.data[cmap.column]
|
|
194
|
+
elif density:
|
|
195
|
+
# Calculate point density
|
|
196
|
+
kernel = stats.gaussian_kde(xy_data)
|
|
197
|
+
z_data = kernel(xy_data)
|
|
198
|
+
|
|
199
|
+
if z_data is not None:
|
|
200
|
+
# Sort the points by density, so that the densest points are plotted last
|
|
201
|
+
idx = z_data.argsort()
|
|
202
|
+
z_data = z_data[idx]
|
|
203
|
+
xy_data = np.take(xy_data, idx, axis=1)
|
|
204
|
+
|
|
205
|
+
if "c" in kwargs:
|
|
206
|
+
warnings.warn("`c` parameter cannot be used if `cmap` or `density` is provided")
|
|
207
|
+
kwargs.pop("c")
|
|
208
|
+
|
|
209
|
+
else:
|
|
210
|
+
z_data = kwargs.pop("c", None)
|
|
211
|
+
|
|
212
|
+
points = ax.scatter(xy_data[0], xy_data[1], c=z_data)
|
|
213
|
+
|
|
214
|
+
if not colorbar or z_data is None:
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
if cmap is not None:
|
|
218
|
+
fig.colorbar(points, ax=ax, label=cmap.get_label())
|
|
219
|
+
else:
|
|
220
|
+
fig.colorbar(points, ax=ax, label="Density", ticks=ticker.NullLocator())
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def axes_plot_xy(
|
|
224
|
+
fig: figure.Figure,
|
|
225
|
+
ax: axes.Axes,
|
|
226
|
+
type_: XYPlotType,
|
|
227
|
+
data: BasicData,
|
|
228
|
+
cmap: CmapData | None = None,
|
|
229
|
+
**plot_kwargs,
|
|
230
|
+
) -> None:
|
|
231
|
+
"""Add plot to given `ax` based on `type_`.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
fig : figure.Figure
|
|
236
|
+
Figure containing the Axes being plotted on.
|
|
237
|
+
ax : axes.Axes
|
|
238
|
+
Axes to create plot on.
|
|
239
|
+
type_ : XYPlotType
|
|
240
|
+
Type of plot to create
|
|
241
|
+
data : BasicData
|
|
242
|
+
Data for creating the plot.
|
|
243
|
+
cmap : CmapData, optional
|
|
244
|
+
Data for defining any colormaps used in the plot.
|
|
245
|
+
|
|
246
|
+
Raises
|
|
247
|
+
------
|
|
248
|
+
NotImplementedError
|
|
249
|
+
For any plot types which haven't been implemented.
|
|
250
|
+
|
|
251
|
+
See Also
|
|
252
|
+
--------
|
|
253
|
+
XYPlotType: for plot types which can be used.
|
|
254
|
+
hexbin, scatter
|
|
255
|
+
"""
|
|
256
|
+
if type_ == XYPlotType.HEXBIN:
|
|
257
|
+
hexbin(fig, ax, data, cmap, **plot_kwargs)
|
|
258
|
+
|
|
259
|
+
elif type_ in (XYPlotType.SCATTER, XYPlotType.SCATTER_DENSITY):
|
|
260
|
+
if len(data.data) > 1_000_000:
|
|
261
|
+
warnings.warn(
|
|
262
|
+
"scatter plots may take a long time with "
|
|
263
|
+
"a lot of data, try hexbin plots instead",
|
|
264
|
+
RuntimeWarning,
|
|
265
|
+
)
|
|
266
|
+
scatter(
|
|
267
|
+
fig,
|
|
268
|
+
ax,
|
|
269
|
+
data,
|
|
270
|
+
density=type_ == XYPlotType.SCATTER_DENSITY,
|
|
271
|
+
cmap=cmap,
|
|
272
|
+
**plot_kwargs,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
else:
|
|
276
|
+
raise NotImplementedError(f"unknown plot type {type_}")
|
|
277
|
+
|
|
278
|
+
if data.title is not None:
|
|
279
|
+
ax.set_title(data.title)
|
|
280
|
+
|
|
281
|
+
if data.x_label is not None:
|
|
282
|
+
ax.set_xlabel(data.x_label)
|
|
283
|
+
elif data.auto_label:
|
|
284
|
+
ax.set_xlabel(data.x_column)
|
|
285
|
+
|
|
286
|
+
if data.y_label is not None:
|
|
287
|
+
ax.set_ylabel(data.y_label)
|
|
288
|
+
elif data.auto_label:
|
|
289
|
+
ax.set_ylabel(data.y_column)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def plot_xy(
|
|
293
|
+
data: pd.DataFrame,
|
|
294
|
+
x_column: str | Sequence[str],
|
|
295
|
+
y_column: str | Sequence[str],
|
|
296
|
+
type_: XYPlotType = XYPlotType.SCATTER,
|
|
297
|
+
title: str | None = None,
|
|
298
|
+
weight_column: None | str | Sequence[str] = None,
|
|
299
|
+
) -> figure.Figure:
|
|
300
|
+
"""Create a graph of `data` based on given columns.
|
|
301
|
+
|
|
302
|
+
Multiple subplots may be produced on the same figure
|
|
303
|
+
if multiple values are given for the columns.
|
|
304
|
+
|
|
305
|
+
Parameters
|
|
306
|
+
----------
|
|
307
|
+
data : pd.DataFrame
|
|
308
|
+
Data to be plotted.
|
|
309
|
+
x_column, y_column : str | Sequence[str]
|
|
310
|
+
Name(s) of columns containing x (or y) values. If multiple
|
|
311
|
+
are given the figure will contain different subplots for each.
|
|
312
|
+
type_ : XYPlotType, default XYPlotType.SCATTER
|
|
313
|
+
Type of plot to add to axes.
|
|
314
|
+
title : str, optional
|
|
315
|
+
Title to add to figure.
|
|
316
|
+
weight_column : str | Sequence[str], optional
|
|
317
|
+
Column(s) containing data for creating colormaps
|
|
318
|
+
on the plots.
|
|
319
|
+
|
|
320
|
+
Returns
|
|
321
|
+
-------
|
|
322
|
+
figure.Figure
|
|
323
|
+
Figure with plots.
|
|
324
|
+
|
|
325
|
+
Raises
|
|
326
|
+
------
|
|
327
|
+
ValueError
|
|
328
|
+
If `x_column` and `y_column` are sequences of different lengths.
|
|
329
|
+
If `weight_column` doesn't contain the same number of columns
|
|
330
|
+
as `x_column`.
|
|
331
|
+
"""
|
|
332
|
+
type_ = XYPlotType(type_)
|
|
333
|
+
|
|
334
|
+
if isinstance(x_column, str) and isinstance(y_column, str):
|
|
335
|
+
x_column, y_column = (x_column,), (y_column,)
|
|
336
|
+
|
|
337
|
+
x_column, y_column = tuple(x_column), tuple(y_column)
|
|
338
|
+
if len(x_column) != len(y_column):
|
|
339
|
+
raise ValueError(
|
|
340
|
+
f"number of x column names ({len(x_column)}) and "
|
|
341
|
+
f"y column names ({len(y_column)}) should be the same"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
if weight_column is not None:
|
|
345
|
+
if isinstance(weight_column, str):
|
|
346
|
+
weight_column = (weight_column,)
|
|
347
|
+
weight_column = tuple(weight_column)
|
|
348
|
+
if len(weight_column) != len(x_column):
|
|
349
|
+
raise ValueError(
|
|
350
|
+
f"number of weight column names ({len(weight_column)}) should "
|
|
351
|
+
f"be the same as the x and y column names ({len(x_column)})"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
plot_data = []
|
|
355
|
+
for i, (x, y) in enumerate(zip(x_column, y_column)):
|
|
356
|
+
plot_data.append(
|
|
357
|
+
dict(
|
|
358
|
+
type_=type_,
|
|
359
|
+
data=BasicData(data, x, y),
|
|
360
|
+
cmap=None if weight_column is None else CmapData(weight_column[i]),
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
fig = subplots.grid_plot(axes_plot_xy, plot_data)
|
|
365
|
+
if title is not None:
|
|
366
|
+
fig.suptitle(title)
|
|
367
|
+
|
|
368
|
+
return fig
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: caf.viz
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python-based visualisation tools for transport data
|
|
5
|
+
Author: Transport for the North
|
|
6
|
+
License: Copyright © Transport for the North (“TfN”) (2024).
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Use of this software and associated documentation files (“the Software”) by you indicates your acceptance of the terms and conditions below (“the Licence”).
|
|
10
|
+
We make available the Software to you on the basis of this Licence. We do not sell the Software to you. We remain the owners of the Software at all times.
|
|
11
|
+
|
|
12
|
+
We grant you a non-exclusive, worldwide, royalty-free, perpetual licence:
|
|
13
|
+
(a) to use the Software;
|
|
14
|
+
(b) to develop, modify and maintain the Software;
|
|
15
|
+
(c) copy, publish, distribute and transmit the Software;
|
|
16
|
+
(d) adapt the Software.
|
|
17
|
+
|
|
18
|
+
Where you do any of the above you must acknowledge the source of the Software in your product or application by including or linking the following attribution statement in a prominent and noticeable location in or in the context of your product or application:
|
|
19
|
+
“Outputs derived from CAF.Viz (the Common Analytical Framework Visualisation Suite), developed by Transport for the North [https://github.com/Transport-for-the-North/caf.viz]”
|
|
20
|
+
|
|
21
|
+
Any adaptations applied to the Software before application should be briefly described alongside the above citation. We encourage you to submit to us any adaptations to the Software and where you choose to do so your submissions will be considered for integration into the Software.
|
|
22
|
+
|
|
23
|
+
The above are important conditions of this Licence and if you fail to comply with them the rights granted to you under this Licence will end automatically.
|
|
24
|
+
This Licence does not grant you any right to use the Software in a way that suggests any official status or that we endorse you or your use of the Software.
|
|
25
|
+
The Software is licenced 'as is' and we exclude all representations, warranties, obligations and liabilities in relation to the Software to the maximum extent permitted by law.
|
|
26
|
+
We are not liable for any errors or omissions in the Software and shall not be liable for any loss, injury or damage of any kind caused by its use. We do not guarantee the continued supply of the Software.
|
|
27
|
+
This Licence is governed by the laws of the jurisdiction in which we have our principal place of business.
|
|
28
|
+
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/Transport-for-the-North/caf.viz/issues
|
|
30
|
+
Project-URL: Homepage, https://github.com/Transport-for-the-North/caf.viz
|
|
31
|
+
Project-URL: Source, https://github.com/Transport-for-the-North/caf.viz
|
|
32
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
36
|
+
Requires-Python: >=3.10
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
License-File: LICENSE
|
|
39
|
+
Requires-Dist: numpy>=1.19.0
|
|
40
|
+
Requires-Dist: pandas>=1.4.0
|
|
41
|
+
Requires-Dist: matplotlib>=3.8.2
|
|
42
|
+
Requires-Dist: scipy>=1.11.4
|
|
43
|
+
Requires-Dist: pydantic>=2.0
|
|
44
|
+
Provides-Extra: lint
|
|
45
|
+
Requires-Dist: black>=22.6.0; extra == "lint"
|
|
46
|
+
Requires-Dist: isort>=5.12.0; extra == "lint"
|
|
47
|
+
Requires-Dist: mypy>=1.0.0; extra == "lint"
|
|
48
|
+
Requires-Dist: mypy_extensions>=1.0.0; extra == "lint"
|
|
49
|
+
Requires-Dist: pydocstyle[toml]>=6.1.1; extra == "lint"
|
|
50
|
+
Requires-Dist: pylint>=2.14.5; extra == "lint"
|
|
51
|
+
Provides-Extra: test
|
|
52
|
+
Requires-Dist: pytest>=7.4.0; extra == "test"
|
|
53
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "test"
|
|
54
|
+
Requires-Dist: pytest-xdist>=3.3.1; extra == "test"
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
<h1 align="center">CAF Viz</h1>
|
|
59
|
+
|
|
60
|
+
<p align="center">
|
|
61
|
+
<a href="https://github.com/PyCQA/pylint"><img alt="linting: pylint" src="https://img.shields.io/badge/linting-pylint-yellowgreen"></a>
|
|
62
|
+
<a href="https://google.github.io/styleguide/pyguide.html"><img alt="code format: Google Style Guide" src="https://img.shields.io/badge/code%20style-Google%20Style%20Guide-blue"></a>
|
|
63
|
+
<a href="https://github.com/psf/black"><img alt="code style: black" src="https://img.shields.io/badge/code%20format-black-000000.svg"></a>
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
Python-based visualisation tools for transport data.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.cfg
|
|
5
|
+
src/caf.viz.egg-info/PKG-INFO
|
|
6
|
+
src/caf.viz.egg-info/SOURCES.txt
|
|
7
|
+
src/caf.viz.egg-info/dependency_links.txt
|
|
8
|
+
src/caf.viz.egg-info/requires.txt
|
|
9
|
+
src/caf.viz.egg-info/top_level.txt
|
|
10
|
+
src/caf/viz/__init__.py
|
|
11
|
+
src/caf/viz/_version.py
|
|
12
|
+
src/caf/viz/py.typed
|
|
13
|
+
src/caf/viz/subplots.py
|
|
14
|
+
src/caf/viz/utils.py
|
|
15
|
+
src/caf/viz/xy_plot.py
|
|
16
|
+
tests/test_example.py
|
|
17
|
+
tests/test_xy_plot.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
numpy>=1.19.0
|
|
2
|
+
pandas>=1.4.0
|
|
3
|
+
matplotlib>=3.8.2
|
|
4
|
+
scipy>=1.11.4
|
|
5
|
+
pydantic>=2.0
|
|
6
|
+
|
|
7
|
+
[lint]
|
|
8
|
+
black>=22.6.0
|
|
9
|
+
isort>=5.12.0
|
|
10
|
+
mypy>=1.0.0
|
|
11
|
+
mypy_extensions>=1.0.0
|
|
12
|
+
pydocstyle[toml]>=6.1.1
|
|
13
|
+
pylint>=2.14.5
|
|
14
|
+
|
|
15
|
+
[test]
|
|
16
|
+
pytest>=7.4.0
|
|
17
|
+
pytest-cov>=4.1.0
|
|
18
|
+
pytest-xdist>=3.3.1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
caf
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Tests for the caf.viz module"""
|
|
3
|
+
# Built-Ins
|
|
4
|
+
|
|
5
|
+
# Third Party
|
|
6
|
+
|
|
7
|
+
# pylint: disable=import-error,wrong-import-position
|
|
8
|
+
|
|
9
|
+
# pylint: enable=import-error,wrong-import-position
|
|
10
|
+
|
|
11
|
+
# # # CONSTANTS # # #
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# # # FIXTURES # # #
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# # # TESTS # # #
|
|
18
|
+
class TestFunctionName:
|
|
19
|
+
"""Placeholder for test class."""
|
|
20
|
+
|
|
21
|
+
def test_something(self):
|
|
22
|
+
"""Placeholder for test function."""
|
|
23
|
+
assert True
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Tests for the `xy_plot` module."""
|
|
3
|
+
|
|
4
|
+
##### IMPORTS #####
|
|
5
|
+
|
|
6
|
+
# Built-Ins
|
|
7
|
+
import itertools
|
|
8
|
+
import pathlib
|
|
9
|
+
from typing import Iterator
|
|
10
|
+
|
|
11
|
+
# Third Party
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import pytest
|
|
15
|
+
from matplotlib import pyplot as plt
|
|
16
|
+
from matplotlib.backends import backend_pdf
|
|
17
|
+
|
|
18
|
+
# Local Imports
|
|
19
|
+
from caf.viz import xy_plot
|
|
20
|
+
|
|
21
|
+
##### CONSTANTS #####
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
##### CLASSES & FUNCTIONS #####
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture(name="random_data")
|
|
28
|
+
def fix_random_data() -> tuple[pd.DataFrame, Iterator[tuple[str, str]]]:
|
|
29
|
+
"""Generate DataFrame containing 10 random x / y columns.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
pd.DataFrame
|
|
34
|
+
DataFrame with 10 pairs of x / y columns containing random data,
|
|
35
|
+
with columns: X1, Y1... X9, Y9.
|
|
36
|
+
Iterator[tuple[str, str]]
|
|
37
|
+
Infinite generator which returns the names of a pair of x / y
|
|
38
|
+
columns, will repeat previous column names after 9.
|
|
39
|
+
"""
|
|
40
|
+
data = {}
|
|
41
|
+
columns = []
|
|
42
|
+
rng = np.random.default_rng()
|
|
43
|
+
for col in range(10):
|
|
44
|
+
names = (f"X{col}", f"Y{col}")
|
|
45
|
+
columns.append(names)
|
|
46
|
+
|
|
47
|
+
rand_data = rng.normal(10_000, 5000, 1000)
|
|
48
|
+
data[names[0]] = rand_data
|
|
49
|
+
data[names[1]] = rand_data + (rng.random(1000) * 10_000)
|
|
50
|
+
|
|
51
|
+
return pd.DataFrame(data), itertools.cycle(columns)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.integration
|
|
55
|
+
@pytest.mark.parametrize("n_subplots", [1, 6, 10, 23])
|
|
56
|
+
def test_plot(
|
|
57
|
+
tmp_path: pathlib.Path,
|
|
58
|
+
random_data: tuple[pd.DataFrame, Iterator[tuple[str, str]]],
|
|
59
|
+
n_subplots: int,
|
|
60
|
+
):
|
|
61
|
+
"""Test `plot_xy` with different number of subplots for all plot types."""
|
|
62
|
+
plt.style.use("caf.viz.tfn")
|
|
63
|
+
|
|
64
|
+
data, column_iter = random_data
|
|
65
|
+
x_columns: list[str] = []
|
|
66
|
+
y_columns: list[str] = []
|
|
67
|
+
|
|
68
|
+
while len(x_columns) < n_subplots:
|
|
69
|
+
x, y = next(column_iter)
|
|
70
|
+
x_columns.append(x)
|
|
71
|
+
y_columns.append(y)
|
|
72
|
+
|
|
73
|
+
out_file = tmp_path / "test_plots.pdf"
|
|
74
|
+
with backend_pdf.PdfPages(out_file) as pdf:
|
|
75
|
+
for type_ in xy_plot.XYPlotType:
|
|
76
|
+
fig = xy_plot.plot_xy(
|
|
77
|
+
data,
|
|
78
|
+
x_columns,
|
|
79
|
+
y_columns,
|
|
80
|
+
type_,
|
|
81
|
+
f"Test {type_.value.strip().replace('_', ' ').title()} Plots",
|
|
82
|
+
)
|
|
83
|
+
pdf.savefig(fig)
|
|
84
|
+
plt.close(fig)
|
|
85
|
+
|
|
86
|
+
assert out_file.is_file(), f"'{out_file.name}' not created"
|