├── tests ├── __init__.py ├── backtests │ ├── __init__.py │ └── test_compare_outputs.py └── unittests │ ├── __init__.py │ ├── test_processing_steps.py │ ├── test_part_of_two_in_three.py │ ├── test_seven_point_one_side_mean.py │ ├── test_two_in_three.py │ ├── test_seven_point_trend.py │ ├── test_limits_calculations.py │ ├── test_part_of_seven_trend.py │ ├── test_special_cause_flag.py │ └── test_pandas_spc_x_calc.py ├── reports └── ae_attendance.qmd ├── docs ├── .gitignore ├── _site │ ├── robots.txt │ ├── posts │ │ ├── welcome │ │ │ └── thumbnail.png │ │ └── rap-maturity │ │ │ └── nhsd-rap.png │ ├── site_libs │ │ ├── bootstrap │ │ │ └── bootstrap-icons.woff │ │ ├── quarto-html │ │ │ ├── tippy.css │ │ │ ├── quarto-syntax-highlighting.css │ │ │ └── anchor.min.js │ │ ├── quarto-nav │ │ │ └── quarto-nav.js │ │ ├── quarto-listing │ │ │ └── quarto-listing.js │ │ └── clipboard │ │ │ └── clipboard.min.js │ ├── listings.json │ ├── sitemap.xml │ ├── updates.xml │ ├── index.css │ ├── _assets │ │ └── style │ │ │ └── styles.css │ └── documentation │ │ └── index.html ├── _assets │ ├── favicon.ico │ ├── style │ │ ├── js.html │ │ ├── theme.scss │ │ └── styles.css │ └── img │ │ └── iconfinder_Rubics_Cube.svg ├── posts │ ├── welcome │ │ ├── thumbnail.png │ │ └── index.qmd │ ├── rap-maturity │ │ ├── nhsd-rap.png │ │ └── index.qmd │ └── _metadata.yml ├── documentation │ ├── index.qmd │ └── pandas_spc_x_calc.qmd ├── faq.qmd ├── tutorials │ ├── pandas.qmd │ └── index.qmd ├── updates.qmd ├── about.qmd ├── contribute.qmd ├── _quarto.yml ├── index.qmd ├── _freeze │ └── site_libs │ │ └── clipboard │ │ └── clipboard.min.js └── index.css ├── nhspy_plotthedots ├── __init__.py ├── utilities │ ├── __init__.py │ ├── field_definitions.py │ ├── processing_steps.py │ └── data_connections.py ├── params.py ├── create_publication.py ├── spc_calculations.py ├── plotly_spc_chart.py └── pandas_spc_calculations.py ├── logo.png ├── requirements.txt ├── environment.yml ├── setup.py ├── .github ├── dependabot.yml └── workflows │ ├── codecov.yml │ ├── quarto_publish.yml │ ├── pytest_flake8.yml │ └── codeql.yml ├── pyproject.toml ├── LICENSE ├── .gitignore └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /reports/ae_attendance.qmd: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /.quarto/ 2 | -------------------------------------------------------------------------------- /tests/backtests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/unittests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /nhspy_plotthedots/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /nhspy_plotthedots/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhs-oa-community/nhspy-plotthedots/HEAD/logo.png -------------------------------------------------------------------------------- /docs/_site/robots.txt: -------------------------------------------------------------------------------- 1 | Sitemap: https://nhs-pycom.github.io/nhspy-plotthedots/sitemap.xml 2 | -------------------------------------------------------------------------------- /tests/backtests/test_compare_outputs.py: -------------------------------------------------------------------------------- 1 | # Compares historical data outputs between RAP and non RAP pipelines. 2 | -------------------------------------------------------------------------------- /docs/_assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhs-oa-community/nhspy-plotthedots/HEAD/docs/_assets/favicon.ico -------------------------------------------------------------------------------- /docs/posts/welcome/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhs-oa-community/nhspy-plotthedots/HEAD/docs/posts/welcome/thumbnail.png -------------------------------------------------------------------------------- /docs/posts/rap-maturity/nhsd-rap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhs-oa-community/nhspy-plotthedots/HEAD/docs/posts/rap-maturity/nhsd-rap.png -------------------------------------------------------------------------------- /docs/_site/posts/welcome/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhs-oa-community/nhspy-plotthedots/HEAD/docs/_site/posts/welcome/thumbnail.png -------------------------------------------------------------------------------- /docs/documentation/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Documentation" 3 | comments: false 4 | --- 5 | 6 | ::: {.callout-warning} 7 | ## Under Development 8 | ::: -------------------------------------------------------------------------------- /docs/_site/posts/rap-maturity/nhsd-rap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhs-oa-community/nhspy-plotthedots/HEAD/docs/_site/posts/rap-maturity/nhsd-rap.png -------------------------------------------------------------------------------- /docs/faq.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Frequently Asked Questions" 3 | comments: false 4 | --- 5 | 6 | ::: {.callout-warning} 7 | ## Under Development 8 | ::: 9 | -------------------------------------------------------------------------------- /docs/_site/site_libs/bootstrap/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhs-oa-community/nhspy-plotthedots/HEAD/docs/_site/site_libs/bootstrap/bootstrap-icons.woff -------------------------------------------------------------------------------- /docs/tutorials/pandas.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Pandas" 3 | subtitle: "Working with pandas" 4 | comments: false 5 | --- 6 | 7 | ::: {.callout-warning} 8 | ## Under Development 9 | ::: -------------------------------------------------------------------------------- /docs/_site/listings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "listing": "/updates.html", 4 | "items": [ 5 | "/posts/rap-maturity/index.html", 6 | "/posts/welcome/index.html" 7 | ] 8 | } 9 | ] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # The libraries used by your code should be listed here 2 | # See https://github.com/NHSDigital/rap-community-of-practice/blob/main/python/project-structure-and-packaging.md 3 | 4 | pandas 5 | numpy 6 | plotly -------------------------------------------------------------------------------- /docs/documentation/pandas_spc_x_calc.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "pandas_spc_x_calc" 3 | subtitle: "Calculates the SPC for a given DataFrame with a set column of values" 4 | comments: false 5 | --- 6 | 7 | ::: {.callout-warning} 8 | ## Under Development 9 | ::: -------------------------------------------------------------------------------- /docs/posts/_metadata.yml: -------------------------------------------------------------------------------- 1 | # options specified here will apply to all posts in this folder 2 | 3 | # freeze computational output 4 | # (see https://quarto.org/docs/projects/code-execution.html#freeze) 5 | freeze: true 6 | 7 | # Enable banner style title blocks 8 | title-block-banner: true 9 | -------------------------------------------------------------------------------- /docs/updates.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Updates" 3 | subtitle: "News, tips, and commentary" 4 | listing: 5 | sort: "date desc" 6 | contents: "posts" 7 | sort-ui: false 8 | filter-ui: false 9 | categories: true 10 | feed: true 11 | page-layout: full 12 | comments: false 13 | --- -------------------------------------------------------------------------------- /nhspy_plotthedots/params.py: -------------------------------------------------------------------------------- 1 | """ 2 | Purpose of script: contains all of the parameters that we expect to change 3 | frequently, e.g. input data. 4 | """ 5 | params = { 6 | 'publication_version': 'This message shows that you have successfully imported params from the params module' 7 | } 8 | -------------------------------------------------------------------------------- /docs/_assets/style/js.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | #A template of the conda environment 2 | #The template contains commonly used packages 3 | #For details: https://github.com/NHSDigital/rap-community-of-practice/blob/main/python/virtual-environments.md 4 | name: nhspy-plotthedots 5 | 6 | channels: 7 | - defaults 8 | 9 | dependencies: 10 | - flake8=3.9.0 11 | - openpyxl 12 | - pip==20.2.4 13 | - python=3.9 14 | - pyodbc 15 | - ipykernel 16 | - ipython 17 | - nbconvert==6.0.7 18 | - numpy==1.19.5 19 | - pandas -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Template of setup.py. 3 | 4 | See https://github.com/NHSDigital/rap-community-of-practice/blob/main/python/project-structure-and-packaging.md 5 | """ 6 | 7 | from setuptools import find_packages, setup 8 | 9 | setup( 10 | name='nhspy-plotthedots', 11 | packages=find_packages(), 12 | version='0.1.6', 13 | description='Python SPC chart tool', 14 | author='NHS Python Community', 15 | license='MIT', 16 | setup_requires=['pytest-runner','flake8'], 17 | tests_require=['pytest'], 18 | ) 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /docs/_assets/style/theme.scss: -------------------------------------------------------------------------------- 1 | /*-- scss:defaults --*/ 2 | $link-color: #39729E; 3 | $text-muted: #6a737b; 4 | 5 | /*-- scss:rules --*/ 6 | 7 | .layout-example { 8 | background: $gray-500; 9 | color: $white; 10 | text-align: center; 11 | margin-bottom: 1em; 12 | font-family: $font-family-monospace; 13 | font-size: .875em; 14 | font-weight: 600; 15 | padding-top: 1em; 16 | border-radius: 3px; 17 | } 18 | 19 | .left { 20 | text-align: left; 21 | padding-left: 1em; 22 | } 23 | 24 | .right { 25 | text-align: right; 26 | padding-right: 1em; 27 | } 28 | 29 | .hello-quarto-banner h1 { 30 | margin-top: 0; 31 | margin-bottom: 0.5rem; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /tests/unittests/test_processing_steps.py: -------------------------------------------------------------------------------- 1 | """ 2 | Template of a unit test script 3 | 4 | 5 | To run this test requires the following step: 6 | 1) Open Anaconda prompt and set the directory to the project folder 7 | 2) Enter: 8 | >pytest 9 | and run 10 | """ 11 | 12 | import pytest 13 | 14 | from nhspy_plotthedots.utilities import processing_steps 15 | 16 | def test_derived_something(): 17 | """ 18 | A template to create a pytest def 19 | 20 | """ 21 | ground_truth = 'output' 22 | 23 | output_from_function = processing_steps.derived_something() 24 | 25 | assert ground_truth == output_from_function -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: code coverage 2 | on: [pull_request] 3 | jobs: 4 | run: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v2 9 | - name: Set up Python 3.10 10 | uses: actions/setup-python@v2 11 | with: 12 | python-version: '3.10' 13 | - name: Install dependencies 14 | run: | 15 | python -m pip install --upgrade pip 16 | pip install pytest-cov pytest 17 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 18 | - name: Run tests and collect coverage 19 | run: pytest --cov nhspy_plotthedots 20 | - name: Upload coverage to Codecov 21 | uses: codecov/codecov-action@v3 -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "nhspy-plotthedots-test" 7 | version = "0.1.6" 8 | authors = [ 9 | { name="Craig R Shenton", email="craig.shenton@nhs.net" }, 10 | { name="Tom Jemmett", email="tom.jemmett@gmail.com" }, 11 | ] 12 | description = "nhspy-plotthedots: Draw XmR charts for NHSE 'Making Data Count' programme" 13 | readme = "README.md" 14 | requires-python = ">=3.7" 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | ] 20 | 21 | [project.urls] 22 | "Homepage" = "https://github.com/nhs-pycom/nhspy-plotthedots" 23 | "Bug Tracker" = "https://github.com/nhs-pycom/nhspy-plotthedots/issues" -------------------------------------------------------------------------------- /nhspy_plotthedots/utilities/field_definitions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Purpose of the script: contains the definitions for each of the fields 3 | (columns) derived in the process. By abstracting these definitions out of the 4 | code and making them reuseable, we achieve some great benefits. First, it 5 | becomes much easier to maintain. When the specifications change next year, we 6 | only need to make the change in one location. Next, it becomes much easier to 7 | test. We write unit tests for each of these definitions and can then reuse 8 | these definitions in many places without increasing risk. 9 | 10 | For more information see https://github.com/NHSDigital/rap-community-of-practice/blob/d8ffe2decd737c594222565423fe31d87d590a12/development-approach/05_unit-testing-field-definitions.md#field-definitions-and-how-to-use-them 11 | """ 12 | aggregate_fields = (None) -------------------------------------------------------------------------------- /docs/about.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "About {nhspy-plotthedots}" 3 | comments: false 4 | --- 5 | 6 | ## Authors 7 | 8 | - [Craig Robert Shenton, PhD](https://github.com/craig-shenton) `author`, `maintainer`. 9 | - [Tom Jemmett](https://github.com/tomjemmett) `author`. 10 | 11 | ## Licence 12 | 13 | MIT License, see [`LICENSE.md`](https://github.com/nhs-pycom/nhspy-plotthedots/blob/main/LICENSE) 14 | 15 | ## Citation 16 | 17 | NHS Python Community *nhspy-plotthedots: Draw XmR charts in python for NHSE 'Making Data Count' programme* (2023). At <[https://github.com/nhs-pycom/nhspy-plotthedots](https://github.com/nhs-pycom/nhspy-plotthedots)>. 18 | 19 | ```r 20 | @Manual{nhspyplotthedots2023, 21 | title = {nhspy-plotthedots: Draw XmR charts in python for NHSE 'Making Data Count' programme}, 22 | author = {NHS Python Community}, 23 | url = {https://github.com/nhs-pycom/nhspy-plotthedots}, 24 | year = {2023}, 25 | } 26 | ``` 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/quarto_publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: main 4 | paths: 'docs/**' 5 | pull_request: 6 | branches: main 7 | paths: 'docs/**' 8 | 9 | name: Render and Publish 10 | 11 | jobs: 12 | build-deploy: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | steps: 17 | - name: Check out repository 18 | uses: actions/checkout@v3 19 | 20 | - name: Set up Quarto 21 | uses: quarto-dev/quarto-actions/setup@v2 22 | 23 | - name: Install Python and Dependencies 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: '3.10' 27 | cache: 'pip' 28 | - run: pip install jupyter 29 | - run: pip install -r requirements.txt 30 | 31 | - name: Render and Publish 32 | uses: quarto-dev/quarto-actions/publish@v2 33 | with: 34 | target: gh-pages 35 | path: docs/ 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /docs/posts/welcome/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "nhspy-plotthedots alpha release" 3 | author: "Craig R Shenton" 4 | date: "2023-01-22" 5 | categories: [release] 6 | image: "thumbnail.png" 7 | description: "v0.1.6 (alpha) now available on test pipy" 8 | title-block-banner: false 9 | --- 10 | 11 | We are excited to announce the release of `nhspy-plotthedots` alpha, version `0.1.6`. This new package is now available on test PyPI at [https://test.pypi.org/project/nhspy-plotthedots-test/](https://test.pypi.org/project/nhspy-plotthedots-test/) for users to test and provide feedback. 12 | 13 | In addition to the package release, the documentation site for nhspy-plotthedots is now live. This quarto site will provide detailed information on how to use the package, including installation instructions, usage examples, and API reference. 14 | 15 | We look forward to hearing your feedback on `nhspy-plotthedots` and hope you find it as useful as we do. 16 | 17 | ::: {.callout-warning} 18 | ## Note 19 | 20 | This is an alpha release for testing, not for use in production. 21 | ::: -------------------------------------------------------------------------------- /nhspy_plotthedots/utilities/processing_steps.py: -------------------------------------------------------------------------------- 1 | """ 2 | Purpose of script: contains the core business logic 3 | """ 4 | 5 | def validation_checks(): 6 | """ 7 | A docstring template to describe the function. 8 | 9 | Currently this function prints out a message to indicates it has been 10 | successfully inported from preprocessing module 11 | 12 | Parameters 13 | ---------- 14 | None 15 | 16 | Returns 17 | ------- 18 | None. 19 | 20 | """ 21 | print("This message shows that you have successfully imported \ 22 | the validation_checks() function from the preprocessing module") 23 | 24 | def derived_something(): 25 | """ 26 | A docstring template to describe the function. 27 | 28 | This function is part of the unit test template 29 | 30 | Returns 31 | ------- 32 | An string with the word output 33 | 34 | """ 35 | print("This message shows that you have successfully imported \ 36 | the derived_something() function from the preprocessing module") 37 | 38 | output_vaule = 'output' 39 | 40 | return output_vaule 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 NHS Python Community 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. 22 | -------------------------------------------------------------------------------- /nhspy_plotthedots/utilities/data_connections.py: -------------------------------------------------------------------------------- 1 | """ 2 | Purpose of script: handles reading data in and writing data back out. 3 | """ 4 | 5 | def get_df_from_sql(): 6 | """ 7 | A docstring template to describe the function. 8 | 9 | Currently this function prints out a message to indicates it has been 10 | successfully inported from data connections module 11 | 12 | Parameters 13 | ---------- 14 | None 15 | 16 | Returns 17 | ------- 18 | None. 19 | 20 | """ 21 | print("This message shows that you have successfully imported \ 22 | the get_df_from_sql() function from the data connections module") 23 | 24 | def write_df_to_sql(): 25 | """ 26 | A docstring template to describe the function. 27 | 28 | Currently this function prints out a message to indicates it has been 29 | successfully inported from data connections module 30 | 31 | Parameters 32 | ---------- 33 | None 34 | 35 | Returns 36 | ------- 37 | None. 38 | 39 | """ 40 | 41 | print("This message shows that you have successfully imported \ 42 | the write_df_to_sql() function from the data connections module") 43 | 44 | -------------------------------------------------------------------------------- /.github/workflows/pytest_flake8.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: pytest and flake8 5 | 6 | on: [pull_request] 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Python 3.10 19 | uses: actions/setup-python@v3 20 | with: 21 | python-version: "3.10" 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install flake8 pytest 26 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 27 | - name: Lint with flake8 28 | run: | 29 | # stop the build if there are Python syntax errors or undefined names 30 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 31 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 32 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 33 | - name: Test with pytest 34 | run: | 35 | pytest 36 | -------------------------------------------------------------------------------- /docs/_site/site_libs/quarto-html/tippy.css: -------------------------------------------------------------------------------- 1 | .tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1} -------------------------------------------------------------------------------- /docs/_assets/img/iconfinder_Rubics_Cube.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nhspy_plotthedots/create_publication.py: -------------------------------------------------------------------------------- 1 | """ 2 | Purpose of the script: organises the steps in a simple, easy-to-understand manner 3 | that should be readable by anyone, even if they don't know python. In this way, 4 | we aim to reduce risk by make the code accessible to new staff. 5 | 6 | In this template, we use the sys library for add inputs via command line 7 | 8 | To use sys, open anaconda prompt and point to the root folder. For example 9 | suppose I want to input 10 in the command line: 10 | 11 | > python .\my_project\create_publication.py 10 12 | 13 | The expected output would be: 14 | This message shows that you have successfully imported the get_df_from_sql() function from the data connections module 15 | This message shows that you have successfully imported the write_df_to_sql() function from the data connections module 16 | This message shows that you have successfully imported the validation_checks() function from the preprocessing module 17 | This message shows that you have successfully imported params from the params module 18 | user parameter: 10 19 | 20 | """ 21 | 22 | # %% 23 | import sys 24 | from utilities import data_connections 25 | from utilities import processing_steps 26 | from params import params 27 | 28 | def main(): 29 | # check if data_connections module has been imported 30 | data_connections.get_df_from_sql() 31 | 32 | data_connections.write_df_to_sql() 33 | 34 | # check if processing_steps module has been imported 35 | processing_steps.validation_checks() 36 | 37 | # check if params file has been imported 38 | print(params['publication_version']) 39 | 40 | # check if user command line parameters have been imported 41 | for arg in sys.argv[1:]: 42 | print('user parameter: ' + arg) 43 | 44 | if __name__ == "__main__": 45 | main() -------------------------------------------------------------------------------- /docs/contribute.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contribute to the docs 3 | subtitle: "How to add a new page to the documentations site using Quarto?" 4 | --- 5 | 6 | ## How to contribute? 7 | 8 | ### Make a new branch of the repository 9 | 10 | ```bash 11 | git checkout -b 12 | ``` 13 | 14 | ### Make a new `.qmd` (q-markdown) file in the `/docs` folder 15 | 16 | To the `.qmd` file add a YAML header with a title and subtitle. 17 | ```yaml 18 | --- 19 | title: Contribute to the docs 20 | subtitle: "How to add a new page to the documentations site using Quarto?" 21 | --- 22 | ``` 23 | 24 | ### Add a link to your file to the `_quarto.yml` config file in `/docs` 25 | 26 | Open the `_quarto.yml` configuration file and find the `sidebar` config code. 27 | ```yaml 28 | sidebar: 29 | - id: nav 30 | style: "floating" 31 | collapse-level: 3 32 | align: left 33 | contents: 34 | ``` 35 | Under the `contents:` object add a new section (if required), a string lable for your page, and a link to the `.qmd` file itself. 36 | 37 | ```yaml 38 | - section: "tutorials" 39 | contents: 40 | - text: "Reproducible Analytical Pipelines" 41 | file: tutorials/intro-to-rap.qmd 42 | ``` 43 | 44 | ### Publish your changes to GitHub 45 | 46 | ::: {.callout-warning appearance="simple" collapse="false"} 47 | ### Render Quarto before publishing 48 | 49 | Remember to [render your changes locally using R-Studio](https://quarto.org/docs/tools/rstudio.html#render-and-preview) (or VScode) before publishing 50 | ::: 51 | 52 | Commit your changes locally 53 | 54 | ```bash 55 | git commit -m 'Added new page to docs' 56 | ``` 57 | 58 | Then push your changes to the remote branch 59 | 60 | ```bash 61 | git push origin 62 | ``` 63 | 64 | Finally, open a Pull Request (PR) [https://github.com/nhs-pycom/nhspy-plotthedots/pulls](https://github.com/nhs-pycom/nhspy-plotthedots/pulls) -------------------------------------------------------------------------------- /docs/_site/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://nhs-pycom.github.io/nhspy-plotthedots/documentation/index.html 5 | 2023-01-22T21:57:06.314Z 6 | 7 | 8 | https://nhs-pycom.github.io/nhspy-plotthedots/posts/welcome/index.html 9 | 2023-01-22T20:57:04.441Z 10 | 11 | 12 | https://nhs-pycom.github.io/nhspy-plotthedots/posts/rap-maturity/index.html 13 | 2023-01-22T20:57:05.331Z 14 | 15 | 16 | https://nhs-pycom.github.io/nhspy-plotthedots/contribute.html 17 | 2023-01-22T21:38:35.503Z 18 | 19 | 20 | https://nhs-pycom.github.io/nhspy-plotthedots/index.html 21 | 2023-01-23T17:09:00.688Z 22 | 23 | 24 | https://nhs-pycom.github.io/nhspy-plotthedots/about.html 25 | 2023-01-22T20:57:10.103Z 26 | 27 | 28 | https://nhs-pycom.github.io/nhspy-plotthedots/updates.html 29 | 2023-01-22T20:57:10.394Z 30 | 31 | 32 | https://nhs-pycom.github.io/nhspy-plotthedots/faq.html 33 | 2023-01-22T21:57:06.585Z 34 | 35 | 36 | https://nhs-pycom.github.io/nhspy-plotthedots/tutorials/index.html 37 | 2023-01-23T17:08:00.386Z 38 | 39 | 40 | https://nhs-pycom.github.io/nhspy-plotthedots/documentation/pandas_spc_x_calc.html 41 | 2023-01-22T21:57:06.054Z 42 | 43 | 44 | https://nhs-pycom.github.io/nhspy-plotthedots/tutorials/pandas.html 45 | 2023-01-22T21:57:06.831Z 46 | 47 | 48 | -------------------------------------------------------------------------------- /tests/unittests/test_part_of_two_in_three.py: -------------------------------------------------------------------------------- 1 | # Python source 2 | # ------------------------------------------------------------------------- 3 | # Copyright (c) 2023 NHS Python Community. All rights reserved. 4 | # Licensed under the MIT License. See license.txt in the project root for 5 | # license information. 6 | # ------------------------------------------------------------------------- 7 | 8 | # FILE: test_part_of_two_in_three.py 9 | # DESCRIPTION: Tests for the part_of_two_in_three() function. 10 | # part_of_two_in_three(), given two boolean lists, uses zip() 11 | # to iterate over the two input lists and applies logical 12 | # AND to corresponding elements. 13 | 14 | # CONTRIBUTORS: v.Morriss 15 | # CONTACT: - 16 | # CREATED: 9 Jul 2024 17 | # VERSION: 0.0.1 18 | 19 | 20 | # Imports 21 | # ------------------------------------------------------------------------- 22 | # Python: 23 | import unittest 24 | 25 | # Local 26 | from nhspy_plotthedots.pandas_spc_calculations import part_of_two_in_three 27 | 28 | # Define tests 29 | # ------------------------------------------------------------------------- 30 | 31 | class PartOfTwoInThree(unittest.TestCase): 32 | 33 | def false(self): 34 | values1 = [False] * 10 35 | values2 = [False] * 10 36 | expected = [False] * 10 37 | self.assertEqual(part_of_two_in_three(values1, values2), expected) 38 | 39 | def true(self): 40 | values1 = [True] * 10 41 | values2 = [True] * 10 42 | expected = [True] * 10 43 | self.assertEqual(part_of_two_in_three(values1, values2), expected) 44 | 45 | def mixed(self): 46 | values1 = [False, True, False, True] 47 | values2 = [False, False, True, True] 48 | expected = [False, False, False, True] 49 | self.assertEqual(part_of_two_in_three(values1, values2), expected) 50 | 51 | def test_null_input(self): 52 | values1 = [] 53 | values2 = [] 54 | expected = [] 55 | self.assertEqual(part_of_two_in_three(values1, values2), expected) 56 | 57 | def left_uneven(self): 58 | values1 = [True, True, True] 59 | values2 = [True] 60 | expected = [True] 61 | self.assertEqual(part_of_two_in_three(values1, values2), expected) 62 | 63 | def right_uneven(self): 64 | values1 = [True] 65 | values2 = [True,True,True] 66 | expected = [True] 67 | self.assertEqual(part_of_two_in_three(values1, values2), expected) 68 | 69 | if __name__ == '__main__': 70 | unittest.main() 71 | -------------------------------------------------------------------------------- /tests/unittests/test_seven_point_one_side_mean.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------- 2 | # Copyright (c) 2023 NHS Python Community. All rights reserved. 3 | # Licensed under the MIT License. See license.txt in the project root for 4 | # license information. 5 | # ------------------------------------------------------------------------- 6 | 7 | # FILE: test_seven_point_one_side_mean.py 8 | 9 | # DESCRIPTION: Tests for the seven_point_one_side_mean() function. 10 | # Given a list of floats representing each entries' 11 | # relation to the mean (1.0 = above, 0.0 = mean, -1.0 = below), 12 | # it returns a boolean list with each entry representing if a 13 | # number is the seventh number above/below the mean. 14 | # 15 | # CONTRIBUTORS: v.Morriss 16 | # CONTACT: - 17 | # CREATED: 9 Jul 2024 18 | # VERSION: 0.0.1 19 | 20 | # Imports 21 | # ------------------------------------------------------------------------- 22 | # Python: 23 | import unittest 24 | 25 | # 3rd party: 26 | 27 | # Local 28 | from nhspy_plotthedots.pandas_spc_calculations import seven_point_one_side_mean 29 | 30 | # Define tests 31 | # ------------------------------------------------------------------------- 32 | 33 | 34 | class TestSevenPointOneSideMean(unittest.TestCase): 35 | 36 | def test_above(self): 37 | values = [1] * 7 38 | expected = [False] * 6 + [True] 39 | self.assertEqual(seven_point_one_side_mean(values), expected) 40 | 41 | def test_below(self): 42 | values = [-1] * 7 43 | expected = [False] * 6 + [True] 44 | self.assertEqual(seven_point_one_side_mean(values), expected) 45 | 46 | def test_equal(self): 47 | values = [-1,0,1,0,-1,0,1] 48 | expected = [False] * 7 49 | self.assertEqual(seven_point_one_side_mean(values), expected) 50 | 51 | def test_zero(self): 52 | values = [0] * 7 53 | expected = [True] * 7 54 | self.assertEqual(seven_point_one_side_mean(values), expected) 55 | 56 | def test_small_input(self): 57 | values = [1, 0, -1] 58 | expected = [0, 0, 0] 59 | self.assertEqual(seven_point_one_side_mean(values), expected) 60 | 61 | def test_large_input(self): 62 | values = [1] * 10 + [-1] * 10 63 | expected = [False] * 6 + [True] * 4 + [False] * 6 + [True] * 4 64 | self.assertEqual(seven_point_one_side_mean(values), expected) 65 | 66 | def test_invalid(self): 67 | values = list(range(-10,10,1)) 68 | expected = [False] * 20 69 | self.assertEqual(seven_point_one_side_mean(values), expected) 70 | 71 | def test_null(self): 72 | values = [] 73 | expected = [] 74 | self.assertEqual(seven_point_one_side_mean(values), expected) 75 | 76 | if __name__ == '__main__': 77 | unittest.main() -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | pull_request: 16 | branches: [ "main" ] 17 | 18 | jobs: 19 | analyze: 20 | name: Analyze 21 | runs-on: ubuntu-latest 22 | permissions: 23 | actions: read 24 | contents: read 25 | security-events: write 26 | 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | language: [ 'python' ] 31 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 32 | # Use only 'java' to analyze code written in Java, Kotlin or both 33 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 34 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v3 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v2 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | 49 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 50 | # queries: security-extended,security-and-quality 51 | 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v2 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 60 | 61 | # If the Autobuild fails above, remove it and uncomment the following three lines. 62 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 63 | 64 | # - run: | 65 | # echo "Run, Build Application using script" 66 | # ./location_of_script_within_repo/buildscript.sh 67 | 68 | - name: Perform CodeQL Analysis 69 | uses: github/codeql-action/analyze@v2 70 | with: 71 | category: "/language:${{matrix.language}}" 72 | -------------------------------------------------------------------------------- /docs/tutorials/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Get Started" 3 | subtitle: "A quick tutorial to learn the basics." 4 | comments: false 5 | anchor-sections: false 6 | --- 7 | 8 | ::::: {.grid .step .column-page-right} 9 | 10 | :::: {.g-col-lg-2 .g-col-12} 11 | ## Step 1 12 | 13 | #### Install {.fw-light} 14 | :::: 15 | 16 | :::: {.g-col-lg-7 .g-col-12} 17 | 18 | ## Download the code locally 19 | 20 | ```{python} 21 | #| output: false 22 | 23 | # Install libs from the test Python Package Index (pypi) repo 24 | %pip install --index-url https://test.pypi.org/simple/ --no-deps nhspy-plotthedots-test 25 | 26 | # Import plotthedots to your environment 27 | from nhspy_plotthedots import pandas_spc_calculations 28 | from nhspy_plotthedots import plotly_spc_chart 29 | ``` 30 | 31 | :::: 32 | 33 | ::::: 34 | 35 | ::::: {.grid .step .column-page-right} 36 | 37 | :::: {.g-col-lg-2 .g-col-12} 38 | ## Step 2 39 | 40 | #### Process Data {.fw-light} 41 | :::: 42 | 43 | :::: {.g-col-lg-7 .g-col-12} 44 | 45 | ## Load time-series data into `pandas` 46 | 47 | ```{python} 48 | import pandas as pd 49 | from datetime import datetime 50 | 51 | # url path of the CSV file 52 | url = 'https://raw.githubusercontent.com/nhs-pycom/nhspy-plotthedots/main/nhspy_plotthedots/data/ae_attendances.csv' 53 | 54 | # Read the CSV file and store it in a pandas DataFrame 55 | df = pd.read_csv(url) 56 | 57 | # Convert 'period' column to datetime format 58 | df['period'] = pd.to_datetime(df['period']) 59 | 60 | # Create a subset of the DataFrame based on certain conditions 61 | sub_set = df[(df['org_code'] == "RQM") & (df['type'] == "1") & (df['period'] < datetime(2018, 4, 1))] 62 | 63 | # It's important to sort values by date for time-series and reset index for spc calculations 64 | sub_set = sub_set.sort_values(by='period').reset_index(drop=True) 65 | sub_set.head() 66 | ``` 67 | 68 | :::: 69 | 70 | ::::: 71 | 72 | ::::: {.grid .step .column-page-right} 73 | 74 | :::: {.g-col-lg-2 .g-col-12} 75 | ## Step 3 76 | 77 | #### Calculation {.fw-light} 78 | :::: 79 | 80 | :::: {.g-col-lg-7 .g-col-12} 81 | 82 | ## Calculate the control limits 83 | ```{python} 84 | # calculate control limits 85 | spc = pandas_spc_calculations.pandas_spc_x_calc(sub_set, 'breaches') 86 | spc.head() 87 | ``` 88 | 89 | :::: 90 | 91 | ::::: 92 | 93 | ::::: {.grid .step .column-page-right} 94 | 95 | :::: {.g-col-lg-2 .g-col-12} 96 | ## Step 4 97 | 98 | #### Visualisation {.fw-light} 99 | :::: 100 | 101 | :::: {.g-col-lg-7 .g-col-12} 102 | 103 | ## Plot the statistical process control chart 104 | ```{python} 105 | #| label: fig-4-hour-breach1 106 | #| fig-cap: Number of A&E attendance 4-Hour Breaches 107 | #| fig-align: center 108 | 109 | import plotly.io as pio 110 | pio.renderers.default = "plotly_mimetype+notebook" 111 | 112 | # plot SPC chart 113 | plotly_spc_chart.plotly_spc_chart(spc, 'breaches', 'period', plot_title = 'Chelsea & Westminster Hospital NHS FT', x_lab = 'Month of attendance', y_lab = 'Number of 4-Hour Target Breaches') 114 | ``` 115 | 116 | :::: 117 | 118 | ::::: -------------------------------------------------------------------------------- /docs/posts/rap-maturity/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "RAP Maturity Framework" 3 | author: "Craig R Shenton" 4 | date: "2023-01-23" 5 | categories: [NHS, RAP] 6 | image: "nhsd-rap.png" 7 | description: "Lets make nhspy-plotthedots fully RAP complient" 8 | title-block-banner: false 9 | --- 10 | 11 | ## The 'Levels of RAP' Maturity Framework 12 | 13 | We are going to be developing `nhspy-plotthedots` in a Reproducible Analytical Pipeline (RAP) way by following the maturity framework developed by [NHS Digital RAP community](https://nhsdigital.github.io/rap-community-of-practice/introduction_to_RAP/levels_of_RAP/). 14 | 15 | There are three levels to RAP: 16 | 17 | 1. Baseline - **RAP fundamentals** offering resilience against future change. 18 | 2. Silver - **Implementing best practice** by following good analytical and software engineering standards. 19 | 3. Gold - **Analysis as a product** to further elevate your analytical work and enhance its reusability to the public. 20 | 21 | ## Baseline RAP - getting the fundamentals right 22 | 23 | In order for a publication to be considered a reproducible analytical pipeline, it must at least meet all of the requirements of Baseline RAP: 24 | 25 | - [x] Data produced by code in an open-source language (e.g., Python, R, SQL). 26 | - [x] Code is version controlled (i.e., Git & GitHub). 27 | - [ ] Repository includes a README.md file (or equivalent) that clearly details steps a user must follow to reproduce the code. 28 | - [ ] Code has been peer reviewed (i.e., use PRs and code reviews) 29 | - [x] Code is published in the open and linked to & from accompanying publication (if relevant). 30 | 31 | ## Silver RAP - implementing best practice 32 | 33 | _Meeting all of the above requirements, plus:_ 34 | 35 | - [x] Outputs are produced by code with minimal manual intervention. 36 | - [ ] Code is well-documented including user guidance, explanation of code structure & methodology and docstrings for functions. 37 | - [x] Code is well-organised following standard directory format. 38 | - [x] Reusable functions and/or classes are used where appropriate. 39 | - [ ] Code adheres to agreed coding standards (e.g., PEP8). 40 | - [ ] Pipeline includes a testing framework (unit tests, back tests). 41 | - [x] Repository includes package dependency information. 42 | - [ ] Logs are automatically recorded by the pipeline to ensure outputs are as expected. 43 | - [ ] Data is handled and output in a [Tidy data format](https://medium.com/@kimrodrikwa/untidy-data-a90b6e3ebe4c). 44 | 45 | ## Gold RAP - analysis as a product 46 | 47 | _Meeting all of the above requirements, plus:_ 48 | 49 | - [x] Code is fully packaged. 50 | - [x] Repository automatically runs tests etc. via [CI](https://github.com/skills/continuous-integration)/CD or a different integration/deployment tool e.g. [GitHub Actions](https://docs.github.com/en/actions). 51 | - [ ] Process runs based on event-based triggers (e.g., new data in database) or on a schedule. 52 | - [ ] Changes to the RAP are clearly signposted. E.g. a changelog in the package, releases etc. (See gov.uk info on [Semantic Versioning](https://github.com/alphagov/govuk-frontend/blob/main/docs/contributing/versioning.md)) 53 | 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # GENERIC GITIGNORE FILE FOR PYTHON 3 | # Copied from https://github.com/github/gitignore/blob/master/Python.gitignore 4 | # This is a useful thing to just copy and paste since it should work for most use cases. 5 | # If you have additional things to exclude from the .gitignore file you should add it above 6 | # this section. 7 | ##################################################################### 8 | 9 | # Mac OS 10 | .DS_Store 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .nox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | *.py,cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | cover/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | db.sqlite3 73 | db.sqlite3-journal 74 | 75 | # Flask stuff: 76 | instance/ 77 | .webassets-cache 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | .pybuilder/ 87 | target/ 88 | 89 | # Jupyter Notebook 90 | .ipynb_checkpoints 91 | 92 | # IPython 93 | profile_default/ 94 | ipython_config.py 95 | 96 | # pyenv 97 | # For a library or package, you might want to ignore these files since the code is 98 | # intended to run in multiple environments; otherwise, check them in: 99 | # .python-version 100 | 101 | # pipenv 102 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 103 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 104 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 105 | # install all needed dependencies. 106 | #Pipfile.lock 107 | 108 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 109 | __pypackages__/ 110 | 111 | # Celery stuff 112 | celerybeat-schedule 113 | celerybeat.pid 114 | 115 | # SageMath parsed files 116 | *.sage.py 117 | 118 | # Environments 119 | .env 120 | .venv 121 | env/ 122 | venv/ 123 | ENV/ 124 | env.bak/ 125 | venv.bak/ 126 | 127 | # Spyder project settings 128 | .spyderproject 129 | .spyproject 130 | 131 | # Rope project settings 132 | .ropeproject 133 | 134 | # mkdocs documentation 135 | /site 136 | 137 | # mypy 138 | .mypy_cache/ 139 | .dmypy.json 140 | dmypy.json 141 | 142 | # Pyre type checker 143 | .pyre/ 144 | 145 | # pytype static type analyzer 146 | .pytype/ 147 | 148 | # Cython debug symbols 149 | cython_debug/ -------------------------------------------------------------------------------- /tests/unittests/test_two_in_three.py: -------------------------------------------------------------------------------- 1 | # Python source 2 | # ------------------------------------------------------------------------- 3 | # Copyright (c) 2023 NHS Python Community. All rights reserved. 4 | # Licensed under the MIT License. See license.txt in the project root for 5 | # license information. 6 | # ------------------------------------------------------------------------- 7 | 8 | # FILE: test_two_in_three.py 9 | # DESCRIPTION: Tests for the two_in_three() function. 10 | # two_in_three() checks if there is a trend of 3 elements 11 | # where at least 2 elements are close to limits and sum of 12 | # relative to mean is 3. 13 | 14 | # CONTRIBUTORS: v.Morriss 15 | # CONTACT: - 16 | # CREATED: 9 Jul 2024 17 | # VERSION: 0.0.1 18 | 19 | # Imports 20 | # ------------------------------------------------------------------------- 21 | # Python: 22 | import unittest 23 | 24 | # Local 25 | from nhspy_plotthedots.pandas_spc_calculations import two_in_three 26 | 27 | # Define tests 28 | # ------------------------------------------------------------------------- 29 | class TestTwoInThree(unittest.TestCase): 30 | 31 | def test_large(self): 32 | bool_values = [True] * 81 33 | values = [ 34 | -1,-1,-1,-1,-1,0,-1,-1,1,-1,0,-1,-1,0,0,-1,0,1,-1,1,-1,-1,1,0,-1,1,1, 35 | 0,-1,-1,0,-1,0,0,-1,1,0,0,-1,0,0,0,0,0,1,0,1,-1,0,1,0,0,1,1, 36 | 1,-1,-1,1,-1,0,1,-1,1,1,0,-1,1,0,0,1,0,1,1,1,-1,1,1,0,1,1,1 37 | ] 38 | expected = [True] * 5 + [False] * 47 + [True] * 3 + [False] * 16 + [True] * 3 + [False] * 4 + [True] * 3 39 | self.assertEqual(two_in_three(bool_values, values), expected) 40 | 41 | def test_negative(self): 42 | bool_values = [True] * 3 43 | values = [-1,-1,-1] 44 | expected = [True] * 3 45 | self.assertEqual(two_in_three(bool_values, values), expected) 46 | 47 | def test_relative_to_mean(self): 48 | bool_values = [True] * 3 49 | values = [1,-1,1] 50 | expected = [False] * 3 51 | self.assertEqual(two_in_three(bool_values, values), expected) 52 | 53 | def test_close_to_limits(self): 54 | bool_values = [False] * 3 55 | values = [1] * 3 56 | expected = [False] * 3 57 | self.assertEqual(two_in_three(bool_values, values), expected) 58 | 59 | def test_unequal_sizes(self): 60 | bool_values = [True,False,True,False] 61 | values = [1,-1] 62 | expected = [False] * 4 63 | self.assertEqual(two_in_three(bool_values, values), expected) 64 | 65 | def test_null_boolean_input(self): 66 | bool_values = [] 67 | values = [1,1,1] 68 | expected = [] 69 | self.assertEqual(two_in_three(bool_values, values), expected) 70 | 71 | def test_null_value_input(self): 72 | bool_values = [True, False] 73 | values = [] 74 | expected = [False, False] 75 | self.assertEqual(two_in_three(bool_values, values), expected) 76 | 77 | def test_null_input(self): 78 | bool_values = [] 79 | values = [] 80 | expected = [] 81 | self.assertEqual(two_in_three(bool_values, values), expected) 82 | 83 | 84 | if __name__ == '__main__': 85 | unittest.main() -------------------------------------------------------------------------------- /docs/_site/site_libs/quarto-html/quarto-syntax-highlighting.css: -------------------------------------------------------------------------------- 1 | /* quarto syntax highlight colors */ 2 | :root { 3 | --quarto-hl-ot-color: #003B4F; 4 | --quarto-hl-at-color: #657422; 5 | --quarto-hl-ss-color: #20794D; 6 | --quarto-hl-an-color: #5E5E5E; 7 | --quarto-hl-fu-color: #4758AB; 8 | --quarto-hl-st-color: #20794D; 9 | --quarto-hl-cf-color: #003B4F; 10 | --quarto-hl-op-color: #5E5E5E; 11 | --quarto-hl-er-color: #AD0000; 12 | --quarto-hl-bn-color: #AD0000; 13 | --quarto-hl-al-color: #AD0000; 14 | --quarto-hl-va-color: #111111; 15 | --quarto-hl-bu-color: inherit; 16 | --quarto-hl-ex-color: inherit; 17 | --quarto-hl-pp-color: #AD0000; 18 | --quarto-hl-in-color: #5E5E5E; 19 | --quarto-hl-vs-color: #20794D; 20 | --quarto-hl-wa-color: #5E5E5E; 21 | --quarto-hl-do-color: #5E5E5E; 22 | --quarto-hl-im-color: #00769E; 23 | --quarto-hl-ch-color: #20794D; 24 | --quarto-hl-dt-color: #AD0000; 25 | --quarto-hl-fl-color: #AD0000; 26 | --quarto-hl-co-color: #5E5E5E; 27 | --quarto-hl-cv-color: #5E5E5E; 28 | --quarto-hl-cn-color: #8f5902; 29 | --quarto-hl-sc-color: #5E5E5E; 30 | --quarto-hl-dv-color: #AD0000; 31 | --quarto-hl-kw-color: #003B4F; 32 | } 33 | 34 | /* other quarto variables */ 35 | :root { 36 | --quarto-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 37 | } 38 | 39 | pre > code.sourceCode > span { 40 | color: #003B4F; 41 | } 42 | 43 | code span { 44 | color: #003B4F; 45 | } 46 | 47 | code.sourceCode > span { 48 | color: #003B4F; 49 | } 50 | 51 | div.sourceCode, 52 | div.sourceCode pre.sourceCode { 53 | color: #003B4F; 54 | } 55 | 56 | code span.ot { 57 | color: #003B4F; 58 | } 59 | 60 | code span.at { 61 | color: #657422; 62 | } 63 | 64 | code span.ss { 65 | color: #20794D; 66 | } 67 | 68 | code span.an { 69 | color: #5E5E5E; 70 | } 71 | 72 | code span.fu { 73 | color: #4758AB; 74 | } 75 | 76 | code span.st { 77 | color: #20794D; 78 | } 79 | 80 | code span.cf { 81 | color: #003B4F; 82 | } 83 | 84 | code span.op { 85 | color: #5E5E5E; 86 | } 87 | 88 | code span.er { 89 | color: #AD0000; 90 | } 91 | 92 | code span.bn { 93 | color: #AD0000; 94 | } 95 | 96 | code span.al { 97 | color: #AD0000; 98 | } 99 | 100 | code span.va { 101 | color: #111111; 102 | } 103 | 104 | code span.pp { 105 | color: #AD0000; 106 | } 107 | 108 | code span.in { 109 | color: #5E5E5E; 110 | } 111 | 112 | code span.vs { 113 | color: #20794D; 114 | } 115 | 116 | code span.wa { 117 | color: #5E5E5E; 118 | font-style: italic; 119 | } 120 | 121 | code span.do { 122 | color: #5E5E5E; 123 | font-style: italic; 124 | } 125 | 126 | code span.im { 127 | color: #00769E; 128 | } 129 | 130 | code span.ch { 131 | color: #20794D; 132 | } 133 | 134 | code span.dt { 135 | color: #AD0000; 136 | } 137 | 138 | code span.fl { 139 | color: #AD0000; 140 | } 141 | 142 | code span.co { 143 | color: #5E5E5E; 144 | } 145 | 146 | code span.cv { 147 | color: #5E5E5E; 148 | font-style: italic; 149 | } 150 | 151 | code span.cn { 152 | color: #8f5902; 153 | } 154 | 155 | code span.sc { 156 | color: #5E5E5E; 157 | } 158 | 159 | code span.dv { 160 | color: #AD0000; 161 | } 162 | 163 | code span.kw { 164 | color: #003B4F; 165 | } 166 | 167 | .prevent-inlining { 168 | content: " namedtuple: 32 | """ 33 | Calculates the SPC for a given list of values. 34 | 35 | Parameters: 36 | - values (List[float]): The list of values for which SPC needs to be 37 | calculated. 38 | - fix_after_n_points (Optional[int]): The number of values after which 39 | the mean and other calculations should be fixed. 40 | 41 | Returns: 42 | namedtuple: A named tuple containing the following values: 43 | - values: The input values 44 | - mean: The mean of the input values 45 | - lpl: The lower process limit of the input values 46 | - upl: The upper process limit of the input values 47 | - outside_limits: A boolean list representing whether a value is 48 | outside the process limits 49 | - relative_to_mean: A list representing the relative value of an 50 | element to mean 51 | - close_to_limits: A boolean list representing whether a value is 52 | close to a limit or not 53 | - special_cause_flag: A boolean list representing whether a value 54 | is a special cause or not 55 | """ 56 | # If fix_after_n_points is provided, fix the mean and other calculations 57 | # to the first n points 58 | fix_values = values[:fix_after_n_points] if fix_after_n_points else values 59 | # constant limit value 60 | limit = 2.66 61 | 62 | mean = np.mean(fix_values) 63 | mr = np.abs(np.diff(fix_values)) 64 | amr = np.mean(mr) 65 | 66 | # screen for outliers 67 | mr = mr[mr < 3.267 * amr] 68 | amr = np.mean(mr) 69 | 70 | lpl = mean - (limit * amr) 71 | upl = mean + (limit * amr) 72 | 73 | # identify near lower/upper process limits 74 | nlpl = mean - (limit * 2 / 3 * amr) 75 | nupl = mean + (limit * 2 / 3 * amr) 76 | 77 | # identify any points which are outside the upper or lower process limits 78 | outside_limits = (values < lpl) | (values > upl) 79 | # identify whether a point is above or below the mean 80 | relative_to_mean = np.sign(values - mean) 81 | 82 | # identify if a point is between the near process limits and process limits 83 | close_to_limits = ~outside_limits & ((values < nlpl) | (values > nupl)) 84 | 85 | spc_return_type = namedtuple("spc_x", [ 86 | "values", 87 | "mean", 88 | "lpl", 89 | "upl", 90 | "outside_limits", 91 | "relative_to_mean", 92 | "close_to_limits", 93 | "special_cause_flag" 94 | ]) 95 | 96 | return spc_return_type( 97 | values, 98 | mean, 99 | lpl, 100 | upl, 101 | outside_limits, 102 | relative_to_mean, 103 | close_to_limits, 104 | special_cause_flag(values, outside_limits, close_to_limits, relative_to_mean) 105 | ) -------------------------------------------------------------------------------- /tests/unittests/test_special_cause_flag.py: -------------------------------------------------------------------------------- 1 | # Python source 2 | # ------------------------------------------------------------------------- 3 | # Copyright (c) 2023 NHS Python Community. All rights reserved. 4 | # Licensed under the MIT License. See license.txt in the project root for 5 | # license information. 6 | # ------------------------------------------------------------------------- 7 | 8 | # FILE: test_special_cause_flag.py 9 | 10 | # DESCRIPTION: Tests for the special_cause_flag() function. 11 | # special_cause_flag() checks if an element is a special 12 | # cause based on 4 conditions: 13 | # 1. It is outside the limits 14 | # 2. It is part of a trend of 7 elements where at least one element has an 15 | # absolute value of 1 16 | # 3. It is part of a trend of 7 elements where the sum of the relative value 17 | # of elements to mean is 1 18 | # 4. It is part of a trend of 3 elements where at least 2 elements are close 19 | # to limits and sum of relative to mean is 3 20 | 21 | # Parameters: 22 | # - values (List[float]): List of float numbers 23 | # - outside_limits (List[bool]): List of boolean values representing whether 24 | # an element is outside the limits or not 25 | # - close_to_limits (List[bool]): List of boolean values representing whether 26 | # an element is close to a limit or not 27 | # - relative_to_mean (List[float]): List of float numbers representing the 28 | # relative value of an element to mean 29 | 30 | # CONTRIBUTORS: v-Morriss 31 | # CONTACT: - 32 | # CREATED: 9 Jul 2023 33 | # VERSION: 0.0.1 34 | 35 | # Imports 36 | # ------------------------------------------------------------------------- 37 | # Python: 38 | import unittest 39 | 40 | # 3rd party: 41 | import numpy as np 42 | 43 | # Local 44 | from nhspy_plotthedots.pandas_spc_calculations import special_cause_flag 45 | 46 | # Define tests 47 | # ------------------------------------------------------------------------- 48 | class SpecialCaseFlag(unittest.TestCase): 49 | 50 | def test_outside_limits(self): 51 | values = np.array([0,0,0]) 52 | outside_limits = np.array([True, True, True]) # <- testing 53 | close_to_limits = np.array([False, False, False]) 54 | relative_to_mean = np.array([0,0,0]) 55 | 56 | expected = np.array([True, True, True]) 57 | answer = special_cause_flag(values, outside_limits, close_to_limits, relative_to_mean) 58 | self.assertTrue((answer == expected).all()) 59 | 60 | def test_sevent_point_mean(self): 61 | values = np.array([0] * 7) 62 | outside_limits = np.array([False] * 7) 63 | close_to_limits = np.array([False] * 7) 64 | relative_to_mean = np.array([1] * 7) # <- testing 65 | 66 | expected = np.array([True] * 7) 67 | answer = special_cause_flag(values, outside_limits, close_to_limits, relative_to_mean) 68 | self.assertTrue((answer == expected).all()) 69 | 70 | def test_values(self): 71 | values = np.array([1] * 7) # <- testing 72 | outside_limits = np.array([False] * 7) 73 | close_to_limits = np.array([False] * 7) 74 | relative_to_mean = np.array([0] * 7) 75 | 76 | expected = np.array([True] * 7) 77 | answer = special_cause_flag(values, outside_limits, close_to_limits, relative_to_mean) 78 | self.assertTrue((answer == expected).all()) 79 | 80 | def test_part_of_three(self): 81 | values = np.array([0] * 3) 82 | outside_limits = np.array([False] * 3) 83 | close_to_limits = np.array([True, True, True]) # <- testing 84 | relative_to_mean = np.array([1,1,1]) # <- testing 85 | expected = np.array([True] * 3) 86 | 87 | answer = special_cause_flag(values, outside_limits, close_to_limits, relative_to_mean) 88 | self.assertTrue((answer == expected).all()) 89 | 90 | if __name__ == '__main__': 91 | unittest.main() 92 | -------------------------------------------------------------------------------- /docs/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | pagetitle: "nhspy-plotthedots" 3 | css: index.css 4 | editor: source 5 | description: | 6 | Calculate control limits and draw statistical process control (SPC) charts in python. 7 | comments: false 8 | --- 9 | 10 | ```{python} 11 | #| echo: false 12 | #| output: false 13 | # Install libs from test PIPY 14 | %pip install --index-url https://test.pypi.org/simple/ --no-deps nhspy-plotthedots-test 15 | 16 | # Import libs 17 | import pandas as pd 18 | from datetime import datetime 19 | import plotly.io as pio 20 | pio.renderers.default = "plotly_mimetype+notebook" 21 | ``` 22 | 23 | ```{python} 24 | #| echo: false 25 | #| output: false 26 | # File path of the CSV file 27 | url = 'https://raw.githubusercontent.com/nhs-pycom/nhspy-plotthedots/main/nhspy_plotthedots/data/ae_attendances.csv' 28 | 29 | # Read the CSV file and store it in a DataFrame 30 | df = pd.read_csv(url) 31 | 32 | # Convert 'period' column to datetime format 33 | df['period'] = pd.to_datetime(df['period']) 34 | 35 | # Create a subset of the DataFrame based on certain conditions 36 | sub_set = df[(df['org_code'] == "RQM") & (df['type'] == "1") & (df['period'] < datetime(2018, 4, 1))] 37 | sub_set = sub_set.sort_values(by='period').reset_index(drop=True) 38 | ``` 39 | 40 | ```{python} 41 | #| echo: false 42 | #| output: false 43 | from nhspy_plotthedots import pandas_spc_calculations 44 | 45 | # calculate control limits 46 | spc = pandas_spc_calculations.pandas_spc_x_calc(sub_set, 'breaches') 47 | ``` 48 | 49 | :::: {.hero-banner} 50 | 51 | ::: {.hero-image .hero-image-left} 52 | ::: 53 | 54 | ::: {.content-block} 55 | 56 | # nhspy-plotthedots v0.1.6 (alpha) 57 | 58 | ### Calculate control limits and draw Statistical Process Control charts in python. 59 | 60 | ::: {.hero-buttons} 61 | [Quick Start](tutorials/index.qmd){.btn-action-primary .btn-action .btn .btn-success .btn-lg role="button"} 62 | 64 | ::: 65 | 66 | ::: 67 | 68 | ::: {.hero-image .hero-image-right} 69 | ::: 70 | 71 | :::: 72 |
73 | Welcome to the NHS Python Community’s python version of the NHS-Rplotthedots package. 74 | 75 | The [NHSR Plot the Dots](https://github.com/nhs-r-community/NHSRplotthedots) package was built by the [NHS-R community](https://nhsrcommunity.com) to provide tools for drawing statistical process control (SPC) charts. The package supports the NHS England programme ['Making Data Count'](https://www.england.nhs.uk/publication/making-data-count/). The programme encourages boards, managers, and analyst teams to present data in ways that show change over time, and drive better understanding of indicators than 'RAG' (red, amber, green) rated reports often present. 76 | 77 | ::: {.callout-note} 78 | ## Note 79 | Please be aware that this package is in the early stages of development, and features may change. 80 | ::: 81 | 82 | ## Statistical Process Control 83 | 84 | Statistical process control (SPC) is an analytical technique that plots data over time. It helps us understand variation and in so doing guides us to take the most appropriate action. 85 | 86 | SPC is a good technique to use when implementing change as it enables you to understand whether changes you are making are resulting in improvement — a key component of the Model for Improvement widely used within the NHS. 87 | 88 | ::: {.panel-tabset} 89 | ## Example `nhspy-plotthedots` Chart 90 | ```{python} 91 | #| echo: false 92 | from nhspy_plotthedots import plotly_spc_chart 93 | 94 | # plot SPC chart 95 | plotly_spc_chart.plotly_spc_chart(spc, 'breaches', 'period', plot_title = 'Chelsea & Westminster Hospital NHS FT', x_lab = 'Month of attendance', y_lab = 'Number of 4-Hour Target Breaches') 96 | ``` 97 | ::: 98 | 99 | ## NHS-R slack 100 | 101 | If you want to learn more about this project, please join the discussion at the [NHS-R Community Slack group](https://nhsrcommunity.slack.com/) and the specific channel [#proj-nhsr-plot-the-dots](https://nhsrcommunity.slack.com/archives/CSVD4SYF3). -------------------------------------------------------------------------------- /docs/_site/site_libs/quarto-html/anchor.min.js: -------------------------------------------------------------------------------- 1 | // @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat 2 | // 3 | // AnchorJS - v4.3.1 - 2021-04-17 4 | // https://www.bryanbraun.com/anchorjs/ 5 | // Copyright (c) 2021 Bryan Braun; Licensed MIT 6 | // 7 | // @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat 8 | !function(A,e){"use strict";"function"==typeof define&&define.amd?define([],e):"object"==typeof module&&module.exports?module.exports=e():(A.AnchorJS=e(),A.anchors=new A.AnchorJS)}(this,function(){"use strict";return function(A){function d(A){A.icon=Object.prototype.hasOwnProperty.call(A,"icon")?A.icon:"",A.visible=Object.prototype.hasOwnProperty.call(A,"visible")?A.visible:"hover",A.placement=Object.prototype.hasOwnProperty.call(A,"placement")?A.placement:"right",A.ariaLabel=Object.prototype.hasOwnProperty.call(A,"ariaLabel")?A.ariaLabel:"Anchor",A.class=Object.prototype.hasOwnProperty.call(A,"class")?A.class:"",A.base=Object.prototype.hasOwnProperty.call(A,"base")?A.base:"",A.truncate=Object.prototype.hasOwnProperty.call(A,"truncate")?Math.floor(A.truncate):64,A.titleText=Object.prototype.hasOwnProperty.call(A,"titleText")?A.titleText:""}function w(A){var e;if("string"==typeof A||A instanceof String)e=[].slice.call(document.querySelectorAll(A));else{if(!(Array.isArray(A)||A instanceof NodeList))throw new TypeError("The selector provided to AnchorJS was invalid.");e=[].slice.call(A)}return e}this.options=A||{},this.elements=[],d(this.options),this.isTouchDevice=function(){return Boolean("ontouchstart"in window||window.TouchEvent||window.DocumentTouch&&document instanceof DocumentTouch)},this.add=function(A){var e,t,o,i,n,s,a,c,r,l,h,u,p=[];if(d(this.options),"touch"===(l=this.options.visible)&&(l=this.isTouchDevice()?"always":"hover"),0===(e=w(A=A||"h2, h3, h4, h5, h6")).length)return this;for(null===document.head.querySelector("style.anchorjs")&&((u=document.createElement("style")).className="anchorjs",u.appendChild(document.createTextNode("")),void 0===(A=document.head.querySelector('[rel="stylesheet"],style'))?document.head.appendChild(u):document.head.insertBefore(u,A),u.sheet.insertRule(".anchorjs-link{opacity:0;text-decoration:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}",u.sheet.cssRules.length),u.sheet.insertRule(":hover>.anchorjs-link,.anchorjs-link:focus{opacity:1}",u.sheet.cssRules.length),u.sheet.insertRule("[data-anchorjs-icon]::after{content:attr(data-anchorjs-icon)}",u.sheet.cssRules.length),u.sheet.insertRule('@font-face{font-family:anchorjs-icons;src:url(data:n/a;base64,AAEAAAALAIAAAwAwT1MvMg8yG2cAAAE4AAAAYGNtYXDp3gC3AAABpAAAAExnYXNwAAAAEAAAA9wAAAAIZ2x5ZlQCcfwAAAH4AAABCGhlYWQHFvHyAAAAvAAAADZoaGVhBnACFwAAAPQAAAAkaG10eASAADEAAAGYAAAADGxvY2EACACEAAAB8AAAAAhtYXhwAAYAVwAAARgAAAAgbmFtZQGOH9cAAAMAAAAAunBvc3QAAwAAAAADvAAAACAAAQAAAAEAAHzE2p9fDzz1AAkEAAAAAADRecUWAAAAANQA6R8AAAAAAoACwAAAAAgAAgAAAAAAAAABAAADwP/AAAACgAAA/9MCrQABAAAAAAAAAAAAAAAAAAAAAwABAAAAAwBVAAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAMCQAGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAg//0DwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAAIAAAACgAAxAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADAAAAAIAAgAAgAAACDpy//9//8AAAAg6cv//f///+EWNwADAAEAAAAAAAAAAAAAAAAACACEAAEAAAAAAAAAAAAAAAAxAAACAAQARAKAAsAAKwBUAAABIiYnJjQ3NzY2MzIWFxYUBwcGIicmNDc3NjQnJiYjIgYHBwYUFxYUBwYGIwciJicmNDc3NjIXFhQHBwYUFxYWMzI2Nzc2NCcmNDc2MhcWFAcHBgYjARQGDAUtLXoWOR8fORYtLTgKGwoKCjgaGg0gEhIgDXoaGgkJBQwHdR85Fi0tOAobCgoKOBoaDSASEiANehoaCQkKGwotLXoWOR8BMwUFLYEuehYXFxYugC44CQkKGwo4GkoaDQ0NDXoaShoKGwoFBe8XFi6ALjgJCQobCjgaShoNDQ0NehpKGgobCgoKLYEuehYXAAAADACWAAEAAAAAAAEACAAAAAEAAAAAAAIAAwAIAAEAAAAAAAMACAAAAAEAAAAAAAQACAAAAAEAAAAAAAUAAQALAAEAAAAAAAYACAAAAAMAAQQJAAEAEAAMAAMAAQQJAAIABgAcAAMAAQQJAAMAEAAMAAMAAQQJAAQAEAAMAAMAAQQJAAUAAgAiAAMAAQQJAAYAEAAMYW5jaG9yanM0MDBAAGEAbgBjAGgAbwByAGoAcwA0ADAAMABAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAP) format("truetype")}',u.sheet.cssRules.length)),u=document.querySelectorAll("[id]"),t=[].map.call(u,function(A){return A.id}),i=0;i\]./()*\\\n\t\b\v\u00A0]/g,"-").replace(/-{2,}/g,"-").substring(0,this.options.truncate).replace(/^-+|-+$/gm,"").toLowerCase()},this.hasAnchorJSLink=function(A){var e=A.firstChild&&-1<(" "+A.firstChild.className+" ").indexOf(" anchorjs-link "),A=A.lastChild&&-1<(" "+A.lastChild.className+" ").indexOf(" anchorjs-link ");return e||A||!1}}}); 9 | // @license-end -------------------------------------------------------------------------------- /tests/unittests/test_pandas_spc_x_calc.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------- 2 | # Copyright (c) 2023 NHS Python Community. All rights reserved. 3 | # Licensed under the MIT License. See license.txt in the project root for 4 | # license information. 5 | # ------------------------------------------------------------------------- 6 | 7 | # FILE: test_pandas_spc_x_calc.py 8 | 9 | # DESCRIPTION: Tests on the pandas_scp_x_calc() function. Given a pandas DataFrame, 10 | # a string indicating the column name of the values to be analysed, 11 | # and an optional integer representing the number of values after which 12 | # the mean and other calculations should be fixed. It returns a pandas 13 | # DataFrame with the same values and the Statistic Process Control (SPC) values. 14 | # The SCP values const of: 15 | # - mean: The mean of the input values 16 | # - lpl: The lower process limit of the input values 17 | # - upl: The upper process limit of the input values 18 | # - outside_limits: A boolean list representing whether a value is 19 | # outside the process limits 20 | # - relative_to_mean: A list representing the relative value of an 21 | # element to mean 22 | # - close_to_limits: A boolean list representing whether a value is 23 | # close to a limit or not 24 | # - special_cause_flag: A boolean list representing whether a value 25 | # is a special cause or not 'outside_limits' representing whether a 26 | # value is outside the process limits 27 | # 28 | # CONTRIBUTORS: v.Morriss 29 | # CONTACT: - 30 | # CREATED: 9 Jul 2024 31 | # VERSION: 0.0.1 32 | 33 | # Imports 34 | # ------------------------------------------------------------------------- 35 | # Python: 36 | import unittest 37 | import math 38 | 39 | # 3rd party: 40 | import pandas as pd 41 | 42 | # Local 43 | from nhspy_plotthedots.pandas_spc_calculations import pandas_spc_x_calc 44 | 45 | # Define tests 46 | # ------------------------------------------------------------------------- 47 | 48 | class TestPandasSpcXCal(unittest.TestCase): 49 | 50 | def test_sample1(self): 51 | df = pd.DataFrame(data = {"column" : [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]}) 52 | col = "column" 53 | n_points = 3 54 | expected = pd.DataFrame(data = 55 | { 56 | "column" : [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], 57 | "mean" : [2.0] * 8, 58 | "lpl" : [-0.6600000000000001] * 8, 59 | "upl" : [4.66] * 8, 60 | "outside_limits" : [False] * 4 + [True] * 4, 61 | "relative_to_mean" : [-1.0, 0.0] + [1.0] * 6, 62 | "close_to_limits" : [False] * 3 + [True] + [False] * 4, 63 | "special_cause_flag" : [True] * 8, 64 | }) 65 | self.assertTrue(pandas_spc_x_calc(df,col,n_points).equals(expected)) 66 | 67 | def test_sample2(self): 68 | df = pd.DataFrame(data = {"column" : [16.0, 9.0, 4.0, 2.0, 1.0]}) 69 | col = "column" 70 | n_points = 11 71 | expected = pd.DataFrame(data = 72 | { 73 | "column" : [16.0, 9.0, 4.0, 2.0, 1.0], 74 | "mean" : [6.4] * 5, 75 | "lpl" : [-3.575000000000001] * 5, 76 | "upl" : [16.375] * 5, 77 | "outside_limits" : [False] * 5, 78 | "relative_to_mean" : [1.0] * 2 + [-1.0] * 3, 79 | "close_to_limits" : [True] + [False] * 4, 80 | "special_cause_flag" : [False] * 5, 81 | }) 82 | self.assertTrue(pandas_spc_x_calc(df,col,n_points).equals(expected)) 83 | 84 | def test_sample3(self): 85 | df = pd.DataFrame(data = {"column" : [1,6,1,8,0,3]}) 86 | col = "column" 87 | n_points = None 88 | expected = pd.DataFrame(data = 89 | { 90 | "column" : [1,6,1,8,0,3], 91 | "mean" : [3.1666666666666665] * 6, 92 | "lpl" : [-11.729333333333333] * 6, 93 | "upl" : [18.062666666666665] * 6, 94 | "outside_limits" : [False] * 6, 95 | "relative_to_mean" : [-1.0, 1.0, -1.0, 1.0, -1.0, -1.0], 96 | "close_to_limits" : [False] * 6, 97 | "special_cause_flag" : [False] * 6, 98 | }) 99 | self.assertTrue(pandas_spc_x_calc(df,col,n_points).equals(expected)) 100 | 101 | def test_nan(self): 102 | df = pd.DataFrame(data = {"column" : [math.nan]}) 103 | col = "column" 104 | n_points = None 105 | expected = pd.DataFrame(data = 106 | { 107 | "column" : [math.nan], 108 | "mean" : [math.nan], 109 | "lpl" : [math.nan], 110 | "upl" : [math.nan], 111 | "outside_limits" : [False], 112 | "relative_to_mean" : [math.nan], 113 | "close_to_limits" : [False], 114 | "special_cause_flag" : [False], 115 | }) 116 | self.assertTrue(pandas_spc_x_calc(df,col,n_points).equals(expected)) 117 | 118 | def test_special_cause(self): 119 | pd.set_option('display.max_columns', None) 120 | df = pd.DataFrame(data = {"column" : [1,1,1,1,2]}) 121 | col = "column" 122 | n_points = None 123 | expected = pd.DataFrame(data = 124 | { 125 | "column" : [1,1,1,1,2], 126 | "mean" : [1.2] * 5, 127 | "lpl" : [1.2] * 5, 128 | "upl" : [1.2] * 5, 129 | "outside_limits" : [True] * 5, 130 | "relative_to_mean" : [-1.0] * 4 + [1.0], 131 | "close_to_limits" : [False] * 5, 132 | "special_cause_flag" : [True] * 5, 133 | }) 134 | self.assertTrue(pandas_spc_x_calc(df,col,n_points).equals(expected)) 135 | 136 | if __name__ == '__main__': 137 | unittest.main() -------------------------------------------------------------------------------- /nhspy_plotthedots/plotly_spc_chart.py: -------------------------------------------------------------------------------- 1 | # Python source 2 | # ------------------------------------------------------------------------- 3 | # Copyright (c) 2023 NHS Python Community. All rights reserved. 4 | # Licensed under the MIT License. See license.txt in the project root for 5 | # license information. 6 | # ------------------------------------------------------------------------- 7 | 8 | # FILE: plotly_spc_chart.py 9 | 10 | # DESCRIPTION: plotly_spc_chart() function. 11 | 12 | # CONTRIBUTORS: Craig R. Shenton 13 | # CONTACT: craig.shenton@nhs.net 14 | # CREATED: 22 Jan 2023 15 | # VERSION: 0.0.1 16 | 17 | # Imports 18 | # ------------------------------------------------------------------------- 19 | # Python: 20 | from datetime import datetime 21 | from dateutil.relativedelta import relativedelta 22 | 23 | # 3rd party: 24 | import pandas as pd 25 | import plotly.graph_objects as go 26 | 27 | # Define get_status() 28 | # ------------------------------------------------------------------------- 29 | def get_status(row: pd.Series) -> str: 30 | """ 31 | Given a row of a dataframe, returns a string depending on the values 32 | of the 'outside_limits', 'close_to_limits' and 'relative_to_mean' 33 | columns of that row. 34 | 35 | :param row: A row of a dataframe 36 | :type row: pd.Series 37 | :return: A string indicating the status 38 | :rtype: str 39 | """ 40 | if row['outside_limits']: 41 | return 'Outside Limit' 42 | elif row['close_to_limits']: 43 | return 'Close to limit' 44 | elif row['relative_to_mean'] > 0: 45 | return 'Above mean' 46 | elif row['relative_to_mean'] < 0: 47 | return 'Below mean' 48 | else: 49 | return '' 50 | 51 | # Define get_colour() 52 | # ------------------------------------------------------------------------- 53 | def get_colour(row: pd.Series) -> str: 54 | """ 55 | Given a row of a dataframe, returns a string depending on the values 56 | of the 'outside_limits', 'close_to_limits' columns of that row. 57 | 58 | :param row: A row of a dataframe 59 | :type row: pd.Series 60 | :return: A string indicating the colour 61 | :rtype: str 62 | """ 63 | if row['outside_limits']: 64 | return 'red' 65 | elif row['close_to_limits']: 66 | return 'yellow' 67 | else: 68 | return 'rgb(22, 96, 167)' 69 | 70 | # Define plotly_spc_chart() 71 | # ------------------------------------------------------------------------- 72 | def plotly_spc_chart(df: pd.DataFrame, 73 | values_col: str, 74 | date_col: str, 75 | plot_title: str, 76 | x_lab: str, 77 | y_lab: str) -> None: 78 | """ 79 | This function creates a line chart using the specified dataframe, values 80 | column, date column, plot title, x-axis label, and y-axis label. The chart 81 | includes a scatter plot of the data points, a line plot of the mean, and 82 | shaded areas for the lower and upper control limits. 83 | 84 | Parameters: 85 | - df (pd.DataFrame): The dataframe to be plotted 86 | - values_col (str): The column name of the values to be plotted 87 | - date_col (str): The column name of the dates 88 | - plot_title (str): The title of the chart 89 | - x_lab (str): The label for the x-axis 90 | - y_lab (str): The label for the y-axis 91 | 92 | Returns: 93 | None 94 | """ 95 | # Create a scatter plot of the data points 96 | scatter = go.Scatter( 97 | x=df[date_col], 98 | y=df[values_col], 99 | name = 'Performance', 100 | mode='lines+markers', 101 | marker=dict( 102 | color=df.apply(lambda row: get_colour(row), axis=1), 103 | size=10, 104 | symbol='circle'), 105 | line = dict(color = 'rgb(22, 96, 167)', 106 | width = 3, dash = 'solid'), 107 | text = df.apply(lambda row: get_status(row), axis=1), 108 | hovertemplate = '%{text}: %{y:.0f}', 109 | ) 110 | # Create a line plot of the mean 111 | mean_line = go.Scatter( 112 | x=df[date_col], 113 | y=df['mean'], 114 | mode='lines', 115 | line = dict(color = 'rgba(174, 37, 115, 0.5)', 116 | width = 2, 117 | dash = 'dash'), 118 | name = "Mean", 119 | hovertemplate = 'mean: %{y:.0f}', 120 | ) 121 | # Create a shaded area for the lower and upper control limits 122 | lpl_area = go.Scatter( 123 | x=df[date_col], 124 | y=df['lpl'], 125 | mode='lines', 126 | line=dict( 127 | color='rgba(174, 37, 115, 0.1)', 128 | width=0, 129 | ), 130 | name = "lpl", 131 | hovertemplate = 'lpl: %{y:.0f}', 132 | ) 133 | upl_area = go.Scatter( 134 | x=df[date_col], 135 | y=df['upl'], 136 | mode='lines', 137 | line=dict( 138 | color='rgba(174, 37, 115, 0.1)', 139 | width=0, 140 | ), 141 | fill='tonexty', 142 | fillcolor='rgba(174, 37, 115, 0.1)', 143 | name = "upl", 144 | hovertemplate = 'upl: %{y:.0f}', 145 | ) 146 | # Set options 147 | min_xaxis = min(df[date_col]) 148 | max_xaxis = max(df[date_col]) 149 | max_yaxis = max(df[values_col]) 150 | remove = ['zoom2d','pan2d', 'select2d', 'lasso2d', 'zoomIn2d', 151 | 'zoomOut2d', 'autoScale2d', 'resetScale2d', 'zoom', 152 | 'pan', 'select', 'zoomIn', 'zoomOut', 'autoScale', 153 | 'resetScale', 'toggleSpikelines', 'hoverClosestCartesian', 154 | 'hoverCompareCartesian', 'toImage'] 155 | # Set layout 156 | layout = go.Layout(title = plot_title, 157 | font = dict(size = 12), 158 | xaxis = dict(title = x_lab, 159 | # add more time to x-axis to show plot circles 160 | range = [min_xaxis - relativedelta(days=5), 161 | max_xaxis + relativedelta(days=5)]), 162 | yaxis = dict(title = y_lab, 163 | # fix y0 at 0 and add 10% to y1 164 | range = [0, max_yaxis + (max_yaxis * 0.1)]), 165 | showlegend = False, 166 | hovermode = "x unified") 167 | # Set configuration 168 | config = {'displaylogo': False, 169 | 'displayModeBar': True, 170 | 'modeBarButtonsToRemove': remove} 171 | # Create the figure and show() 172 | fig = go.Figure(data=[scatter, mean_line, lpl_area, upl_area], layout=layout) 173 | fig.update_layout(template='plotly_white') 174 | fig.show(config=config) -------------------------------------------------------------------------------- /docs/_site/site_libs/quarto-nav/quarto-nav.js: -------------------------------------------------------------------------------- 1 | const headroomChanged = new CustomEvent("quarto-hrChanged", { 2 | detail: {}, 3 | bubbles: true, 4 | cancelable: false, 5 | composed: false, 6 | }); 7 | 8 | window.document.addEventListener("DOMContentLoaded", function () { 9 | let init = false; 10 | 11 | function throttle(func, wait) { 12 | var timeout; 13 | return function () { 14 | const context = this; 15 | const args = arguments; 16 | const later = function () { 17 | clearTimeout(timeout); 18 | timeout = null; 19 | func.apply(context, args); 20 | }; 21 | 22 | if (!timeout) { 23 | timeout = setTimeout(later, wait); 24 | } 25 | }; 26 | } 27 | 28 | function headerOffset() { 29 | // Set an offset if there is are fixed top navbar 30 | const headerEl = window.document.querySelector("header.fixed-top"); 31 | if (headerEl) { 32 | return headerEl.clientHeight; 33 | } else { 34 | return 0; 35 | } 36 | } 37 | 38 | function footerOffset() { 39 | const footerEl = window.document.querySelector("footer.footer"); 40 | if (footerEl) { 41 | return footerEl.clientHeight; 42 | } else { 43 | return 0; 44 | } 45 | } 46 | 47 | function updateDocumentOffsetWithoutAnimation() { 48 | updateDocumentOffset(false); 49 | } 50 | 51 | function updateDocumentOffset(animated) { 52 | // set body offset 53 | const topOffset = headerOffset(); 54 | const bodyOffset = topOffset + footerOffset(); 55 | const bodyEl = window.document.body; 56 | bodyEl.setAttribute("data-bs-offset", topOffset); 57 | bodyEl.style.paddingTop = topOffset + "px"; 58 | 59 | // deal with sidebar offsets 60 | const sidebars = window.document.querySelectorAll( 61 | ".sidebar, .headroom-target" 62 | ); 63 | sidebars.forEach((sidebar) => { 64 | if (!animated) { 65 | sidebar.classList.add("notransition"); 66 | // Remove the no transition class after the animation has time to complete 67 | setTimeout(function () { 68 | sidebar.classList.remove("notransition"); 69 | }, 201); 70 | } 71 | 72 | if (window.Headroom && sidebar.classList.contains("sidebar-unpinned")) { 73 | sidebar.style.top = "0"; 74 | sidebar.style.maxHeight = "100vh"; 75 | } else { 76 | sidebar.style.top = topOffset + "px"; 77 | sidebar.style.maxHeight = "calc(100vh - " + topOffset + "px)"; 78 | } 79 | }); 80 | 81 | // allow space for footer 82 | const mainContainer = window.document.querySelector(".quarto-container"); 83 | if (mainContainer) { 84 | mainContainer.style.minHeight = "calc(100vh - " + bodyOffset + "px)"; 85 | } 86 | 87 | // link offset 88 | let linkStyle = window.document.querySelector("#quarto-target-style"); 89 | if (!linkStyle) { 90 | linkStyle = window.document.createElement("style"); 91 | linkStyle.setAttribute("id", "quarto-target-style"); 92 | window.document.head.appendChild(linkStyle); 93 | } 94 | while (linkStyle.firstChild) { 95 | linkStyle.removeChild(linkStyle.firstChild); 96 | } 97 | if (topOffset > 0) { 98 | linkStyle.appendChild( 99 | window.document.createTextNode(` 100 | section:target::before { 101 | content: ""; 102 | display: block; 103 | height: ${topOffset}px; 104 | margin: -${topOffset}px 0 0; 105 | }`) 106 | ); 107 | } 108 | if (init) { 109 | window.dispatchEvent(headroomChanged); 110 | } 111 | init = true; 112 | } 113 | 114 | // initialize headroom 115 | var header = window.document.querySelector("#quarto-header"); 116 | if (header && window.Headroom) { 117 | const headroom = new window.Headroom(header, { 118 | tolerance: 5, 119 | onPin: function () { 120 | const sidebars = window.document.querySelectorAll( 121 | ".sidebar, .headroom-target" 122 | ); 123 | sidebars.forEach((sidebar) => { 124 | sidebar.classList.remove("sidebar-unpinned"); 125 | }); 126 | updateDocumentOffset(); 127 | }, 128 | onUnpin: function () { 129 | const sidebars = window.document.querySelectorAll( 130 | ".sidebar, .headroom-target" 131 | ); 132 | sidebars.forEach((sidebar) => { 133 | sidebar.classList.add("sidebar-unpinned"); 134 | }); 135 | updateDocumentOffset(); 136 | }, 137 | }); 138 | headroom.init(); 139 | 140 | let frozen = false; 141 | window.quartoToggleHeadroom = function () { 142 | if (frozen) { 143 | headroom.unfreeze(); 144 | frozen = false; 145 | } else { 146 | headroom.freeze(); 147 | frozen = true; 148 | } 149 | }; 150 | } 151 | 152 | // Observe size changed for the header 153 | const headerEl = window.document.querySelector("header.fixed-top"); 154 | if (headerEl && window.ResizeObserver) { 155 | const observer = new window.ResizeObserver( 156 | updateDocumentOffsetWithoutAnimation 157 | ); 158 | observer.observe(headerEl, { 159 | attributes: true, 160 | childList: true, 161 | characterData: true, 162 | }); 163 | } else { 164 | window.addEventListener( 165 | "resize", 166 | throttle(updateDocumentOffsetWithoutAnimation, 50) 167 | ); 168 | } 169 | setTimeout(updateDocumentOffsetWithoutAnimation, 250); 170 | 171 | // fixup index.html links if we aren't on the filesystem 172 | if (window.location.protocol !== "file:") { 173 | const links = window.document.querySelectorAll("a"); 174 | for (let i = 0; i < links.length; i++) { 175 | links[i].href = links[i].href.replace(/\/index\.html/, "/"); 176 | } 177 | 178 | // Fixup any sharing links that require urls 179 | // Append url to any sharing urls 180 | const sharingLinks = window.document.querySelectorAll( 181 | "a.sidebar-tools-main-item" 182 | ); 183 | for (let i = 0; i < sharingLinks.length; i++) { 184 | const sharingLink = sharingLinks[i]; 185 | const href = sharingLink.getAttribute("href"); 186 | if (href) { 187 | sharingLink.setAttribute( 188 | "href", 189 | href.replace("|url|", window.location.href) 190 | ); 191 | } 192 | } 193 | 194 | // Scroll the active navigation item into view, if necessary 195 | const navSidebar = window.document.querySelector("nav#quarto-sidebar"); 196 | if (navSidebar) { 197 | // Find the active item 198 | const activeItem = navSidebar.querySelector("li.sidebar-item a.active"); 199 | if (activeItem) { 200 | // Wait for the scroll height and height to resolve by observing size changes on the 201 | // nav element that is scrollable 202 | const resizeObserver = new ResizeObserver((_entries) => { 203 | // The bottom of the element 204 | const elBottom = activeItem.offsetTop; 205 | const viewBottom = navSidebar.scrollTop + navSidebar.clientHeight; 206 | 207 | // The element height and scroll height are the same, then we are still loading 208 | if (viewBottom !== navSidebar.scrollHeight) { 209 | // Determine if the item isn't visible and scroll to it 210 | if (elBottom >= viewBottom) { 211 | navSidebar.scrollTop = elBottom; 212 | } 213 | 214 | // stop observing now since we've completed the scroll 215 | resizeObserver.unobserve(navSidebar); 216 | } 217 | }); 218 | resizeObserver.observe(navSidebar); 219 | } 220 | } 221 | } 222 | }); 223 | -------------------------------------------------------------------------------- /docs/_site/updates.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | nhspy-plotthedots 9 | https://nhs-pycom.github.io/nhspy-plotthedots/updates.html 10 | 11 | Draw XmR charts in python for NHSE 'Making Data Count' programme. 12 | quarto-1.2.280 13 | Mon, 23 Jan 2023 00:00:00 GMT 14 | 15 | RAP Maturity Framework 16 | Craig R Shenton 17 | https://nhs-pycom.github.io/nhspy-plotthedots/posts/rap-maturity/index.html 18 | 23 |

The ‘Levels of RAP’ Maturity Framework

24 |

We are going to be developing nhspy-plotthedots in a Reproducible Analytical Pipeline (RAP) way by following the maturity framework developed by NHS Digital RAP community.

25 |

There are three levels to RAP:

26 |
    27 |
  1. Baseline - RAP fundamentals offering resilience against future change.
  2. 28 |
  3. Silver - Implementing best practice by following good analytical and software engineering standards.
  4. 29 |
  5. Gold - Analysis as a product to further elevate your analytical work and enhance its reusability to the public.
  6. 30 |
31 | 32 |
33 |

Baseline RAP - getting the fundamentals right

34 |

In order for a publication to be considered a reproducible analytical pipeline, it must at least meet all of the requirements of Baseline RAP:

35 |
    36 |
  • Data produced by code in an open-source language (e.g., Python, R, SQL).
  • 37 |
  • Code is version controlled (i.e., Git & GitHub).
  • 38 |
  • Repository includes a README.md file (or equivalent) that clearly details steps a user must follow to reproduce the code.
  • 39 |
  • Code has been peer reviewed (i.e., use PRs and code reviews)
  • 40 |
  • Code is published in the open and linked to & from accompanying publication (if relevant).
  • 41 |
42 |
43 |
44 |

Silver RAP - implementing best practice

45 |

Meeting all of the above requirements, plus:

46 |
    47 |
  • Outputs are produced by code with minimal manual intervention.
  • 48 |
  • Code is well-documented including user guidance, explanation of code structure & methodology and docstrings for functions.
  • 49 |
  • Code is well-organised following standard directory format.
  • 50 |
  • Reusable functions and/or classes are used where appropriate.
  • 51 |
  • Code adheres to agreed coding standards (e.g., PEP8).
  • 52 |
  • Pipeline includes a testing framework (unit tests, back tests).
  • 53 |
  • Repository includes package dependency information.
  • 54 |
  • Logs are automatically recorded by the pipeline to ensure outputs are as expected.
  • 55 |
  • Data is handled and output in a Tidy data format.
  • 56 |
57 |
58 |
59 |

Gold RAP - analysis as a product

60 |

Meeting all of the above requirements, plus:

61 |
    62 |
  • Code is fully packaged.
  • 63 |
  • Repository automatically runs tests etc. via CI/CD or a different integration/deployment tool e.g. GitHub Actions.
  • 64 |
  • Process runs based on event-based triggers (e.g., new data in database) or on a schedule.
  • 65 |
  • Changes to the RAP are clearly signposted. E.g. a changelog in the package, releases etc. (See gov.uk info on Semantic Versioning)
  • 66 |
67 | 68 | 69 |
70 | 71 | ]]>
72 | NHS 73 | RAP 74 | https://nhs-pycom.github.io/nhspy-plotthedots/posts/rap-maturity/index.html 75 | Mon, 23 Jan 2023 00:00:00 GMT 76 | 77 |
78 | 79 | nhspy-plotthedots alpha release 80 | Craig R Shenton 81 | https://nhs-pycom.github.io/nhspy-plotthedots/posts/welcome/index.html 82 | We are excited to announce the release of nhspy-plotthedots alpha, version 0.1.6. This new package is now available on test PyPI at https://test.pypi.org/project/nhspy-plotthedots-test/ for users to test and provide feedback.

