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 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
+ ![Transport for the North Logo](https://github.com/Transport-for-the-North/caf.toolkit/blob/main/docs/TFN_Landscape_Colour_CMYK.png)
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,11 @@
1
+ ![Transport for the North Logo](https://github.com/Transport-for-the-North/caf.toolkit/blob/main/docs/TFN_Landscape_Colour_CMYK.png)
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"
@@ -0,0 +1,9 @@
1
+ [options]
2
+ cmdclass =
3
+ sdist = versioningit.cmdclass.sdist
4
+ build_py = versioningit.cmdclass.build_py
5
+
6
+ [egg_info]
7
+ tag_build =
8
+ tag_date = 0
9
+
@@ -0,0 +1,5 @@
1
+ """CAF package containing functionality for transport related visualisations."""
2
+
3
+ from ._version import __version__
4
+
5
+ # Aliases
@@ -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
+ ![Transport for the North Logo](https://github.com/Transport-for-the-North/caf.toolkit/blob/main/docs/TFN_Landscape_Colour_CMYK.png)
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,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"