87 |

In addition to the package release, the documentation site for nhspy-plotthedots is now live. This quarto site will provide detailed information on how to use the package, including installation instructions, usage examples, and API reference.

88 |

We look forward to hearing your feedback on nhspy-plotthedots and hope you find it as useful as we do.

89 |
90 |
91 |
92 | 93 |
94 |
95 | Note 96 |
97 |
98 |
99 |

This is an alpha release for testing, not for use in production.

100 |
101 |
102 | 103 | 104 | 105 | ]]>
106 | release 107 | https://nhs-pycom.github.io/nhspy-plotthedots/posts/welcome/index.html 108 | Sun, 22 Jan 2023 00:00:00 GMT 109 | 110 |
111 |
112 |
113 | -------------------------------------------------------------------------------- /docs/_site/site_libs/quarto-listing/quarto-listing.js: -------------------------------------------------------------------------------- 1 | const kProgressiveAttr = "data-src"; 2 | let categoriesLoaded = false; 3 | 4 | window.quartoListingCategory = (category) => { 5 | if (categoriesLoaded) { 6 | activateCategory(category); 7 | setCategoryHash(category); 8 | } 9 | }; 10 | 11 | window["quarto-listing-loaded"] = () => { 12 | // Process any existing hash 13 | const hash = getHash(); 14 | 15 | if (hash) { 16 | // If there is a category, switch to that 17 | if (hash.category) { 18 | activateCategory(hash.category); 19 | } 20 | // Paginate a specific listing 21 | const listingIds = Object.keys(window["quarto-listings"]); 22 | for (const listingId of listingIds) { 23 | const page = hash[getListingPageKey(listingId)]; 24 | if (page) { 25 | showPage(listingId, page); 26 | } 27 | } 28 | } 29 | 30 | const listingIds = Object.keys(window["quarto-listings"]); 31 | for (const listingId of listingIds) { 32 | // The actual list 33 | const list = window["quarto-listings"][listingId]; 34 | 35 | // Update the handlers for pagination events 36 | refreshPaginationHandlers(listingId); 37 | 38 | // Render any visible items that need it 39 | renderVisibleProgressiveImages(list); 40 | 41 | // Whenever the list is updated, we also need to 42 | // attach handlers to the new pagination elements 43 | // and refresh any newly visible items. 44 | list.on("updated", function () { 45 | renderVisibleProgressiveImages(list); 46 | setTimeout(() => refreshPaginationHandlers(listingId)); 47 | 48 | // Show or hide the no matching message 49 | toggleNoMatchingMessage(list); 50 | }); 51 | } 52 | }; 53 | 54 | window.document.addEventListener("DOMContentLoaded", function (_event) { 55 | // Attach click handlers to categories 56 | const categoryEls = window.document.querySelectorAll( 57 | ".quarto-listing-category .category" 58 | ); 59 | 60 | for (const categoryEl of categoryEls) { 61 | const category = categoryEl.getAttribute("data-category"); 62 | categoryEl.onclick = () => { 63 | activateCategory(category); 64 | setCategoryHash(category); 65 | }; 66 | } 67 | 68 | // Attach a click handler to the category title 69 | // (there should be only one, but since it is a class name, handle N) 70 | const categoryTitleEls = window.document.querySelectorAll( 71 | ".quarto-listing-category-title" 72 | ); 73 | for (const categoryTitleEl of categoryTitleEls) { 74 | categoryTitleEl.onclick = () => { 75 | activateCategory(""); 76 | setCategoryHash(""); 77 | }; 78 | } 79 | 80 | categoriesLoaded = true; 81 | }); 82 | 83 | function toggleNoMatchingMessage(list) { 84 | const selector = `#${list.listContainer.id} .listing-no-matching`; 85 | const noMatchingEl = window.document.querySelector(selector); 86 | if (noMatchingEl) { 87 | if (list.visibleItems.length === 0) { 88 | noMatchingEl.classList.remove("d-none"); 89 | } else { 90 | if (!noMatchingEl.classList.contains("d-none")) { 91 | noMatchingEl.classList.add("d-none"); 92 | } 93 | } 94 | } 95 | } 96 | 97 | function setCategoryHash(category) { 98 | setHash({ category }); 99 | } 100 | 101 | function setPageHash(listingId, page) { 102 | const currentHash = getHash() || {}; 103 | currentHash[getListingPageKey(listingId)] = page; 104 | setHash(currentHash); 105 | } 106 | 107 | function getListingPageKey(listingId) { 108 | return `${listingId}-page`; 109 | } 110 | 111 | function refreshPaginationHandlers(listingId) { 112 | const listingEl = window.document.getElementById(listingId); 113 | const paginationEls = listingEl.querySelectorAll( 114 | ".pagination li.page-item:not(.disabled) .page.page-link" 115 | ); 116 | for (const paginationEl of paginationEls) { 117 | paginationEl.onclick = (sender) => { 118 | setPageHash(listingId, sender.target.getAttribute("data-i")); 119 | showPage(listingId, sender.target.getAttribute("data-i")); 120 | return false; 121 | }; 122 | } 123 | } 124 | 125 | function renderVisibleProgressiveImages(list) { 126 | // Run through the visible items and render any progressive images 127 | for (const item of list.visibleItems) { 128 | const itemEl = item.elm; 129 | if (itemEl) { 130 | const progressiveImgs = itemEl.querySelectorAll( 131 | `img[${kProgressiveAttr}]` 132 | ); 133 | for (const progressiveImg of progressiveImgs) { 134 | const srcValue = progressiveImg.getAttribute(kProgressiveAttr); 135 | if (srcValue) { 136 | progressiveImg.setAttribute("src", srcValue); 137 | } 138 | progressiveImg.removeAttribute(kProgressiveAttr); 139 | } 140 | } 141 | } 142 | } 143 | 144 | function getHash() { 145 | // Hashes are of the form 146 | // #name:value|name1:value1|name2:value2 147 | const currentUrl = new URL(window.location); 148 | const hashRaw = currentUrl.hash ? currentUrl.hash.slice(1) : undefined; 149 | return parseHash(hashRaw); 150 | } 151 | 152 | const kAnd = "&"; 153 | const kEquals = "="; 154 | 155 | function parseHash(hash) { 156 | if (!hash) { 157 | return undefined; 158 | } 159 | const hasValuesStrs = hash.split(kAnd); 160 | const hashValues = hasValuesStrs 161 | .map((hashValueStr) => { 162 | const vals = hashValueStr.split(kEquals); 163 | if (vals.length === 2) { 164 | return { name: vals[0], value: vals[1] }; 165 | } else { 166 | return undefined; 167 | } 168 | }) 169 | .filter((value) => { 170 | return value !== undefined; 171 | }); 172 | 173 | const hashObj = {}; 174 | hashValues.forEach((hashValue) => { 175 | hashObj[hashValue.name] = decodeURIComponent(hashValue.value); 176 | }); 177 | return hashObj; 178 | } 179 | 180 | function makeHash(obj) { 181 | return Object.keys(obj) 182 | .map((key) => { 183 | return `${key}${kEquals}${obj[key]}`; 184 | }) 185 | .join(kAnd); 186 | } 187 | 188 | function setHash(obj) { 189 | const hash = makeHash(obj); 190 | window.history.pushState(null, null, `#${hash}`); 191 | } 192 | 193 | function showPage(listingId, page) { 194 | const list = window["quarto-listings"][listingId]; 195 | if (list) { 196 | list.show((page - 1) * list.page + 1, list.page); 197 | } 198 | } 199 | 200 | function activateCategory(category) { 201 | // Deactivate existing categories 202 | const activeEls = window.document.querySelectorAll( 203 | ".quarto-listing-category .category.active" 204 | ); 205 | for (const activeEl of activeEls) { 206 | activeEl.classList.remove("active"); 207 | } 208 | 209 | // Activate this category 210 | const categoryEl = window.document.querySelector( 211 | `.quarto-listing-category .category[data-category='${category}'` 212 | ); 213 | if (categoryEl) { 214 | categoryEl.classList.add("active"); 215 | } 216 | 217 | // Filter the listings to this category 218 | filterListingCategory(category); 219 | } 220 | 221 | function filterListingCategory(category) { 222 | const listingIds = Object.keys(window["quarto-listings"]); 223 | for (const listingId of listingIds) { 224 | const list = window["quarto-listings"][listingId]; 225 | if (list) { 226 | if (category === "") { 227 | // resets the filter 228 | list.filter(); 229 | } else { 230 | // filter to this category 231 | list.filter(function (item) { 232 | const itemValues = item.values(); 233 | if (itemValues.categories !== null) { 234 | const categories = itemValues.categories.split(","); 235 | return categories.includes(category); 236 | } else { 237 | return false; 238 | } 239 | }); 240 | } 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nhspy-plotthedots v0.1.0 {alpha} 2 | 3 | 4 | 11 | 12 | [![pytest+flake8](https://github.com/nhs-pycom/nhspy-plotthedots/actions/workflows/pytest_flake8.yml/badge.svg)](https://github.com/nhs-pycom/nhspy-plotthedots/actions/workflows/pytest_flake8.yml) 13 | [![codecov](https://codecov.io/gh/nhs-pycom/nhspy-plotthedots/branch/main/graph/badge.svg?token=WJUC4OBRLM)](https://codecov.io/gh/nhs-pycom/nhspy-plotthedots) 14 | [![Contributors][contributors-shield]][contributors-url] 15 | [![Code Lines][code-lines]][code-lines-url] 16 | [![Forks][forks-shield]][forks-url] 17 | [![Stargazers][stars-shield]][stars-url] 18 | [![Issues][issues-shield]][issues-url] 19 | [![MIT License][license-shield]][license-url] 20 | 21 | 22 |
23 |

24 | 25 | Logo 26 | 27 | 28 |

nhspy-plotthedots v0.1.0 {alpha}

29 | 30 |

31 | NHS Python Community 32 |
33 | Explore the docs » 34 |
35 | Report Bug 36 | · 37 | Request Feature 38 |

39 |

40 | 41 | 42 |
43 |

Table of Contents

44 |
    45 |
  1. 46 | About The Project 47 | 50 |
  2. 51 |
  3. 52 | Getting Started 53 | 57 |
  4. 58 |
  5. Usage
  6. 59 |
  7. Roadmap
  8. 60 |
  9. Contributing
  10. 61 |
  11. License
  12. 62 |
  13. Contact
  14. 63 |
  15. Acknowledgements
  16. 64 |
65 |
66 | 67 | 68 | 69 | ## About The Project 70 | 71 | ### Note: This is an alpha project, not for use in production until v1.0.0 72 | 73 | We are working with the NHS-R community to develop a python implementation of 'NHSRplotthedots' SPC package to support NHSE/I 'Making Data Count' programme 74 | 75 | Base Code by [Tom Jemmett](https://github.com/tomjemmett) 76 | 77 | - [https://gist.github.com/tomjemmett/c167376e5b6464ec1c00975be2d7864e](https://gist.github.com/tomjemmett/c167376e5b6464ec1c00975be2d7864e) 78 | 79 | - [Package Template](https://github.com/NHSDigital/rap-package-template) from NHS Digital's [RAP Community of Practice](https://nhsdigital.github.io/rap-community-of-practice/) 80 | 81 | _**Note:** No private or patient data are shared in this repository._ 82 | 83 | ### Folder Stucture 84 | 85 | | Name | Link | Description | 86 | | ---------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------- | 87 | | .github/workflows | [[Link](https://github.com/nhs-pycom/nhspy-plotthedots/tree/main/.github/workflows)] | Azure Data Factory notebooks for running ephemeral job clusters | 88 | | nhspy-plotthedots | [[Link](https://github.com/nhs-pycom/nhspy-plotthedots/tree/main/nhspy_plotthedots)] | Project folder | 89 | | ~/data | [[Link](https://github.com/nhs-pycom/nhspy-plotthedots/tree/main/nhspy_plotthedots/data)] | Example datasets | 90 | | ~/dev | [[Link](https://github.com/nhs-pycom/nhspy-plotthedots/tree/main/nhspy_plotthedots/dev)] | For testing new code | 91 | | ~/utilities | [[Link](https://github.com/nhs-pycom/nhspy-plotthedots/tree/main/nhspy_plotthedots/utilities)] | Helper functions | 92 | | tests/unittests | [[Link](https://github.com/nhs-pycom/nhspy-plotthedots/tree/main/tests/unittests)] | Unit testing framework | 93 | 94 | ### Built With 95 | 96 | - [Python 3](https://www.python.org/) 97 | 98 | 99 | 100 | ## Getting Started 101 | 102 | To get up and running follow these simple steps. 103 | 104 | ### Installation 105 | 106 | ```bash 107 | pip install --index-url https://test.pypi.org/simple/ --no-deps nhspy-plotthedots-test 108 | ``` 109 | 110 | Note this is the the test version of pypi, not for use in production. 111 | 112 | 113 | ## Usage 114 | 115 | Please refer to our [Read the Docs](https://nhs-pycom.github.io/nhspy-plotthedots/) site 116 | 117 | 118 | 119 | ## Roadmap 120 | 121 | See the [open issues](https://github.com/nhs-pycom/nhspy-plotthedots/issues) for a list of proposed features (and known issues). 122 | 123 | 124 | 125 | ## Contributing 126 | 127 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 128 | 129 | 1. Fork the Project 130 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 131 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 132 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 133 | 5. Open a Pull Request 134 | 135 | _See [CONTRIBUTING.md](https://github.com/nhs-pycom/nhspy-plotthedots/blob/main/CONTRIBUTING.md) for detailed guidance._ 136 | 137 | 138 | 139 | ## License 140 | 141 | Distributed under the MIT License. _See [LICENSE.md](https://github.com/nhs-pycom/nhspy-plotthedots/blob/main/LICENSE) for more information._ 142 | 143 | 144 | 145 | ## About 146 | 147 | Project contact email: [craig.shenton@nhs.net](mailto:craig.shenton@nhs.net) 148 | 149 | ## Acknowledgements 150 | 151 | - [Tom Jemmett](https://github.com/tomjemmett) 152 | - [NHS-R Community](https://nhsrcommunity.com/) 153 | 154 | 155 | 156 | 157 | [contributors-shield]: https://img.shields.io/github/contributors/nhs-pycom/nhspy-plotthedots.svg?color=blue 158 | [contributors-url]: https://github.com/nhs-pycom/nhspy-plotthedots/graphs/contributors 159 | [forks-shield]: https://img.shields.io/github/forks/nhs-pycom/nhspy-plotthedots.svg?color=blue 160 | [forks-url]: https://github.com/nhs-pycom/nhspy-plotthedots/network/members 161 | [stars-shield]: https://img.shields.io/github/stars/nhs-pycom/nhspy-plotthedots.svg?color=blue 162 | [stars-url]: https://github.com/nhs-pycom/nhspy-plotthedots/stargazers 163 | [issues-shield]: https://img.shields.io/github/issues/nhs-pycom/nhspy-plotthedots.svg?color=blue 164 | [issues-url]: https://github.com/nhs-pycom/nhspy-plotthedots/issues 165 | [license-shield]: https://img.shields.io/github/license/nhs-pycom/nhspy-plotthedots.svg?color=blue 166 | [license-url]: https://github.com/nhs-pycom/nhspy-plotthedots/blob/main/LICENSE 167 | [code-lines]: https://img.shields.io/tokei/lines/github/nhs-pycom/nhspy-plotthedots?color=blue&label=Code%20Lines 168 | [code-lines-url]: https://github.com/nhs-pycom/nhspy-plotthedots 169 | -------------------------------------------------------------------------------- /docs/_freeze/site_libs/clipboard/clipboard.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * clipboard.js v2.0.10 3 | * https://clipboardjs.com/ 4 | * 5 | * Licensed MIT © Zeno Rocha 6 | */ 7 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return o}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),c=n.n(e);function a(t){try{return document.execCommand(t)}catch(t){return}}var f=function(t){t=c()(t);return a("cut"),t};var l=function(t){var e,n,o,r=1.nav-link { 159 | border: none; 160 | border-bottom: 2px solid #39729E !important; 161 | color: #39729E; 162 | background-color: transparent; 163 | 164 | } 165 | 166 | .hello-quarto-banner .nav-pills button { 167 | width: 125px; 168 | } 169 | 170 | .hello-quarto .tab-content { 171 | border: none; 172 | padding: 0; 173 | color: rgb(84, 85, 85); 174 | } 175 | 176 | .hello-quarto .tab-content p { 177 | font-size: 1.1em; 178 | margin-bottom: 1.5em; 179 | } 180 | 181 | .hello-quarto div.sourceCode { 182 | background-color: white; 183 | border: 1px solid #dee2e6; 184 | } 185 | 186 | .hello-output { 187 | background-color: white; 188 | border: 1px solid #dee2e6; 189 | max-height: 660px; 190 | } 191 | 192 | .features { 193 | padding-bottom: 2em; 194 | } 195 | 196 | .feature { 197 | margin-top: 20px; 198 | } 199 | 200 | @media (min-width: 800px) { 201 | .features { 202 | display: flex; 203 | flex-direction: row; 204 | flex-wrap: wrap; 205 | margin: 0 0 0 -30px; 206 | width: calc(100% + 30px); 207 | } 208 | .feature { 209 | width: calc(33% - 30px); 210 | margin: 20px 0 0 30px; 211 | } 212 | } 213 | 214 | .feature h3 { 215 | margin-top: 0; 216 | } 217 | 218 | .feature p:first-of-type { 219 | margin-bottom: 0.2rem; 220 | color: rgb(84, 85, 85); 221 | } 222 | 223 | .get-started { 224 | text-align: center; 225 | padding-bottom: 2rem; 226 | } 227 | 228 | .get-started h3 { 229 | margin-top: 1rem; 230 | margin-bottom: 2rem; 231 | } 232 | 233 | nav.page-navigation { 234 | display: none; 235 | } 236 | 237 | .nav-footer { 238 | border-top: none !important; 239 | } 240 | 241 | .nav-pills .nav-link.active, .nav-pills .show>.nav-link { 242 | color: #fff; 243 | background-color: #ff7518; 244 | } 245 | 246 | 247 | .navbar-brand-container { 248 | margin-right: 0; 249 | } 250 | 251 | 252 | @media (min-width: 1020px) { 253 | .navbar-brand-container { 254 | margin-right: 1em; 255 | } 256 | } 257 | 258 | 259 | 260 | @media (max-width: 1060px) and (min-width: 991.98px) { 261 | 262 | #navbarCollapse ul:last-of-type a.nav-link { 263 | padding-left: .25em; 264 | padding-right: .25em; 265 | } 266 | 267 | .navbar #quarto-search { 268 | margin-left: .1em; 269 | } 270 | 271 | .navbar .bi-twitter, 272 | .navbar .bi-github, 273 | .navbar .bi-rss 274 | { 275 | font-size: .8em; 276 | } 277 | } 278 | 279 | #quarto-header { 280 | border-bottom: 1px solid #dee2e6; 281 | background-color: #005eb8; 282 | } 283 | 284 | @media (min-width: 991.98px) { 285 | #quarto-header { 286 | border-bottom: 1px solid #dee2e6; 287 | background-color: #005eb8; 288 | } 289 | } 290 | 291 | .navbar-brand > img { 292 | max-height: 36px; 293 | } 294 | 295 | 296 | .platform-table td { 297 | vertical-align: middle; 298 | } 299 | 300 | .platform-table td > div.sourceCode { 301 | margin-top: 0.3rem; 302 | margin-bottom: 0.3rem; 303 | } 304 | 305 | 306 | .document-example { 307 | opacity: 0.9; 308 | padding: 6px; 309 | font-weight: 500; 310 | margin-bottom: 1rem; 311 | } 312 | 313 | .document-example div { 314 | padding: 5px; 315 | } 316 | 317 | 318 | .document-example .citation { 319 | color: blue; 320 | } 321 | 322 | .trademark { 323 | font-size: 0.6rem; 324 | display: inline-block; 325 | margin-left: -3px; 326 | } 327 | 328 | .search-attribution { 329 | margin-top: 20px; 330 | padding-bottom: 20px; 331 | height: 40px; 332 | } 333 | 334 | .download-button { 335 | margin-top: 1em; 336 | } 337 | 338 | .download-table { 339 | margin-bottom: 2em; 340 | } 341 | 342 | .download-table p { 343 | margin-bottom: 0; 344 | } 345 | 346 | .download-table .checksum { 347 | color: var(--bs-primary); 348 | font-size: .775em; 349 | cursor: pointer; 350 | padding-top: 4px; 351 | } 352 | 353 | .download-button { 354 | display:flex; 355 | padding-bottom: 10px; 356 | padding-top: 10px; 357 | } 358 | 359 | .download-button .secondary { 360 | font-size: .775em; 361 | margin-bottom: 0; 362 | } 363 | 364 | .download-button .container { 365 | display: flex; 366 | padding-left: 10px; 367 | padding-right: 40px; 368 | } 369 | 370 | .download-button .icon-container { 371 | fill: white; 372 | width: 30px; 373 | margin-right: 15px; 374 | } 375 | 376 | iframe.reveal-demo { 377 | width: 100%; 378 | height: 350px; 379 | outline: none; 380 | } 381 | 382 | 383 | .slide-deck { 384 | border: 3px solid #dee2e6; 385 | width: 100%; 386 | height: 475px; 387 | } 388 | 389 | @media only screen and (max-width: 600px) { 390 | .slide-deck { 391 | height: 400px; 392 | } 393 | } 394 | 395 | 396 | @media (max-width: 575px) { 397 | 398 | .link-cards .card { 399 | margin-bottom: 20px; 400 | margin-right: 35px; 401 | } 402 | 403 | } 404 | 405 | @media (min-width: 576px) { 406 | .link-cards { 407 | display: flex; 408 | flex-direction: row; 409 | flex-wrap: wrap; 410 | } 411 | 412 | .link-cards .card { 413 | width: 190px; 414 | margin: 0 20px 12px 0; 415 | } 416 | 417 | 418 | } 419 | 420 | 421 | .link-cards .card { 422 | border: none; 423 | padding: 0; 424 | } 425 | 426 | .link-cards .card-title h4 { 427 | margin-top: 0; 428 | } 429 | 430 | .link-cards .card-title p { 431 | margin-bottom: 0; 432 | } 433 | 434 | .link-cards .card-subtitle { 435 | margin-bottom: 0.7rem; 436 | } 437 | 438 | .link-cards .card-body { 439 | padding: 0.5rem; 440 | padding-left: 0.1rem; 441 | } 442 | 443 | .link-cards .card-body ul { 444 | margin-bottom: 0; 445 | padding-left: 0; 446 | list-style-type: none; 447 | } 448 | 449 | .link-cards .card-body ul a { 450 | text-decoration: none; 451 | } 452 | 453 | .link-cards .card-body ul li { 454 | padding-bottom: 0.2rem; 455 | } 456 | 457 | 458 | .card .source-code { 459 | margin-top: 3px; 460 | } 461 | 462 | .carousel.card { 463 | font-size: 16px; 464 | padding-top: 2em; 465 | } 466 | 467 | .carousel.card a { 468 | text-decoration: none; 469 | } 470 | 471 | .carousel img { 472 | width: 70%; 473 | margin-bottom: 110px; 474 | } 475 | 476 | .carousel .carousel-control-prev-icon, 477 | .carousel .carousel-control-next-icon { 478 | margin-bottom: 110px; 479 | } 480 | 481 | 482 | .gallery-category { 483 | column-gap: 10px; 484 | } 485 | 486 | .btn-action-primary { 487 | color: white; 488 | background-color: #005eb8 !important; 489 | } 490 | 491 | .btn-action-primary:hover { 492 | background-color: #ffeb3b !important; 493 | color: #000 !important; 494 | } 495 | 496 | .btn-action { 497 | min-width: 165px; 498 | border-radius: 30px; 499 | border: none; 500 | } 501 | 502 | .panel-tabset[data-group="tools-tabset"] .choose-your-tool { 503 | max-width: 90px; 504 | margin-right: 25px; 505 | margin-top: 30px; 506 | font-weight: 300; 507 | font-size: 1.3rem; 508 | text-align: left; 509 | vertical-align: center; 510 | } 511 | 512 | .panel-tabset[data-group="tools-tabset"] .tab-content { 513 | border: none; 514 | padding-left: 5px; 515 | } 516 | 517 | .panel-tabset[data-group="tools-tabset"] .nav-tabs { 518 | border-bottom: none; 519 | } 520 | 521 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link { 522 | text-align: center; 523 | margin-right: 10px; 524 | margin-top: 10px; 525 | color: inherit; 526 | width: 102px; 527 | font-size: 0.8em; 528 | } 529 | 530 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link, 531 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link.active, 532 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-item.show .nav-link { 533 | border: 1px solid rgb(222, 226, 230); 534 | border-radius: 10px; 535 | } 536 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link:hover { 537 | border-color: rgb(80,146,221); 538 | border-width: 1px; 539 | } 540 | 541 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link.active, 542 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-item.show .nav-link { 543 | border-color: rgb(80,146,221); 544 | border-width: 2px; 545 | } 546 | 547 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link img { 548 | width: 65px; 549 | height: 65px; 550 | display: block; 551 | margin-bottom: 2px; 552 | } 553 | 554 | /* 555 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link { 556 | text-align: center; 557 | margin-right: 10px; 558 | margin-top: 10px; 559 | color: inherit; 560 | width: 102px; 561 | font-size: 0.8em; 562 | } 563 | 564 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link img { 565 | width: 45px; 566 | height: 45px; 567 | margin-left: 10px; 568 | display: block; 569 | margin-bottom: 2px; 570 | } 571 | */ 572 | 573 | 574 | .download-text { 575 | font-size: 1.1em; 576 | font-weight: 500; 577 | } 578 | 579 | .footnotes-end-of-document { 580 | display: none; 581 | } -------------------------------------------------------------------------------- /docs/_site/index.css: -------------------------------------------------------------------------------- 1 | .content-block { 2 | padding-top: 20px; 3 | padding-bottom: 10px; 4 | margin-left: 30px; 5 | margin-right: 30px; 6 | } 7 | 8 | 9 | @media(min-width: 900px) { 10 | .content-block { 11 | margin-left: 50px; 12 | margin-right: 50px; 13 | } 14 | } 15 | 16 | @media (min-width: 1400px) { 17 | .content-block { 18 | max-width: 1280px; 19 | margin-left: auto; 20 | margin-right: auto; 21 | } 22 | .navbar { 23 | max-width: 1350px; 24 | left: 50%; 25 | transform: translateX(-50%); 26 | } 27 | .nav-footer { 28 | position: relative; 29 | max-width: 1350px; 30 | left: 50%; 31 | transform: translateX(-50%); 32 | } 33 | } 34 | 35 | .hero-banner { 36 | position: relative; 37 | background-color: #f0f4f5; 38 | display: flex; 39 | justify-content: center; 40 | } 41 | 42 | .hero-banner h1 { 43 | color: #005EB8; 44 | font-size: 2.8rem; 45 | } 46 | 47 | .hero-banner h1 span { 48 | font-size: 1.6rem; 49 | } 50 | 51 | 52 | .hero-banner .hero-image { 53 | position: absolute; 54 | display: none; 55 | height: auto; 56 | } 57 | 58 | @media (min-width: 1000px) { 59 | .hero-banner .hero-image { 60 | display: initial; 61 | width: 270px; 62 | } 63 | } 64 | 65 | @media (min-width: 1200px) { 66 | .hero-banner .hero-image { 67 | width: 340px; 68 | } 69 | } 70 | 71 | @media (min-width: 1400px) { 72 | .hero-banner .hero-image { 73 | width: 440px; 74 | } 75 | } 76 | 77 | .hero-banner .hero-image p { 78 | margin-bottom: 0; 79 | } 80 | 81 | .hero-banner .hero-image-left { 82 | left: 0; 83 | bottom: 0; 84 | } 85 | 86 | .hero-banner .hero-image-right { 87 | right: 0; 88 | bottom: 0; 89 | } 90 | 91 | 92 | .hero-banner .content-block { 93 | max-width: 600px; 94 | z-index: 2; 95 | } 96 | 97 | .hero-banner a { 98 | text-decoration: none; 99 | } 100 | 101 | .hero-banner h3 { 102 | margin-top: 1.3rem; 103 | margin-bottom: 1.3rem; 104 | } 105 | 106 | .hero-banner h4 { 107 | margin-top: 0; 108 | } 109 | 110 | .hero-banner a[role="button"] { 111 | margin-right: 17px; 112 | margin-top: 0.6rem; 113 | margin-bottom: 1.6rem; 114 | } 115 | 116 | 117 | .hero-banner #btn-guide { 118 | background-color: #959595 !important; 119 | border: none; 120 | } 121 | 122 | .hero-banner ul { 123 | padding-inline-start: 21px; 124 | font-size: 1.1rem; 125 | } 126 | 127 | .hero-banner ul li { 128 | padding-bottom: 0.4rem; 129 | } 130 | 131 | 132 | .alt-background { 133 | background-color: rgb(247,249,251); 134 | border-top: 1px solid #dee2e6; 135 | border-bottom: 1px solid #dee2e6; 136 | } 137 | 138 | .hello-quarto { 139 | padding-bottom: 1rem; 140 | } 141 | 142 | @media (min-width: 600px) { 143 | .hello-quarto-banner { 144 | display: inline-flex; 145 | align-content: center; 146 | justify-content: center; 147 | } 148 | 149 | .hello-quarto-banner h1 { 150 | margin-right: 40px; 151 | } 152 | 153 | .hello-quarto-banner ul { 154 | 155 | } 156 | } 157 | 158 | .hello-quarto-banner .nav-pills .nav-link.active, .nav-pills .show>.nav-link { 159 | border: none; 160 | border-bottom: 2px solid #39729E !important; 161 | color: #39729E; 162 | background-color: transparent; 163 | 164 | } 165 | 166 | .hello-quarto-banner .nav-pills button { 167 | width: 125px; 168 | } 169 | 170 | .hello-quarto .tab-content { 171 | border: none; 172 | padding: 0; 173 | color: rgb(84, 85, 85); 174 | } 175 | 176 | .hello-quarto .tab-content p { 177 | font-size: 1.1em; 178 | margin-bottom: 1.5em; 179 | } 180 | 181 | .hello-quarto div.sourceCode { 182 | background-color: white; 183 | border: 1px solid #dee2e6; 184 | } 185 | 186 | .hello-output { 187 | background-color: white; 188 | border: 1px solid #dee2e6; 189 | max-height: 660px; 190 | } 191 | 192 | .features { 193 | padding-bottom: 2em; 194 | } 195 | 196 | .feature { 197 | margin-top: 20px; 198 | } 199 | 200 | @media (min-width: 800px) { 201 | .features { 202 | display: flex; 203 | flex-direction: row; 204 | flex-wrap: wrap; 205 | margin: 0 0 0 -30px; 206 | width: calc(100% + 30px); 207 | } 208 | .feature { 209 | width: calc(33% - 30px); 210 | margin: 20px 0 0 30px; 211 | } 212 | } 213 | 214 | .feature h3 { 215 | margin-top: 0; 216 | } 217 | 218 | .feature p:first-of-type { 219 | margin-bottom: 0.2rem; 220 | color: rgb(84, 85, 85); 221 | } 222 | 223 | .get-started { 224 | text-align: center; 225 | padding-bottom: 2rem; 226 | } 227 | 228 | .get-started h3 { 229 | margin-top: 1rem; 230 | margin-bottom: 2rem; 231 | } 232 | 233 | nav.page-navigation { 234 | display: none; 235 | } 236 | 237 | .nav-footer { 238 | border-top: none !important; 239 | } 240 | 241 | .nav-pills .nav-link.active, .nav-pills .show>.nav-link { 242 | color: #fff; 243 | background-color: #ff7518; 244 | } 245 | 246 | 247 | .navbar-brand-container { 248 | margin-right: 0; 249 | } 250 | 251 | 252 | @media (min-width: 1020px) { 253 | .navbar-brand-container { 254 | margin-right: 1em; 255 | } 256 | } 257 | 258 | 259 | 260 | @media (max-width: 1060px) and (min-width: 991.98px) { 261 | 262 | #navbarCollapse ul:last-of-type a.nav-link { 263 | padding-left: .25em; 264 | padding-right: .25em; 265 | } 266 | 267 | .navbar #quarto-search { 268 | margin-left: .1em; 269 | } 270 | 271 | .navbar .bi-twitter, 272 | .navbar .bi-github, 273 | .navbar .bi-rss 274 | { 275 | font-size: .8em; 276 | } 277 | } 278 | 279 | #quarto-header { 280 | border-bottom: 1px solid #dee2e6; 281 | background-color: #005eb8; 282 | } 283 | 284 | @media (min-width: 991.98px) { 285 | #quarto-header { 286 | border-bottom: 1px solid #dee2e6; 287 | background-color: #005eb8; 288 | } 289 | } 290 | 291 | .navbar-brand > img { 292 | max-height: 36px; 293 | } 294 | 295 | 296 | .platform-table td { 297 | vertical-align: middle; 298 | } 299 | 300 | .platform-table td > div.sourceCode { 301 | margin-top: 0.3rem; 302 | margin-bottom: 0.3rem; 303 | } 304 | 305 | 306 | .document-example { 307 | opacity: 0.9; 308 | padding: 6px; 309 | font-weight: 500; 310 | margin-bottom: 1rem; 311 | } 312 | 313 | .document-example div { 314 | padding: 5px; 315 | } 316 | 317 | 318 | .document-example .citation { 319 | color: blue; 320 | } 321 | 322 | .trademark { 323 | font-size: 0.6rem; 324 | display: inline-block; 325 | margin-left: -3px; 326 | } 327 | 328 | .search-attribution { 329 | margin-top: 20px; 330 | padding-bottom: 20px; 331 | height: 40px; 332 | } 333 | 334 | .download-button { 335 | margin-top: 1em; 336 | } 337 | 338 | .download-table { 339 | margin-bottom: 2em; 340 | } 341 | 342 | .download-table p { 343 | margin-bottom: 0; 344 | } 345 | 346 | .download-table .checksum { 347 | color: var(--bs-primary); 348 | font-size: .775em; 349 | cursor: pointer; 350 | padding-top: 4px; 351 | } 352 | 353 | .download-button { 354 | display:flex; 355 | padding-bottom: 10px; 356 | padding-top: 10px; 357 | } 358 | 359 | .download-button .secondary { 360 | font-size: .775em; 361 | margin-bottom: 0; 362 | } 363 | 364 | .download-button .container { 365 | display: flex; 366 | padding-left: 10px; 367 | padding-right: 40px; 368 | } 369 | 370 | .download-button .icon-container { 371 | fill: white; 372 | width: 30px; 373 | margin-right: 15px; 374 | } 375 | 376 | iframe.reveal-demo { 377 | width: 100%; 378 | height: 350px; 379 | outline: none; 380 | } 381 | 382 | 383 | .slide-deck { 384 | border: 3px solid #dee2e6; 385 | width: 100%; 386 | height: 475px; 387 | } 388 | 389 | @media only screen and (max-width: 600px) { 390 | .slide-deck { 391 | height: 400px; 392 | } 393 | } 394 | 395 | 396 | @media (max-width: 575px) { 397 | 398 | .link-cards .card { 399 | margin-bottom: 20px; 400 | margin-right: 35px; 401 | } 402 | 403 | } 404 | 405 | @media (min-width: 576px) { 406 | .link-cards { 407 | display: flex; 408 | flex-direction: row; 409 | flex-wrap: wrap; 410 | } 411 | 412 | .link-cards .card { 413 | width: 190px; 414 | margin: 0 20px 12px 0; 415 | } 416 | 417 | 418 | } 419 | 420 | 421 | .link-cards .card { 422 | border: none; 423 | padding: 0; 424 | } 425 | 426 | .link-cards .card-title h4 { 427 | margin-top: 0; 428 | } 429 | 430 | .link-cards .card-title p { 431 | margin-bottom: 0; 432 | } 433 | 434 | .link-cards .card-subtitle { 435 | margin-bottom: 0.7rem; 436 | } 437 | 438 | .link-cards .card-body { 439 | padding: 0.5rem; 440 | padding-left: 0.1rem; 441 | } 442 | 443 | .link-cards .card-body ul { 444 | margin-bottom: 0; 445 | padding-left: 0; 446 | list-style-type: none; 447 | } 448 | 449 | .link-cards .card-body ul a { 450 | text-decoration: none; 451 | } 452 | 453 | .link-cards .card-body ul li { 454 | padding-bottom: 0.2rem; 455 | } 456 | 457 | 458 | .card .source-code { 459 | margin-top: 3px; 460 | } 461 | 462 | .carousel.card { 463 | font-size: 16px; 464 | padding-top: 2em; 465 | } 466 | 467 | .carousel.card a { 468 | text-decoration: none; 469 | } 470 | 471 | .carousel img { 472 | width: 70%; 473 | margin-bottom: 110px; 474 | } 475 | 476 | .carousel .carousel-control-prev-icon, 477 | .carousel .carousel-control-next-icon { 478 | margin-bottom: 110px; 479 | } 480 | 481 | 482 | .gallery-category { 483 | column-gap: 10px; 484 | } 485 | 486 | .btn-action-primary { 487 | color: white; 488 | background-color: #005eb8 !important; 489 | } 490 | 491 | .btn-action-primary:hover { 492 | background-color: #ffeb3b !important; 493 | color: #000 !important; 494 | } 495 | 496 | .btn-action { 497 | min-width: 165px; 498 | border-radius: 30px; 499 | border: none; 500 | } 501 | 502 | .panel-tabset[data-group="tools-tabset"] .choose-your-tool { 503 | max-width: 90px; 504 | margin-right: 25px; 505 | margin-top: 30px; 506 | font-weight: 300; 507 | font-size: 1.3rem; 508 | text-align: left; 509 | vertical-align: center; 510 | } 511 | 512 | .panel-tabset[data-group="tools-tabset"] .tab-content { 513 | border: none; 514 | padding-left: 5px; 515 | } 516 | 517 | .panel-tabset[data-group="tools-tabset"] .nav-tabs { 518 | border-bottom: none; 519 | } 520 | 521 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link { 522 | text-align: center; 523 | margin-right: 10px; 524 | margin-top: 10px; 525 | color: inherit; 526 | width: 102px; 527 | font-size: 0.8em; 528 | } 529 | 530 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link, 531 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link.active, 532 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-item.show .nav-link { 533 | border: 1px solid rgb(222, 226, 230); 534 | border-radius: 10px; 535 | } 536 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link:hover { 537 | border-color: rgb(80,146,221); 538 | border-width: 1px; 539 | } 540 | 541 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link.active, 542 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-item.show .nav-link { 543 | border-color: rgb(80,146,221); 544 | border-width: 2px; 545 | } 546 | 547 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link img { 548 | width: 65px; 549 | height: 65px; 550 | display: block; 551 | margin-bottom: 2px; 552 | } 553 | 554 | /* 555 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link { 556 | text-align: center; 557 | margin-right: 10px; 558 | margin-top: 10px; 559 | color: inherit; 560 | width: 102px; 561 | font-size: 0.8em; 562 | } 563 | 564 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link img { 565 | width: 45px; 566 | height: 45px; 567 | margin-left: 10px; 568 | display: block; 569 | margin-bottom: 2px; 570 | } 571 | */ 572 | 573 | 574 | .download-text { 575 | font-size: 1.1em; 576 | font-weight: 500; 577 | } 578 | 579 | .footnotes-end-of-document { 580 | display: none; 581 | } -------------------------------------------------------------------------------- /docs/_assets/style/styles.css: -------------------------------------------------------------------------------- 1 | 2 | /* nav bar start */ 3 | 4 | .navbar-dark { 5 | background-color: #005eb8; 6 | } 7 | 8 | .navbar-dark .navbar-nav .nav-link { 9 | background-color: #005eb8; 10 | color: #fff; 11 | } 12 | 13 | .navbar #quarto-search.type-overlay .aa-Autocomplete svg.aa-SubmitIcon { 14 | width: 26px; 15 | height: 26px; 16 | color: #fff; 17 | opacity: 1; 18 | } 19 | 20 | .navbar-dark .navbar-nav .show > .nav-link, .navbar-dark .navbar-nav .active > .nav-link, .navbar-dark .navbar-nav .nav-link.active { 21 | color: #fff; 22 | } 23 | 24 | @media (min-width: 992px) { 25 | .navbar-expand-lg .navbar-collapse { 26 | background-color: #005eb8; 27 | color: #fff; 28 | } 29 | } 30 | 31 | .navbar-dark a:hover { 32 | text-decoration: underline; 33 | color: #fff !important; 34 | } 35 | 36 | .navbar-dark a:focus { 37 | text-decoration: underline; 38 | color: #fff !important; 39 | } 40 | 41 | .dropdown-menu a:hover { 42 | text-decoration: none; 43 | background-color: #ffeb3b !important; 44 | color: #000 !important; 45 | } 46 | 47 | .navbar-dark .navbar-toggler { 48 | color: #005eb8; 49 | border-color: #fff; 50 | } 51 | 52 | .navbar-dark .navbar-toggler-icon { 53 | background-image: url("data:image/svg+xml,%3Csvg enable-background='new 0 0 24 24' id='Layer_1' version='1.1' viewBox='0 0 24 24' xml:space='preserve' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg%3E%3Cpolygon fill='none' points='12,23.5 12,11 22.5,5.5 22.5,17.5 ' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cpolyline fill='none' points='12,11 1.5,5.5 1.5,17.5 12,23.5 ' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cpolyline fill='none' points='1.5,5.5 12,0.5 22.5,5.5 ' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cline fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' x1='17.5' x2='17.5' y1='8.1190472' y2='20'/%3E%3Cline fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' x1='6.5' x2='6.5' y1='8.1190472' y2='20'/%3E%3Cpolyline fill='none' points='1.5,11.5 12,17 22.5,11.5 ' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cline fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' x1='6.875041' x2='17.6190472' y1='2.9404566' y2='8.0566893'/%3E%3Cline fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' x1='17.124958' x2='6.3809514' y1='2.9404562' y2='8.0566893'/%3E%3C/g%3E%3C/svg%3E%0A"); 54 | } 55 | /* NHS logo 56 | /* background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23fff' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); 57 | /* background-image: url("data:image/svg+xml,%3Csvg enable-background='new 0 0 24 24' id='Layer_1' version='1.1' viewBox='0 0 24 24' xml:space='preserve' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg%3E%3Cpolygon fill='none' points='12,23.5 12,11 22.5,5.5 22.5,17.5 ' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cpolyline fill='none' points='12,11 1.5,5.5 1.5,17.5 12,23.5 ' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cpolyline fill='none' points='1.5,5.5 12,0.5 22.5,5.5 ' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cline fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' x1='17.5' x2='17.5' y1='8.1190472' y2='20'/%3E%3Cline fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' x1='6.5' x2='6.5' y1='8.1190472' y2='20'/%3E%3Cpolyline fill='none' points='1.5,11.5 12,17 22.5,11.5 ' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cline fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' x1='6.875041' x2='17.6190472' y1='2.9404566' y2='8.0566893'/%3E%3Cline fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' x1='17.124958' x2='6.3809514' y1='2.9404562' y2='8.0566893'/%3E%3C/g%3E%3C/svg%3E%0A"); 58 | 59 | /* nav bar end */ 60 | 61 | @media (min-width: 991.98px) { 62 | #quarto-header { 63 | border-bottom: 1px solid #dee2e6; 64 | background-color: #005eb8; 65 | } 66 | } 67 | 68 | .navbar-brand > img { 69 | max-height: 40px; 70 | width: 90px; 71 | padding-right: 0px; 72 | } 73 | 74 | .navbar-brand-container { 75 | margin-right: 0; 76 | } 77 | 78 | 79 | @media (min-width: 1020px) { 80 | .navbar-brand-container { 81 | margin-right: 1em; 82 | } 83 | } 84 | 85 | @media (min-width: 1400px) { 86 | .content-block { 87 | max-width: 1350px; 88 | margin-left: auto; 89 | margin-right: auto; 90 | } 91 | .navbar { 92 | max-width: 1350px; 93 | left: 50%; 94 | transform: translateX(-50%); 95 | } 96 | .nav-footer { 97 | position: relative; 98 | max-width: 1350px; 99 | left: 50%; 100 | transform: translateX(-50%); 101 | } 102 | } 103 | 104 | 105 | 106 | @media (max-width: 1060px) and (min-width: 991.98px) { 107 | 108 | #navbarCollapse ul:last-of-type a.nav-link { 109 | padding-left: .25em; 110 | padding-right: .25em; 111 | } 112 | 113 | .navbar #quarto-search { 114 | margin-left: .1em; 115 | } 116 | 117 | .navbar .bi-twitter, 118 | .navbar .bi-github, 119 | .navbar .bi-rss 120 | { 121 | font-size: .8em; 122 | } 123 | } 124 | 125 | .navbar-brand > img { 126 | max-height: 36px; 127 | } 128 | 129 | 130 | .platform-table td { 131 | vertical-align: middle; 132 | } 133 | 134 | .platform-table td > div.sourceCode { 135 | margin-top: 0.3rem; 136 | margin-bottom: 0.3rem; 137 | } 138 | 139 | 140 | .document-example { 141 | opacity: 0.9; 142 | padding: 6px; 143 | font-weight: 500; 144 | margin-bottom: 1rem; 145 | } 146 | 147 | .document-example div { 148 | padding: 5px; 149 | } 150 | 151 | 152 | .document-example .citation { 153 | color: blue; 154 | } 155 | 156 | .trademark { 157 | font-size: 0.6rem; 158 | display: inline-block; 159 | margin-left: -3px; 160 | } 161 | 162 | .search-attribution { 163 | margin-top: 20px; 164 | padding-bottom: 20px; 165 | height: 40px; 166 | } 167 | 168 | .download-button { 169 | margin-top: 1em; 170 | } 171 | 172 | .download-table { 173 | margin-bottom: 2em; 174 | } 175 | 176 | .download-table p { 177 | margin-bottom: 0; 178 | } 179 | 180 | .download-table .checksum { 181 | color: var(--bs-primary); 182 | font-size: .775em; 183 | cursor: pointer; 184 | padding-top: 4px; 185 | } 186 | 187 | .download-button { 188 | display:flex; 189 | padding-bottom: 10px; 190 | padding-top: 10px; 191 | } 192 | 193 | .download-button .secondary { 194 | font-size: .775em; 195 | margin-bottom: 0; 196 | } 197 | 198 | .download-button .container { 199 | display: flex; 200 | padding-left: 10px; 201 | padding-right: 40px; 202 | } 203 | 204 | .download-button .icon-container { 205 | fill: white; 206 | width: 30px; 207 | margin-right: 15px; 208 | } 209 | 210 | iframe.reveal-demo { 211 | width: 100%; 212 | height: 350px; 213 | outline: none; 214 | } 215 | 216 | 217 | .slide-deck { 218 | border: 3px solid #dee2e6; 219 | width: 100%; 220 | height: 475px; 221 | } 222 | 223 | @media only screen and (max-width: 600px) { 224 | .slide-deck { 225 | height: 400px; 226 | } 227 | } 228 | 229 | 230 | @media (max-width: 575px) { 231 | 232 | .link-cards .card { 233 | margin-bottom: 20px; 234 | margin-right: 35px; 235 | } 236 | 237 | } 238 | 239 | @media (min-width: 576px) { 240 | .link-cards { 241 | display: flex; 242 | flex-direction: row; 243 | flex-wrap: wrap; 244 | } 245 | 246 | .link-cards .card { 247 | width: 190px; 248 | margin: 0 20px 12px 0; 249 | } 250 | 251 | 252 | } 253 | 254 | 255 | .link-cards .card { 256 | border: none; 257 | padding: 0; 258 | } 259 | 260 | .link-cards .card-title h4 { 261 | margin-top: 0; 262 | } 263 | 264 | .link-cards .card-title p { 265 | margin-bottom: 0; 266 | } 267 | 268 | .link-cards .card-subtitle { 269 | margin-bottom: 0.7rem; 270 | } 271 | 272 | .link-cards .card-body { 273 | padding: 0.5rem; 274 | padding-left: 0.1rem; 275 | } 276 | 277 | .link-cards .card-body ul { 278 | margin-bottom: 0; 279 | padding-left: 0; 280 | list-style-type: none; 281 | } 282 | 283 | .link-cards .card-body ul a { 284 | text-decoration: none; 285 | } 286 | 287 | .link-cards .card-body ul li { 288 | padding-bottom: 0.2rem; 289 | } 290 | 291 | 292 | .card .source-code { 293 | margin-top: 3px; 294 | } 295 | 296 | .carousel.card { 297 | font-size: 16px; 298 | padding-top: 2em; 299 | } 300 | 301 | .carousel.card a { 302 | text-decoration: none; 303 | } 304 | 305 | .carousel img { 306 | width: 70%; 307 | margin-bottom: 110px; 308 | } 309 | 310 | .carousel .carousel-control-prev-icon, 311 | .carousel .carousel-control-next-icon { 312 | margin-bottom: 110px; 313 | } 314 | 315 | 316 | .gallery-category { 317 | column-gap: 10px; 318 | } 319 | 320 | .btn-action-primary { 321 | color: white; 322 | background-color: #447099 !important; 323 | } 324 | 325 | .btn-action-primary:hover { 326 | color: white; 327 | } 328 | 329 | .btn-action { 330 | min-width: 165px; 331 | border-radius: 30px; 332 | border: none; 333 | } 334 | 335 | .panel-tabset[data-group="tools-tabset"] .choose-your-tool { 336 | max-width: 90px; 337 | margin-right: 25px; 338 | margin-top: 30px; 339 | font-weight: 300; 340 | font-size: 1.3rem; 341 | text-align: left; 342 | vertical-align: center; 343 | } 344 | 345 | .panel-tabset[data-group="tools-tabset"] .tab-content { 346 | border: none; 347 | padding-left: 5px; 348 | } 349 | 350 | .panel-tabset[data-group="tools-tabset"] .nav-tabs { 351 | border-bottom: none; 352 | } 353 | 354 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link { 355 | text-align: center; 356 | margin-right: 10px; 357 | margin-top: 10px; 358 | color: inherit; 359 | width: 102px; 360 | font-size: 0.8em; 361 | } 362 | 363 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link, 364 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link.active, 365 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-item.show .nav-link { 366 | border: 1px solid rgb(222, 226, 230); 367 | border-radius: 10px; 368 | } 369 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link:hover { 370 | border-color: rgb(80,146,221); 371 | border-width: 1px; 372 | } 373 | 374 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link.active, 375 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-item.show .nav-link { 376 | border-color: rgb(80,146,221); 377 | border-width: 2px; 378 | } 379 | 380 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link img { 381 | width: 65px; 382 | height: 65px; 383 | display: block; 384 | margin-bottom: 2px; 385 | } 386 | 387 | /* 388 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link { 389 | text-align: center; 390 | margin-right: 10px; 391 | margin-top: 10px; 392 | color: inherit; 393 | width: 102px; 394 | font-size: 0.8em; 395 | } 396 | 397 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link img { 398 | width: 45px; 399 | height: 45px; 400 | margin-left: 10px; 401 | display: block; 402 | margin-bottom: 2px; 403 | } 404 | */ 405 | 406 | 407 | .download-text { 408 | font-size: 1.1em; 409 | font-weight: 500; 410 | } 411 | -------------------------------------------------------------------------------- /docs/_site/_assets/style/styles.css: -------------------------------------------------------------------------------- 1 | 2 | /* nav bar start */ 3 | 4 | .navbar-dark { 5 | background-color: #005eb8; 6 | } 7 | 8 | .navbar-dark .navbar-nav .nav-link { 9 | background-color: #005eb8; 10 | color: #fff; 11 | } 12 | 13 | .navbar #quarto-search.type-overlay .aa-Autocomplete svg.aa-SubmitIcon { 14 | width: 26px; 15 | height: 26px; 16 | color: #fff; 17 | opacity: 1; 18 | } 19 | 20 | .navbar-dark .navbar-nav .show > .nav-link, .navbar-dark .navbar-nav .active > .nav-link, .navbar-dark .navbar-nav .nav-link.active { 21 | color: #fff; 22 | } 23 | 24 | @media (min-width: 992px) { 25 | .navbar-expand-lg .navbar-collapse { 26 | background-color: #005eb8; 27 | color: #fff; 28 | } 29 | } 30 | 31 | .navbar-dark a:hover { 32 | text-decoration: underline; 33 | color: #fff !important; 34 | } 35 | 36 | .navbar-dark a:focus { 37 | text-decoration: underline; 38 | color: #fff !important; 39 | } 40 | 41 | .dropdown-menu a:hover { 42 | text-decoration: none; 43 | background-color: #ffeb3b !important; 44 | color: #000 !important; 45 | } 46 | 47 | .navbar-dark .navbar-toggler { 48 | color: #005eb8; 49 | border-color: #fff; 50 | } 51 | 52 | .navbar-dark .navbar-toggler-icon { 53 | background-image: url("data:image/svg+xml,%3Csvg enable-background='new 0 0 24 24' id='Layer_1' version='1.1' viewBox='0 0 24 24' xml:space='preserve' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg%3E%3Cpolygon fill='none' points='12,23.5 12,11 22.5,5.5 22.5,17.5 ' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cpolyline fill='none' points='12,11 1.5,5.5 1.5,17.5 12,23.5 ' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cpolyline fill='none' points='1.5,5.5 12,0.5 22.5,5.5 ' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cline fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' x1='17.5' x2='17.5' y1='8.1190472' y2='20'/%3E%3Cline fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' x1='6.5' x2='6.5' y1='8.1190472' y2='20'/%3E%3Cpolyline fill='none' points='1.5,11.5 12,17 22.5,11.5 ' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cline fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' x1='6.875041' x2='17.6190472' y1='2.9404566' y2='8.0566893'/%3E%3Cline fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' x1='17.124958' x2='6.3809514' y1='2.9404562' y2='8.0566893'/%3E%3C/g%3E%3C/svg%3E%0A"); 54 | } 55 | /* NHS logo 56 | /* background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23fff' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); 57 | /* background-image: url("data:image/svg+xml,%3Csvg enable-background='new 0 0 24 24' id='Layer_1' version='1.1' viewBox='0 0 24 24' xml:space='preserve' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg%3E%3Cpolygon fill='none' points='12,23.5 12,11 22.5,5.5 22.5,17.5 ' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cpolyline fill='none' points='12,11 1.5,5.5 1.5,17.5 12,23.5 ' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cpolyline fill='none' points='1.5,5.5 12,0.5 22.5,5.5 ' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cline fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' x1='17.5' x2='17.5' y1='8.1190472' y2='20'/%3E%3Cline fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' x1='6.5' x2='6.5' y1='8.1190472' y2='20'/%3E%3Cpolyline fill='none' points='1.5,11.5 12,17 22.5,11.5 ' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cline fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' x1='6.875041' x2='17.6190472' y1='2.9404566' y2='8.0566893'/%3E%3Cline fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' x1='17.124958' x2='6.3809514' y1='2.9404562' y2='8.0566893'/%3E%3C/g%3E%3C/svg%3E%0A"); 58 | 59 | /* nav bar end */ 60 | 61 | @media (min-width: 991.98px) { 62 | #quarto-header { 63 | border-bottom: 1px solid #dee2e6; 64 | background-color: #005eb8; 65 | } 66 | } 67 | 68 | .navbar-brand > img { 69 | max-height: 40px; 70 | width: 90px; 71 | padding-right: 0px; 72 | } 73 | 74 | .navbar-brand-container { 75 | margin-right: 0; 76 | } 77 | 78 | 79 | @media (min-width: 1020px) { 80 | .navbar-brand-container { 81 | margin-right: 1em; 82 | } 83 | } 84 | 85 | @media (min-width: 1400px) { 86 | .content-block { 87 | max-width: 1350px; 88 | margin-left: auto; 89 | margin-right: auto; 90 | } 91 | .navbar { 92 | max-width: 1350px; 93 | left: 50%; 94 | transform: translateX(-50%); 95 | } 96 | .nav-footer { 97 | position: relative; 98 | max-width: 1350px; 99 | left: 50%; 100 | transform: translateX(-50%); 101 | } 102 | } 103 | 104 | 105 | 106 | @media (max-width: 1060px) and (min-width: 991.98px) { 107 | 108 | #navbarCollapse ul:last-of-type a.nav-link { 109 | padding-left: .25em; 110 | padding-right: .25em; 111 | } 112 | 113 | .navbar #quarto-search { 114 | margin-left: .1em; 115 | } 116 | 117 | .navbar .bi-twitter, 118 | .navbar .bi-github, 119 | .navbar .bi-rss 120 | { 121 | font-size: .8em; 122 | } 123 | } 124 | 125 | .navbar-brand > img { 126 | max-height: 36px; 127 | } 128 | 129 | 130 | .platform-table td { 131 | vertical-align: middle; 132 | } 133 | 134 | .platform-table td > div.sourceCode { 135 | margin-top: 0.3rem; 136 | margin-bottom: 0.3rem; 137 | } 138 | 139 | 140 | .document-example { 141 | opacity: 0.9; 142 | padding: 6px; 143 | font-weight: 500; 144 | margin-bottom: 1rem; 145 | } 146 | 147 | .document-example div { 148 | padding: 5px; 149 | } 150 | 151 | 152 | .document-example .citation { 153 | color: blue; 154 | } 155 | 156 | .trademark { 157 | font-size: 0.6rem; 158 | display: inline-block; 159 | margin-left: -3px; 160 | } 161 | 162 | .search-attribution { 163 | margin-top: 20px; 164 | padding-bottom: 20px; 165 | height: 40px; 166 | } 167 | 168 | .download-button { 169 | margin-top: 1em; 170 | } 171 | 172 | .download-table { 173 | margin-bottom: 2em; 174 | } 175 | 176 | .download-table p { 177 | margin-bottom: 0; 178 | } 179 | 180 | .download-table .checksum { 181 | color: var(--bs-primary); 182 | font-size: .775em; 183 | cursor: pointer; 184 | padding-top: 4px; 185 | } 186 | 187 | .download-button { 188 | display:flex; 189 | padding-bottom: 10px; 190 | padding-top: 10px; 191 | } 192 | 193 | .download-button .secondary { 194 | font-size: .775em; 195 | margin-bottom: 0; 196 | } 197 | 198 | .download-button .container { 199 | display: flex; 200 | padding-left: 10px; 201 | padding-right: 40px; 202 | } 203 | 204 | .download-button .icon-container { 205 | fill: white; 206 | width: 30px; 207 | margin-right: 15px; 208 | } 209 | 210 | iframe.reveal-demo { 211 | width: 100%; 212 | height: 350px; 213 | outline: none; 214 | } 215 | 216 | 217 | .slide-deck { 218 | border: 3px solid #dee2e6; 219 | width: 100%; 220 | height: 475px; 221 | } 222 | 223 | @media only screen and (max-width: 600px) { 224 | .slide-deck { 225 | height: 400px; 226 | } 227 | } 228 | 229 | 230 | @media (max-width: 575px) { 231 | 232 | .link-cards .card { 233 | margin-bottom: 20px; 234 | margin-right: 35px; 235 | } 236 | 237 | } 238 | 239 | @media (min-width: 576px) { 240 | .link-cards { 241 | display: flex; 242 | flex-direction: row; 243 | flex-wrap: wrap; 244 | } 245 | 246 | .link-cards .card { 247 | width: 190px; 248 | margin: 0 20px 12px 0; 249 | } 250 | 251 | 252 | } 253 | 254 | 255 | .link-cards .card { 256 | border: none; 257 | padding: 0; 258 | } 259 | 260 | .link-cards .card-title h4 { 261 | margin-top: 0; 262 | } 263 | 264 | .link-cards .card-title p { 265 | margin-bottom: 0; 266 | } 267 | 268 | .link-cards .card-subtitle { 269 | margin-bottom: 0.7rem; 270 | } 271 | 272 | .link-cards .card-body { 273 | padding: 0.5rem; 274 | padding-left: 0.1rem; 275 | } 276 | 277 | .link-cards .card-body ul { 278 | margin-bottom: 0; 279 | padding-left: 0; 280 | list-style-type: none; 281 | } 282 | 283 | .link-cards .card-body ul a { 284 | text-decoration: none; 285 | } 286 | 287 | .link-cards .card-body ul li { 288 | padding-bottom: 0.2rem; 289 | } 290 | 291 | 292 | .card .source-code { 293 | margin-top: 3px; 294 | } 295 | 296 | .carousel.card { 297 | font-size: 16px; 298 | padding-top: 2em; 299 | } 300 | 301 | .carousel.card a { 302 | text-decoration: none; 303 | } 304 | 305 | .carousel img { 306 | width: 70%; 307 | margin-bottom: 110px; 308 | } 309 | 310 | .carousel .carousel-control-prev-icon, 311 | .carousel .carousel-control-next-icon { 312 | margin-bottom: 110px; 313 | } 314 | 315 | 316 | .gallery-category { 317 | column-gap: 10px; 318 | } 319 | 320 | .btn-action-primary { 321 | color: white; 322 | background-color: #447099 !important; 323 | } 324 | 325 | .btn-action-primary:hover { 326 | color: white; 327 | } 328 | 329 | .btn-action { 330 | min-width: 165px; 331 | border-radius: 30px; 332 | border: none; 333 | } 334 | 335 | .panel-tabset[data-group="tools-tabset"] .choose-your-tool { 336 | max-width: 90px; 337 | margin-right: 25px; 338 | margin-top: 30px; 339 | font-weight: 300; 340 | font-size: 1.3rem; 341 | text-align: left; 342 | vertical-align: center; 343 | } 344 | 345 | .panel-tabset[data-group="tools-tabset"] .tab-content { 346 | border: none; 347 | padding-left: 5px; 348 | } 349 | 350 | .panel-tabset[data-group="tools-tabset"] .nav-tabs { 351 | border-bottom: none; 352 | } 353 | 354 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link { 355 | text-align: center; 356 | margin-right: 10px; 357 | margin-top: 10px; 358 | color: inherit; 359 | width: 102px; 360 | font-size: 0.8em; 361 | } 362 | 363 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link, 364 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link.active, 365 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-item.show .nav-link { 366 | border: 1px solid rgb(222, 226, 230); 367 | border-radius: 10px; 368 | } 369 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link:hover { 370 | border-color: rgb(80,146,221); 371 | border-width: 1px; 372 | } 373 | 374 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link.active, 375 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-item.show .nav-link { 376 | border-color: rgb(80,146,221); 377 | border-width: 2px; 378 | } 379 | 380 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link img { 381 | width: 65px; 382 | height: 65px; 383 | display: block; 384 | margin-bottom: 2px; 385 | } 386 | 387 | /* 388 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link { 389 | text-align: center; 390 | margin-right: 10px; 391 | margin-top: 10px; 392 | color: inherit; 393 | width: 102px; 394 | font-size: 0.8em; 395 | } 396 | 397 | .panel-tabset[data-group="tools-tabset"] .nav-tabs .nav-link img { 398 | width: 45px; 399 | height: 45px; 400 | margin-left: 10px; 401 | display: block; 402 | margin-bottom: 2px; 403 | } 404 | */ 405 | 406 | 407 | .download-text { 408 | font-size: 1.1em; 409 | font-weight: 500; 410 | } 411 | -------------------------------------------------------------------------------- /nhspy_plotthedots/pandas_spc_calculations.py: -------------------------------------------------------------------------------- 1 | # Python source 2 | # ------------------------------------------------------------------------- 3 | # Copyright (c) 2023 NHS Python Community. All rights reserved. 4 | # Licensed under the MIT License. See license.txt in the project root for 5 | # license information. 6 | # ------------------------------------------------------------------------- 7 | 8 | # FILE: pandas_spc_calculations.py 9 | 10 | # DESCRIPTION: Calculations for pandas_spc_x_calc() function. 11 | 12 | # SOURCE https://gist.github.com/tomjemmett/c167376e5b6464ec1c00975be2d7864e 13 | # CONTRIBUTORS: Craig R. Shenton 14 | # CONTACT: craig.shenton@nhs.net 15 | # CREATED: 21 Jan 2023 16 | # VERSION: 0.0.1 17 | 18 | # Imports 19 | # ------------------------------------------------------------------------- 20 | # Python: 21 | from typing import List, Optional, Tuple 22 | 23 | # 3rd party: 24 | import numpy as np 25 | import pandas as pd 26 | 27 | # Define seven_point_one_side_mean() 28 | # ------------------------------------------------------------------------- 29 | def seven_point_one_side_mean(relative_to_mean: List[float]) -> List[bool]: 30 | """ 31 | Parameters: 32 | values (List[float]): List of float numbers 33 | 34 | Returns: 35 | List[bool]: A list of boolean values 36 | """ 37 | # pad the vector with 6 zero's at the beginning 38 | vp = np.insert(relative_to_mean, 0, [0] * 6) 39 | 40 | return [ 41 | np.all(vp[i + 6] == vp[i:(i + 6)]) # and (vp[i + 6] != 0) 42 | for i in range(len(relative_to_mean)) 43 | ] 44 | 45 | # Define seven_point_trend() 46 | # ------------------------------------------------------------------------- 47 | def seven_point_trend(values: List[float]) -> List[int]: 48 | """ 49 | Given a list of floats, this function checks for a trend in the last 7 values. 50 | It returns a list of integers indicating the trend (-1 for decreasing trend, 51 | 1 for increasing trend, and 0 for no trend) after the 6th consecutive change. 52 | 53 | Parameters: 54 | values (List[float]): List of float numbers 55 | 56 | Returns: 57 | List[int]: List of integers indicating the trend (-1 for decreasing trend, 58 | 1 for increasing trend, and 0 for no trend) after the 6th consecutive change. 59 | """ 60 | # edge case: len(values) < 7 61 | # check if the number of elements in the input list is less than 7 62 | if len(values) < 7: 63 | # return a list of zeroes of the same length as the input list 64 | return np.zeros(len(values), dtype=int).tolist() 65 | 66 | # calculate the difference between consecutive values and store them in the 67 | # diff variable, with 6 zeros to indicate no change before the values begin. 68 | diff = np.insert(np.diff(values), 0, [0] * 6) 69 | 70 | # create an empty list to store the trend 71 | trend = [] 72 | 73 | for i in range(len(diff)-5): 74 | # Check if all the differences in the last 6 elements are positive 75 | if all(x>0 for x in diff[i:i+6]): 76 | # Append 1 to the trend list, indicating an increasing trend 77 | trend.append(1) 78 | # Check if all the differences in the last 6 elements are negative 79 | elif all(x<0 for x in diff[i:i+6]): 80 | # Append -1 to the trend list, indicating a decreasing trend 81 | trend.append(-1) 82 | else: 83 | # Append 0 to the trend list, indicating no trend 84 | trend.append(0) 85 | return trend 86 | 87 | # Define part_of_seven_trend() 88 | # ------------------------------------------------------------------------- 89 | def part_of_seven_trend(values: List[float]) -> List[bool]: 90 | """ 91 | Check if there is a trend of 7 elements where at least one element has 92 | an absolute value of 1. 93 | 94 | Parameters: 95 | values (List[float]): List of float numbers 96 | 97 | Returns: 98 | List[bool]: A list of boolean values representing whether there is 99 | a trend of 7 elements where at least one element has an absolute 100 | value of 1 101 | """ 102 | # pad the vector with 6 zero's at the end 103 | vp = np.insert(values, len(values), [0] * 6) 104 | 105 | return [ 106 | np.any(np.abs(vp[i:(i + 7)]) == 1) 107 | for i in range(len(values)) 108 | ] 109 | 110 | 111 | # Define two_in_three() 112 | # ------------------------------------------------------------------------- 113 | def two_in_three(close_to_limits: List[bool], relative_to_mean: List[float]) -> List[bool]: 114 | # sourcery skip: simplify-len-comparison 115 | """ 116 | Check if there is a trend of 3 elements where at least 2 elements are 117 | close to limits and sum of relative to mean is 3. 118 | 119 | Parameters: 120 | - close_to_limits (List[bool]): List of boolean values representing 121 | whether an element is close to a limit or not 122 | - relative_to_mean (List[float]): List of float numbers representing 123 | the relative value of an element to mean 124 | 125 | Returns: 126 | List[bool]: A list of boolean values representing whether there is 127 | a trend of 3 elements where at least 2 elements are close to limits 128 | and sum of relative to mean is 3 129 | """ 130 | if len(close_to_limits) == 0: 131 | return [] 132 | # pad the vectors with two 0 at start, two 0 at end 133 | close_to_limits_pad = np.pad(close_to_limits, 2, "constant", constant_values=False) 134 | relative_to_mean_pad = np.pad(relative_to_mean, 2, "constant", constant_values=0) 135 | 136 | return [ 137 | np.any([ 138 | sum(close_to_limits_pad[j:(j+3)]) >= 2 and abs(sum(relative_to_mean_pad[j:(j+3)])) == 3 139 | for j in range(i, i+3) 140 | ]) 141 | for i in range(len(close_to_limits)) 142 | ] 143 | 144 | 145 | # Define part_of_two_in_three() 146 | # ------------------------------------------------------------------------- 147 | def part_of_two_in_three(two_in_three: List[bool], close_to_limits: List[bool]) -> List[bool]: 148 | """ 149 | The function uses the zip() function to iterate over the two input lists 150 | and applies a logical AND operation on the corresponding elements and 151 | returns a list of the results. 152 | 153 | Parameters: 154 | - two_in_three (List[bool]): List of boolean values representing whether 155 | an element is part of a trend of 3 elements where at least 2 elements are 156 | close to limits and sum of relative to mean is 3 157 | - close_to_limits (List[bool]): List of boolean values representing 158 | whether an element is close to a limit or not 159 | 160 | Returns: 161 | List[bool]: A list of boolean values representing whether an element is 162 | both close to limits and part of a trend of 3 elements where at least 2 163 | elements are close to limits and sum of relative to mean is 3 164 | """ 165 | return [ 166 | i and j 167 | for i, j in zip(close_to_limits, two_in_three) 168 | ] 169 | 170 | # Define special_cause_flag() 171 | # ------------------------------------------------------------------------- 172 | def special_cause_flag(values: List[float], 173 | outside_limits: List[bool], 174 | close_to_limits: List[bool], 175 | relative_to_mean: List[float]) -> List[bool]: 176 | """ 177 | Check if an element is a special cause based on 4 conditions: 178 | 1. It is outside the limits 179 | 2. It is part of a trend of 7 elements where at least one element has an 180 | absolute value of 1 181 | 3. It is part of a trend of 7 elements where the sum of the relative value 182 | of elements to mean is 1 183 | 4. It is part of a trend of 3 elements where at least 2 elements are close 184 | to limits and sum of relative to mean is 3 185 | 186 | Parameters: 187 | - values (List[float]): List of float numbers 188 | - outside_limits (List[bool]): List of boolean values representing whether 189 | an element is outside the limits or not 190 | - close_to_limits (List[bool]): List of boolean values representing whether 191 | an element is close to a limit or not 192 | - relative_to_mean (List[float]): List of float numbers representing the 193 | relative value of an element to mean 194 | 195 | Returns: 196 | List[bool]: A list of boolean values representing whether an element is a 197 | special cause or not 198 | """ 199 | return ( 200 | outside_limits | 201 | part_of_seven_trend(seven_point_one_side_mean(relative_to_mean)) | 202 | part_of_seven_trend(seven_point_trend(values)) | 203 | part_of_two_in_three(two_in_three(close_to_limits, relative_to_mean), close_to_limits) 204 | ) 205 | 206 | # Define limits_calculations() 207 | # ------------------------------------------------------------------------- 208 | def limits_calculations(fix_values: List[float]) -> Tuple[float, float, float, float]: 209 | """ 210 | Calculates the limits for a given list of values. 211 | 212 | Parameters: 213 | - fix_values (List[float]): The list of values for which the special cause 214 | limits need to be calculated. 215 | 216 | Returns: 217 | Tuple[float, float, float, float]: A tuple containing the following values: 218 | - mean: The mean of the input values 219 | - lpl: The lower process limit of the input values 220 | - upl: The upper process limit of the input values 221 | - nlpl: The near lower process limit of the input values 222 | - nupl: The near upper process limit of the input values 223 | """ 224 | # constant limit value 225 | limit = 2.66 226 | 227 | mean = np.mean(fix_values) 228 | mr = np.abs(np.diff(fix_values)) 229 | amr = np.mean(mr) 230 | 231 | # screen for outliers 232 | mr = mr[mr < 3.267 * amr] 233 | amr = np.mean(mr) 234 | 235 | lpl = mean - (limit * amr) 236 | upl = mean + (limit * amr) 237 | # identify near lower/upper process limits 238 | nlpl = mean - (limit * 2 / 3 * amr) 239 | nupl = mean + (limit * 2 / 3 * amr) 240 | return mean, lpl, upl, nlpl, nupl 241 | 242 | # Define pandas_spc_x_calc() 243 | # ------------------------------------------------------------------------- 244 | def pandas_spc_x_calc(df: pd.DataFrame, 245 | values_col: str, 246 | fix_after_n_points: Optional[int] = None) -> pd.DataFrame: 247 | """ 248 | Calculates the SPC for a given DataFrame with a set column of values 249 | 250 | Parameters: 251 | - values (List[float]): The list of values for which SPC needs to be 252 | calculated. 253 | - fix_after_n_points (Optional[int]): The number of values after which 254 | the mean and other calculations should be fixed. 255 | 256 | Returns: 257 | pd.DataFrame: The input DataFrame with additional columns: 258 | - mean: The mean of the input values 259 | - lpl: The lower process limit of the input values 260 | - upl: The upper process limit of the input values 261 | - outside_limits: A boolean list representing whether a value is 262 | outside the process limits 263 | - relative_to_mean: A list representing the relative value of an 264 | element to mean 265 | - close_to_limits: A boolean list representing whether a value is 266 | close to a limit or not 267 | - special_cause_flag: A boolean list representing whether a value 268 | is a special cause or not'outside_limits' representing whether a 269 | value is outside the process limits 270 | """ 271 | values = df[values_col].values 272 | fix_values = values[:fix_after_n_points] if fix_after_n_points else values 273 | mean, lpl, upl, nlpl, nupl = limits_calculations(fix_values) 274 | 275 | # identify any points which are outside the upper or lower process limits 276 | outside_limits = (values < lpl) | (values > upl) 277 | # identify whether a point is above or below the mean 278 | relative_to_mean = np.sign(values - mean) 279 | 280 | # identify if a point is between the near process limits and process limits 281 | close_to_limits = ~outside_limits & ((values < nlpl) | (values > nupl)) 282 | 283 | # create output pandas dataframe from numpy calculations 284 | output_df = df 285 | output_df['mean'] = mean 286 | output_df['lpl'] = lpl 287 | output_df['upl'] = upl 288 | output_df['outside_limits'] = outside_limits 289 | output_df['relative_to_mean'] = relative_to_mean 290 | output_df['close_to_limits'] = close_to_limits 291 | output_df['special_cause_flag'] = special_cause_flag(values, outside_limits, close_to_limits, relative_to_mean) 292 | 293 | return output_df -------------------------------------------------------------------------------- /docs/_site/documentation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | nhspy-plotthedots - Documentation 11 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
73 |
74 | 145 | 153 |
154 | 155 |
156 | 157 | 213 | 214 | 217 | 218 |
219 | 220 |
221 |
222 |

Documentation

223 |
224 | 225 | 226 | 227 |
228 | 229 | 230 | 231 | 232 |
233 | 234 | 235 |
236 | 237 |
238 |
239 |
240 | 241 |
242 |
243 | Under Development 244 |
245 |
246 |
247 | 248 |
249 |
250 | 251 | 252 | 253 |
254 | 266 | 399 | 411 |
412 | 427 | 428 | 429 | 430 | --------------------------------------------------------------------------------